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