PageRenderTime 79ms CodeModel.GetById 2ms app.highlight 68ms RepoModel.GetById 1ms app.codeStats 1ms

/src/PanoJS.js

http://panojs.googlecode.com/
JavaScript | 924 lines | 627 code | 122 blank | 175 comment | 153 complexity | 4911dd31d1d9f8c352c877899154ea34 MD5 | raw file
  1/**
  2 * Panoramic JavaScript Image Viewer (PanoJS) 1.0.3-SVN
  3 *
  4 * Generates a draggable and zoomable viewer for images that would
  5 * be otherwise too large for a browser window.  Examples would include
  6 * maps or high resolution document scans.
  7 *
  8 * Images must be precut into tiles, such as by the accompanying tilemaker.py
  9 * python library.
 10 *
 11 * <div class="viewer">
 12 *   <div class="well"><!-- --></div>
 13 *   <div class="surface"><!-- --></div>
 14 *   <div class="controls">
 15 *     <a href="#" class="zoomIn">+</a>
 16 *     <a href="#" class="zoomOut">-</a>
 17 *   </div>
 18 * </div>
 19 * 
 20 * The "well" node is where generated IMG elements are appended. It
 21 * should have the CSS rule "overflow: hidden", to occlude image tiles
 22 * that have scrolled out of view.
 23 * 
 24 * The "surface" node is the transparent mouse-responsive layer of the
 25 * image viewer, and should match the well in size.
 26 *
 27 * var viewerBean = new PanoJS(element, 'tiles', 256, 3, 1);
 28 *
 29 * To disable the image toolbar in IE, be sure to add the following:
 30 * <meta http-equiv="imagetoolbar" content="no" />
 31 *
 32 * Copyright (c) 2005 Michal Migurski <mike-gsv@teczno.com>
 33 *                    Dan Allen <dan.allen@mojavelinux.com>
 34 * 
 35 * Redistribution and use in source form, with or without modification,
 36 * are permitted provided that the following conditions are met:
 37 * 1. Redistributions of source code must retain the above copyright
 38 *    notice, this list of conditions and the following disclaimer.
 39 * 2. The name of the author may not be used to endorse or promote products
 40 *    derived from this software without specific prior written permission.
 41 * 
 42 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 43 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 44 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 45 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 46 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 47 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 48 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 49 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 50 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 51 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 52 *
 53 * @author Michal Migurski <mike-gsv@teczno.com>
 54 * @author Dan Allen <dan.allen@mojavelinux.com>
 55 *
 56 * NOTE: if artifacts are appearing, then positions include half-pixels
 57 * TODO: additional jsdoc and package jsmin
 58 * TODO: Tile could be an object
 59 */
 60function PanoJS(viewer, options) {
 61
 62	// listeners that are notified on a move (pan) event
 63	this.viewerMovedListeners = [];
 64	// listeners that are notified on a zoom event
 65	this.viewerZoomedListeners = [];
 66
 67	if (typeof viewer == 'string') {
 68		this.viewer = document.getElementById(viewer);
 69	}
 70	else {
 71		this.viewer = viewer;
 72	}
 73
 74	if (typeof options == 'undefined') {
 75		options = {};
 76	}
 77
 78	if (typeof options.tileUrlProvider != 'undefined' &&
 79		PanoJS.isInstance(options.tileUrlProvider, PanoJS.TileUrlProvider)) {
 80		this.tileUrlProvider = options.tileUrlProvider;
 81	}
 82	else {
 83		this.tileUrlProvider = new PanoJS.TileUrlProvider(
 84			options.tileBaseUri ? options.tileBaseUri : PanoJS.TILE_BASE_URI,
 85			options.tilePrefix ? options.tilePrefix : PanoJS.TILE_PREFIX,
 86			options.tileExtension ? options.tileExtension : PanoJS.TILE_EXTENSION
 87		);
 88	}
 89
 90	this.tileSize = (options.tileSize ? options.tileSize : PanoJS.TILE_SIZE);
 91
 92	// assign and do some validation on the zoom levels to ensure sanity
 93	this.zoomLevel = (typeof options.initialZoom == 'undefined' ? -1 : parseInt(options.initialZoom));
 94	this.maxZoomLevel = (typeof options.maxZoom == 'undefined' ? 0 : Math.abs(parseInt(options.maxZoom)));
 95	if (this.zoomLevel > this.maxZoomLevel) {
 96		this.zoomLevel = this.maxZoomLevel;
 97	}
 98
 99	this.initialPan = (options.initialPan ? options.initialPan : PanoJS.INITIAL_PAN);
100
101	this.initialized = false;
102	this.surface = null;
103	this.well = null;
104	this.width = 0;
105	this.height = 0;
106	this.top = 0;
107	this.left = 0;
108	this.x = 0;
109	this.y = 0;
110	this.border = -1;
111	this.mark = { 'x' : 0, 'y' : 0 };
112	this.pressed = false;
113	this.tiles = [];
114	this.cache = {};
115	var blankTile = options.blankTile ? options.blankTile : PanoJS.BLANK_TILE_IMAGE;
116	var loadingTile = options.loadingTile ? options.loadingTile : PanoJS.LOADING_TILE_IMAGE;
117	this.cache['blank'] = new Image();
118	this.cache['blank'].src = blankTile;
119	if (blankTile != loadingTile) {
120		this.cache['loading'] = new Image();
121		this.cache['loading'].src = loadingTile;
122	}
123	else {
124		this.cache['loading'] = this.cache['blank'];
125	}
126
127	// employed to throttle the number of redraws that
128	// happen while the mouse is moving
129	this.moveCount = 0;
130	this.slideMonitor = 0;
131	this.slideAcceleration = 0;
132
133	// add to viewer registry
134	PanoJS.VIEWERS[PanoJS.VIEWERS.length] = this;
135}
136
137// project specific variables
138PanoJS.PROJECT_NAME = 'PanoJS';
139PanoJS.PROJECT_VERSION = '1.0.0';
140PanoJS.REVISION_FLAG = '';
141
142// CSS definition settings
143PanoJS.SURFACE_STYLE_CLASS = 'surface';
144PanoJS.WELL_STYLE_CLASS = 'well';
145PanoJS.CONTROLS_STYLE_CLASS = 'controls'
146PanoJS.TILE_STYLE_CLASS = 'tile';
147
148// language settings
149PanoJS.MSG_BEYOND_MIN_ZOOM = 'Cannot zoom out past the current level.';
150PanoJS.MSG_BEYOND_MAX_ZOOM = 'Cannot zoom in beyond the current level.';
151
152// defaults if not provided as constructor options
153PanoJS.TILE_BASE_URI = 'tiles';
154PanoJS.TILE_PREFIX = 'tile-';
155PanoJS.TILE_EXTENSION = 'jpg';
156PanoJS.TILE_SIZE = 256;
157PanoJS.BLANK_TILE_IMAGE = 'blank.gif';
158PanoJS.LOADING_TILE_IMAGE = 'blank.gif';
159PanoJS.INITIAL_PAN = { 'x' : .5, 'y' : .5 };
160PanoJS.USE_LOADER_IMAGE = true;
161PanoJS.USE_SLIDE = true;
162PanoJS.USE_KEYBOARD = true;
163
164// performance tuning variables
165PanoJS.MOVE_THROTTLE = 3;
166PanoJS.SLIDE_DELAY = 40;
167PanoJS.SLIDE_ACCELERATION_FACTOR = 5;
168
169// the following are calculated settings
170PanoJS.DOM_ONLOAD = (navigator.userAgent.indexOf('KHTML') >= 0 ? false : true);
171PanoJS.GRAB_MOUSE_CURSOR = (navigator.userAgent.search(/KHTML|Opera/i) >= 0 ? 'pointer' : (document.attachEvent ? 'url(grab.cur)' : '-moz-grab'));
172PanoJS.GRABBING_MOUSE_CURSOR = (navigator.userAgent.search(/KHTML|Opera/i) >= 0 ? 'move' : (document.attachEvent ? 'url(grabbing.cur)' : '-moz-grabbing'));
173
174// registry of all known viewers
175PanoJS.VIEWERS = [];
176
177// utility functions
178PanoJS.isInstance = function(object, clazz) {
179	// FIXME: can this just be replaced with instanceof operator? It has been reported that __proto__ is specific to Netscape
180	while (object != null) {
181		if (object == clazz.prototype) {
182			return true;
183		}
184
185		object = object.__proto__;
186	}
187
188	return false;
189}
190
191PanoJS.prototype = {
192
193	/**
194	 * Resize the viewer to fit snug inside the browser window (or frame),
195	 * spacing it from the edges by the specified border.
196	 *
197	 * This method should be called prior to init()
198	 * FIXME: option to hide viewer to prevent scrollbar interference
199	 */
200	fitToWindow : function(border) {
201		if (typeof border != 'number' || border < 0) {
202			border = 0;
203		}
204
205		this.border = border;
206		var calcWidth = 0;
207		var calcHeight = 0;
208		if (window.innerWidth) {
209			calcWidth = window.innerWidth;
210			calcHeight = window.innerHeight;
211		}
212		else {
213			calcWidth = (document.compatMode == 'CSS1Compat' ? document.documentElement.clientWidth : document.body.clientWidth);
214			calcHeight = (document.compatMode == 'CSS1Compat' ? document.documentElement.clientHeight : document.body.clientHeight);
215		}
216		
217		calcWidth = Math.max(calcWidth - 2 * border, 0);
218		calcHeight = Math.max(calcHeight - 2 * border, 0);
219		if (calcWidth % 2) {
220			calcWidth--;
221		}
222
223		if (calcHeight % 2) {
224			calcHeight--;
225		}
226
227		this.width = calcWidth;
228		this.height = calcHeight;
229		this.viewer.style.width = this.width + 'px';
230		this.viewer.style.height = this.height + 'px';
231		this.viewer.style.top = border + 'px';
232		this.viewer.style.left = border + 'px';
233	},
234
235	init : function() {
236		if (document.attachEvent) {
237			document.body.ondragstart = function() { return false; }
238		}
239		
240		if (this.width == 0 && this.height == 0) {
241			this.width = this.viewer.offsetWidth;
242			this.height = this.viewer.offsetHeight;
243		}
244
245		var fullSize = this.tileSize;
246		// explicit set of zoom level
247		if (this.zoomLevel >= 0 && this.zoomLevel <= this.maxZoomLevel) {
248			fullSize = this.tileSize * Math.pow(2, this.zoomLevel);
249		}
250		// calculate the zoom level based on what fits best in window
251		else {
252			this.zoomLevel = -1;
253			fullSize = this.tileSize / 2;
254			do {
255				this.zoomLevel += 1;
256				fullSize *= 2;
257			} while (fullSize < Math.max(this.width, this.height));
258			// take into account picture smaller than window size
259			if (this.zoomLevel > this.maxZoomLevel) {
260				var diff = this.zoomLevel - this.maxZoomLevel;
261				this.zoomLevel = this.maxZoomLevel;
262				fullSize /= Math.pow(2, diff);
263			}
264		}
265
266		// move top level up and to the left so that the image is centered
267		this.x = Math.floor((fullSize - this.width) * -this.initialPan.x);
268		this.y = Math.floor((fullSize - this.height) * -this.initialPan.y);
269
270		// offset of viewer in the window
271		for (var node = this.viewer; node; node = node.offsetParent) {
272			this.top += node.offsetTop;
273			this.left += node.offsetLeft;
274		}
275
276		for (var child = this.viewer.firstChild; child; child = child.nextSibling) {
277			if (child.className == PanoJS.SURFACE_STYLE_CLASS) {
278				this.surface = child;
279				child.backingBean = this;
280			}
281			else if (child.className == PanoJS.WELL_STYLE_CLASS) {
282				this.well = child;
283				child.backingBean = this;
284			}
285			else if (child.className == PanoJS.CONTROLS_STYLE_CLASS) {
286				for (var control = child.firstChild; control; control = control.nextSibling) {
287					if (control.className) {
288						control.onclick = PanoJS[control.className + 'Handler'];
289					}
290				}
291			}
292		}
293
294		this.viewer.backingBean = this;
295		this.surface.style.cursor = PanoJS.GRAB_MOUSE_CURSOR;
296		this.prepareTiles();
297		this.initialized = true;
298	},
299
300	prepareTiles : function() {
301		var rows = Math.ceil(this.height / this.tileSize) + 1;
302		var cols = Math.ceil(this.width / this.tileSize) + 1;
303
304		for (var c = 0; c < cols; c++) {
305			var tileCol = [];
306
307			for (var r = 0; r < rows; r++) {
308				/**
309				 * element is the DOM element associated with this tile
310				 * posx/posy are the pixel offsets of the tile
311				 * xIndex/yIndex are the index numbers of the tile segment
312				 * qx/qy represents the quadrant location of the tile
313				 */
314				var tile = {
315					'element' : null,
316					'posx' : 0,
317					'posy' : 0,
318					'xIndex' : c,
319					'yIndex' : r,
320					'qx' : c,
321					'qy' : r
322				};
323
324				tileCol.push(tile);
325			}
326		
327			this.tiles.push(tileCol);
328		}
329
330		this.surface.onmousedown = PanoJS.mousePressedHandler;
331		this.surface.onmouseup = this.surface.onmouseout = PanoJS.mouseReleasedHandler;
332		this.surface.ondblclick = PanoJS.doubleClickHandler;
333		if (PanoJS.USE_KEYBOARD) {
334			window.onkeypress = PanoJS.keyboardMoveHandler;
335			window.onkeydown = PanoJS.keyboardZoomHandler;
336		}
337
338		this.positionTiles();
339	},
340
341	/**
342	 * Position the tiles based on the x, y coordinates of the
343	 * viewer, taking into account the motion offsets, which
344	 * are calculated by a motion event handler.
345	 */
346	positionTiles : function(motion, reset) {
347		// default to no motion, just setup tiles
348		if (typeof motion == 'undefined') {
349			motion = { 'x' : 0, 'y' : 0 };
350		}
351
352		for (var c = 0; c < this.tiles.length; c++) {
353			for (var r = 0; r < this.tiles[c].length; r++) {
354				var tile = this.tiles[c][r];
355
356				tile.posx = (tile.xIndex * this.tileSize) + this.x + motion.x;
357				tile.posy = (tile.yIndex * this.tileSize) + this.y + motion.y;
358
359				var visible = true;
360
361				if (tile.posx > this.width) {
362					// tile moved out of view to the right
363					// consider the tile coming into view from the left
364					do {
365						tile.xIndex -= this.tiles.length;
366						tile.posx = (tile.xIndex * this.tileSize) + this.x + motion.x;
367					} while (tile.posx > this.width);
368
369					if (tile.posx + this.tileSize < 0) {
370						visible = false;
371					}
372
373				} else {
374					// tile may have moved out of view from the left
375					// if so, consider the tile coming into view from the right
376					while (tile.posx < -this.tileSize) {
377						tile.xIndex += this.tiles.length;
378						tile.posx = (tile.xIndex * this.tileSize) + this.x + motion.x;
379					}
380
381					if (tile.posx > this.width) {
382						visible = false;
383					}
384				}
385
386				if (tile.posy > this.height) {
387					// tile moved out of view to the bottom
388					// consider the tile coming into view from the top
389					do {
390						tile.yIndex -= this.tiles[c].length;
391						tile.posy = (tile.yIndex * this.tileSize) + this.y + motion.y;
392					} while (tile.posy > this.height);
393
394					if (tile.posy + this.tileSize < 0) {
395						visible = false;
396					}
397
398				} else {
399					// tile may have moved out of view to the top
400					// if so, consider the tile coming into view from the bottom
401					while (tile.posy < -this.tileSize) {
402						tile.yIndex += this.tiles[c].length;
403						tile.posy = (tile.yIndex * this.tileSize) + this.y + motion.y;
404					}
405
406					if (tile.posy > this.height) {
407						visible = false;
408					}
409				}
410
411				// initialize the image object for this quadrant
412				if (!this.initialized) {
413					this.assignTileImage(tile, true);
414					tile.element.style.top = tile.posy + 'px';
415					tile.element.style.left = tile.posx + 'px';
416				}
417
418				// display the image if visible
419				if (visible) {
420					this.assignTileImage(tile);
421				}
422
423				// seems to need this no matter what
424				tile.element.style.top = tile.posy + 'px';
425				tile.element.style.left = tile.posx + 'px';
426			}
427		}
428
429		// reset the x, y coordinates of the viewer according to motion
430		if (reset) {
431			this.x += motion.x;
432			this.y += motion.y;
433		}
434	},
435
436	/**
437	 * Determine the source image of the specified tile based
438	 * on the zoom level and position of the tile.  If forceBlankImage
439	 * is specified, the source should be automatically set to the
440	 * null tile image.  This method will also setup an onload
441	 * routine, delaying the appearance of the tile until it is fully
442	 * loaded, if configured to do so.
443	 */
444	assignTileImage : function(tile, forceBlankImage) {
445		var tileImgId, src;
446		var useBlankImage = (forceBlankImage ? true : false);
447
448		// check if image has been scrolled too far in any particular direction
449		// and if so, use the null tile image
450		if (!useBlankImage) {
451			var left = tile.xIndex < 0;
452			var high = tile.yIndex < 0;
453			var right = tile.xIndex >= Math.pow(2, this.zoomLevel);
454			var low = tile.yIndex >= Math.pow(2, this.zoomLevel);
455			if (high || left || low || right) {
456				useBlankImage = true;
457			}
458		}
459
460		if (useBlankImage) {
461			tileImgId = 'blank:' + tile.qx + ':' + tile.qy;
462			src = this.cache['blank'].src;
463		}
464		else {
465			tileImgId = src = this.tileUrlProvider.assembleUrl(tile.xIndex, tile.yIndex, this.zoomLevel);
466		}
467
468		// only remove tile if identity is changing
469		if (tile.element != null &&
470			tile.element.parentNode != null &&
471			tile.element.relativeSrc != src) {
472			this.well.removeChild(tile.element);
473		}
474
475		var tileImg = this.cache[tileImgId];
476		// create cache if not exist
477		if (tileImg == null) {
478			tileImg = this.cache[tileImgId] = this.createPrototype(src);
479		}
480
481		if (useBlankImage || !PanoJS.USE_LOADER_IMAGE || tileImg.complete || (tileImg.image && tileImg.image.complete)) {
482			tileImg.onload = function() {};
483			if (tileImg.image) {
484				tileImg.image.onload = function() {};
485			}
486
487			if (tileImg.parentNode == null) {
488				tile.element = this.well.appendChild(tileImg);
489			}
490		}
491		else {
492			var loadingImgId = 'loading:' + tile.qx + ':' + tile.qy;
493			var loadingImg = this.cache[loadingImgId];
494			if (loadingImg == null) {
495				loadingImg = this.cache[loadingImgId] = this.createPrototype(this.cache['loading'].src);
496			}
497
498			loadingImg.targetSrc = tileImgId;
499
500			var well = this.well;
501			tile.element = well.appendChild(loadingImg);
502			tileImg.onload = function() {
503				// make sure our destination is still present
504				if (loadingImg.parentNode && loadingImg.targetSrc == tileImgId) {
505					tileImg.style.top = loadingImg.style.top;
506					tileImg.style.left = loadingImg.style.left;
507					well.replaceChild(tileImg, loadingImg);
508					tile.element = tileImg;
509				}
510
511				tileImg.onload = function() {};
512				return false;
513			}
514
515			// konqueror only recognizes the onload event on an Image
516			// javascript object, so we must handle that case here
517			if (!PanoJS.DOM_ONLOAD) {
518				tileImg.image = new Image();
519				tileImg.image.onload = tileImg.onload;
520				tileImg.image.src = tileImg.src;
521			}
522		}
523	},
524
525	createPrototype : function(src) {
526		var img = document.createElement('img');
527		img.src = src;
528		img.relativeSrc = src;
529		img.className = PanoJS.TILE_STYLE_CLASS;
530		img.style.width = this.tileSize + 'px';
531		img.style.height = this.tileSize + 'px';
532		return img;
533	},
534
535	addViewerMovedListener : function(listener) {
536		this.viewerMovedListeners.push(listener);
537	},
538
539	addViewerZoomedListener : function(listener) {
540		this.viewerZoomedListeners.push(listener);
541	},
542
543	/**
544	 * Notify listeners of a zoom event on the viewer.
545	 */
546	notifyViewerZoomed : function() {
547		var percentage = (100/(this.maxZoomLevel + 1)) * (this.zoomLevel + 1);
548		for (var i = 0; i < this.viewerZoomedListeners.length; i++) {
549			this.viewerZoomedListeners[i].viewerZoomed(
550				new PanoJS.ZoomEvent(this.x, this.y, this.zoomLevel, percentage)
551			);
552		}
553	},
554
555	/**
556	 * Notify listeners of a move event on the viewer.
557	 */
558	notifyViewerMoved : function(coords) {
559		if (typeof coords == 'undefined') {
560			coords = { 'x' : 0, 'y' : 0 };
561		}
562
563		for (var i = 0; i < this.viewerMovedListeners.length; i++) {
564			this.viewerMovedListeners[i].viewerMoved(
565				new PanoJS.MoveEvent(
566					this.x + (coords.x - this.mark.x),
567					this.y + (coords.y - this.mark.y)
568				)
569			);
570		}
571	},
572
573	zoom : function(direction) {
574		// ensure we are not zooming out of range
575		if (this.zoomLevel + direction < 0) {
576			if (PanoJS.MSG_BEYOND_MIN_ZOOM) {
577				alert(PanoJS.MSG_BEYOND_MIN_ZOOM);
578			}
579			return;
580		}
581		else if (this.zoomLevel + direction > this.maxZoomLevel) {
582			if (PanoJS.MSG_BEYOND_MAX_ZOOM) {
583				alert(PanoJS.MSG_BEYOND_MAX_ZOOM);
584			}
585			return;
586		}
587
588		this.blank();
589
590		var coords = { 'x' : Math.floor(this.width / 2), 'y' : Math.floor(this.height / 2) };
591
592		var before = {
593			'x' : (coords.x - this.x),
594			'y' : (coords.y - this.y)
595		};
596
597		var after = {
598			'x' : Math.floor(before.x * Math.pow(2, direction)),
599			'y' : Math.floor(before.y * Math.pow(2, direction))
600		};
601
602		this.x = coords.x - after.x;
603		this.y = coords.y - after.y;
604		this.zoomLevel += direction;
605		this.positionTiles();
606
607		this.notifyViewerZoomed();
608	},
609
610	/** 
611	 * Clear all the tiles from the well for a complete reinitialization of the
612	 * viewer. At this point the viewer is not considered to be initialized.
613	 */
614	clear : function() {
615		this.blank();
616		this.initialized = false;
617		this.tiles = [];
618	},
619
620	/**
621	 * Remove all tiles from the well, which effectively "hides"
622	 * them for a repaint.
623	 */
624	blank : function() {
625		for (imgId in this.cache) {
626			var img = this.cache[imgId];
627			img.onload = function() {};
628			if (img.image) {
629				img.image.onload = function() {};
630			}
631
632			if (img.parentNode != null) {
633				this.well.removeChild(img);
634			}
635		}
636	},
637
638	/**
639	 * Method specifically for handling a mouse move event.  A direct
640	 * movement of the viewer can be achieved by calling positionTiles() directly.
641	 */
642	moveViewer : function(coords) {
643		this.positionTiles({ 'x' : (coords.x - this.mark.x), 'y' : (coords.y - this.mark.y) });
644		this.notifyViewerMoved(coords);
645	},
646
647	/**
648	 * Make the specified coords the new center of the image placement.
649	 * This method is typically triggered as the result of a double-click
650	 * event.  The calculation considers the distance between the center
651	 * of the viewable area and the specified (viewer-relative) coordinates.
652	 * If absolute is specified, treat the point as relative to the entire
653	 * image, rather than only the viewable portion.
654	 */
655	recenter : function(coords, absolute) {
656		if (absolute) {
657			coords.x += this.x;
658			coords.y += this.y;
659		}
660
661		var motion = {
662			'x' : Math.floor((this.width / 2) - coords.x),
663			'y' : Math.floor((this.height / 2) - coords.y)
664		};
665
666		if (motion.x == 0 && motion.y == 0) {
667			return;
668		}
669
670		if (PanoJS.USE_SLIDE) {
671			var target = motion;
672			var x, y;
673			// handle special case of vertical movement
674			if (target.x == 0) {
675				x = 0;
676				y = this.slideAcceleration;
677			}
678			else {
679				var slope = Math.abs(target.y / target.x);
680				x = Math.round(Math.pow(Math.pow(this.slideAcceleration, 2) / (1 + Math.pow(slope, 2)), .5));
681				y = Math.round(slope * x);
682			}
683			
684			motion = {
685				'x' : Math.min(x, Math.abs(target.x)) * (target.x < 0 ? -1 : 1),
686				'y' : Math.min(y, Math.abs(target.y)) * (target.y < 0 ? -1 : 1)
687			}
688		}
689
690		this.positionTiles(motion, true);
691		this.notifyViewerMoved();
692
693		if (!PanoJS.USE_SLIDE) {
694			return;
695		}
696
697		var newcoords = {
698			'x' : coords.x + motion.x,
699			'y' : coords.y + motion.y
700		};
701
702		var self = this;
703		// TODO: use an exponential growth rather than linear (should also depend on how far we are going)
704		// FIXME: this could be optimized by calling positionTiles directly perhaps
705		this.slideAcceleration += PanoJS.SLIDE_ACCELERATION_FACTOR;
706		this.slideMonitor = setTimeout(function() { self.recenter(newcoords); }, PanoJS.SLIDE_DELAY );
707	},
708
709	resize : function() {
710		// IE fires a premature resize event
711		if (!this.initialized) {
712			return;
713		}
714
715        var newWidth = this.viewer.offsetWidth;
716        var newHeight = this.viewer.offsetHeight;
717
718		this.viewer.style.display = 'none';
719		this.clear();
720
721		var before = {
722			'x' : Math.floor(this.width / 2),
723			'y' : Math.floor(this.height / 2)
724		};
725
726		if (this.border >= 0) {
727			this.fitToWindow(this.border);
728		}
729		else {
730            this.width = newWidth;
731            this.height = newHeight;
732        }
733
734		this.prepareTiles();
735
736		var after = {
737			'x' : Math.floor(this.width / 2),
738			'y' : Math.floor(this.height / 2)
739		};
740
741		if (this.border >= 0) {
742			this.x += (after.x - before.x);
743			this.y += (after.y - before.y);
744		}
745		this.positionTiles();
746		this.viewer.style.display = '';
747		this.initialized = true;
748		this.notifyViewerMoved();
749	},
750
751	/**
752	 * Resolve the coordinates from this mouse event by subtracting the
753	 * offset of the viewer in the browser window (or frame).  This does
754	 * take into account the scroll offset of the page.
755	 */
756	resolveCoordinates : function(e) {
757		return {
758			'x' : (e.pageX || (e.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft))) - this.left,
759			'y' : (e.pageY || (e.clientY + (document.documentElement.scrollTop || document.body.scrollTop))) - this.top
760		}
761	},
762
763	press : function(coords) {
764		this.activate(true);
765		this.mark = coords;
766	},
767
768	release : function(coords) {
769		this.activate(false);
770		var motion = {
771			'x' : (coords.x - this.mark.x),
772			'y' : (coords.y - this.mark.y)
773		};
774
775		this.x += motion.x;
776		this.y += motion.y;
777		this.mark = { 'x' : 0, 'y' : 0 };
778	},
779
780	/**
781	 * Activate the viewer into motion depending on whether the mouse is pressed or
782	 * not pressed.  This method localizes the changes that must be made to the
783	 * layers.
784	 */
785	activate : function(pressed) {
786		this.pressed = pressed;
787		this.surface.style.cursor = (pressed ? PanoJS.GRABBING_MOUSE_CURSOR : PanoJS.GRAB_MOUSE_CURSOR);
788		this.surface.onmousemove = (pressed ? PanoJS.mouseMovedHandler : function() {});
789	},
790
791	/**
792	 * Check whether the specified point exceeds the boundaries of
793	 * the viewer's primary image.
794	 */
795	pointExceedsBoundaries : function(coords) {
796		return (coords.x < this.x ||
797			coords.y < this.y ||
798			coords.x > (this.tileSize * Math.pow(2, this.zoomLevel) + this.x) ||
799			coords.y > (this.tileSize * Math.pow(2, this.zoomLevel) + this.y));
800	},
801
802	// QUESTION: where is the best place for this method to be invoked?
803	resetSlideMotion : function() {
804		// QUESTION: should this be > 0 ?	
805		if (this.slideMonitor != 0) {
806			clearTimeout(this.slideMonitor);
807			this.slideMonitor = 0;
808		}
809
810		this.slideAcceleration = 0;
811	}
812};
813
814PanoJS.TileUrlProvider = function(baseUri, prefix, extension) {
815	this.baseUri = baseUri;
816	this.prefix = prefix;
817	this.extension = extension;
818}
819
820PanoJS.TileUrlProvider.prototype = {
821	assembleUrl: function(xIndex, yIndex, zoom) {
822		return this.baseUri + '/' +
823			this.prefix + zoom + '-' + xIndex + '-' + yIndex + '.' + this.extension +
824			(PanoJS.REVISION_FLAG ? '?r=' + PanoJS.REVISION_FLAG : '');
825	}
826}
827
828PanoJS.mousePressedHandler = function(e) {
829	e = e ? e : window.event;
830	// only grab on left-click
831	if (e.button < 2) {
832		var self = this.backingBean;
833		var coords = self.resolveCoordinates(e);
834		if (self.pointExceedsBoundaries(coords)) {
835			e.cancelBubble = true;
836		}
837		else {
838			self.press(coords);
839		}
840	}
841
842	// NOTE: MANDATORY! must return false so event does not propagate to well!
843	return false;
844};
845
846PanoJS.mouseReleasedHandler = function(e) {
847	e = e ? e : window.event;
848	var self = this.backingBean;
849	if (self.pressed) {
850		// OPTION: could decide to move viewer only on release, right here
851		self.release(self.resolveCoordinates(e));
852	}
853};
854
855PanoJS.mouseMovedHandler = function(e) {
856	e = e ? e : window.event;
857	var self = this.backingBean;
858	self.moveCount++;
859	if (self.moveCount % PanoJS.MOVE_THROTTLE == 0) {
860		self.moveViewer(self.resolveCoordinates(e));
861	}
862};
863
864PanoJS.zoomInHandler = function(e) {
865	e = e ? e : window.event;
866	var self = this.parentNode.parentNode.backingBean;
867	self.zoom(1);
868	return false;
869};
870
871PanoJS.zoomOutHandler = function(e) {
872	e = e ? e : window.event;
873	var self = this.parentNode.parentNode.backingBean;
874	self.zoom(-1);
875	return false;
876};
877
878PanoJS.doubleClickHandler = function(e) {
879	e = e ? e : window.event;
880	var self = this.backingBean;
881	coords = self.resolveCoordinates(e);
882	if (!self.pointExceedsBoundaries(coords)) {
883		self.resetSlideMotion();
884		self.recenter(coords);
885	}
886};
887
888PanoJS.keyboardMoveHandler = function(e) {
889	e = e ? e : window.event;
890	for (var i = 0; i < PanoJS.VIEWERS.length; i++) {
891		var viewer = PanoJS.VIEWERS[i];
892		if (e.keyCode == 38)
893				viewer.positionTiles({'x': 0,'y': -PanoJS.MOVE_THROTTLE}, true);
894		if (e.keyCode == 39)
895				viewer.positionTiles({'x': -PanoJS.MOVE_THROTTLE,'y': 0}, true);
896		if (e.keyCode == 40)
897				viewer.positionTiles({'x': 0,'y': PanoJS.MOVE_THROTTLE}, true);
898		if (e.keyCode == 37)
899				viewer.positionTiles({'x': PanoJS.MOVE_THROTTLE,'y': 0}, true);
900	}
901}
902
903PanoJS.keyboardZoomHandler = function(e) {
904	e = e ? e : window.event;
905	for (var i = 0; i < PanoJS.VIEWERS.length; i++) {
906		var viewer = PanoJS.VIEWERS[i];
907		if (e.keyCode == 109)
908				viewer.zoom(-1);
909		if (e.keyCode == 107)
910				viewer.zoom(1);
911	}
912}
913
914PanoJS.MoveEvent = function(x, y) {
915	this.x = x;
916	this.y = y;
917};
918
919PanoJS.ZoomEvent = function(x, y, level, percentage) {
920	this.x = x;
921	this.y = y;
922	this.percentage = percentage;
923	this.level = level;
924};