xref: /plugin/panoview/script.js (revision 1faa1ff036ef1d08d80723e22dca8ede944761c0)
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() { return false; }
237        }
238
239        if (this.width == 0 && this.height == 0) {
240            this.width = this.viewer.offsetWidth;
241            this.height = this.viewer.offsetHeight;
242        }
243
244        this.calcScale();
245
246        var fullSize = this.tileSize;
247        // explicit set of zoom level
248        if (this.zoomLevel >= 0 && this.zoomLevel <= this.maxZoomLevel) {
249            fullSize = this.tileSize * Math.pow(2, this.zoomLevel);
250        }
251        // calculate the zoom level based on what fits best in window
252        else {
253            this.zoomLevel = -1;
254            fullSize = this.tileSize / 2;
255            do {
256                this.zoomLevel += 1;
257                fullSize *= 2;
258            } while (fullSize < Math.max(this.width, this.height));
259            // take into account picture smaller than window size
260            if (this.zoomLevel > this.maxZoomLevel) {
261                var diff = this.zoomLevel - this.maxZoomLevel;
262                this.zoomLevel = this.maxZoomLevel;
263                fullSize /= Math.pow(2, diff);
264            }
265        }
266
267        //fixme fullsize from above not used anymore?
268
269
270        // calculate the center
271        this.x = Math.floor(this.scaleSize.x * 0.5 - this.width * 0.5) * -1;
272        this.y = Math.floor(this.scaleSize.y * 0.5 - this.height * 0.5) * -1;
273
274
275        // offset of viewer in the window
276        for (var node = this.viewer; node; node = node.offsetParent) {
277            this.top += node.offsetTop;
278            this.left += node.offsetLeft;
279        }
280
281        for (var child = this.viewer.firstChild; child; child = child.nextSibling) {
282            if (child.className == PanoJS.SURFACE_STYLE_CLASS) {
283                this.surface = child;
284                child.backingBean = this;
285                // empty surface
286                child.innerHTML = '';
287            }
288            else if (child.className == PanoJS.WELL_STYLE_CLASS) {
289                this.well = child;
290                child.backingBean = this;
291            }
292            else if (child.className == PanoJS.CONTROLS_STYLE_CLASS) {
293                child.style.display = '';
294                for (var control = child.firstChild; control; control = control.nextSibling) {
295                    if (control.className) {
296                        control.onclick = PanoJS[control.className + 'Handler'];
297                    }
298                }
299            }
300        }
301
302        this.viewer.backingBean = this;
303        this.surface.style.cursor = PanoJS.GRAB_MOUSE_CURSOR;
304        this.prepareTiles();
305        this.initialized = true;
306    },
307
308    calcScale : function(){
309        var inv = Math.max(this.imgsize.x,this.imgsize.y)  /
310                  (this.tileSize * Math.pow(2, this.zoomLevel));
311        this.scaleSize.x = this.imgsize.x / inv;
312        this.scaleSize.y = this.imgsize.y / inv;
313    },
314
315    prepareTiles : function() {
316        var rows = Math.ceil(this.height / this.tileSize) + 1;
317        var cols = Math.ceil(this.width / this.tileSize) + 1;
318
319        for (var c = 0; c < cols; c++) {
320            var tileCol = [];
321
322            for (var r = 0; r < rows; r++) {
323                /**
324                 * element is the DOM element associated with this tile
325                 * posx/posy are the pixel offsets of the tile
326                 * xIndex/yIndex are the index numbers of the tile segment
327                 * qx/qy represents the quadrant location of the tile
328                 */
329                var tile = {
330                    'element' : null,
331                    'posx' : 0,
332                    'posy' : 0,
333                    'xIndex' : c,
334                    'yIndex' : r,
335                    'qx' : c,
336                    'qy' : r
337                };
338
339                tileCol.push(tile);
340            }
341
342            this.tiles.push(tileCol);
343        }
344
345        this.surface.onmousedown = PanoJS.mousePressedHandler;
346        this.surface.onmouseup = this.surface.onmouseout = PanoJS.mouseReleasedHandler;
347        this.surface.ondblclick = PanoJS.doubleClickHandler;
348        if (PanoJS.USE_KEYBOARD) {
349            window.onkeypress = PanoJS.keyboardMoveHandler;
350            window.onkeydown = PanoJS.keyboardZoomHandler;
351        }
352
353        this.positionTiles();
354    },
355
356    /**
357     * Position the tiles based on the x, y coordinates of the
358     * viewer, taking into account the motion offsets, which
359     * are calculated by a motion event handler.
360     */
361    positionTiles : function(motion, reset) {
362        // default to no motion, just setup tiles
363        if (typeof motion == 'undefined') {
364            motion = { 'x' : 0, 'y' : 0 };
365        }
366
367        for (var c = 0; c < this.tiles.length; c++) {
368            for (var r = 0; r < this.tiles[c].length; r++) {
369                var tile = this.tiles[c][r];
370
371                tile.posx = (tile.xIndex * this.tileSize) + this.x + motion.x;
372                tile.posy = (tile.yIndex * this.tileSize) + this.y + motion.y;
373
374                var visible = true;
375
376                if (tile.posx > this.width) {
377                    // tile moved out of view to the right
378                    // consider the tile coming into view from the left
379                    do {
380                        tile.xIndex -= this.tiles.length;
381                        tile.posx = (tile.xIndex * this.tileSize) + this.x + motion.x;
382                    } while (tile.posx > this.width);
383
384                    if (tile.posx + this.tileSize < 0) {
385                        visible = false;
386                    }
387
388                } else {
389                    // tile may have moved out of view from the left
390                    // if so, consider the tile coming into view from the right
391                    while (tile.posx < -this.tileSize) {
392                        tile.xIndex += this.tiles.length;
393                        tile.posx = (tile.xIndex * this.tileSize) + this.x + motion.x;
394                    }
395
396                    if (tile.posx > this.width) {
397                        visible = false;
398                    }
399                }
400
401                if (tile.posy > this.height) {
402                    // tile moved out of view to the bottom
403                    // consider the tile coming into view from the top
404                    do {
405                        tile.yIndex -= this.tiles[c].length;
406                        tile.posy = (tile.yIndex * this.tileSize) + this.y + motion.y;
407                    } while (tile.posy > this.height);
408
409                    if (tile.posy + this.tileSize < 0) {
410                        visible = false;
411                    }
412
413                } else {
414                    // tile may have moved out of view to the top
415                    // if so, consider the tile coming into view from the bottom
416                    while (tile.posy < -this.tileSize) {
417                        tile.yIndex += this.tiles[c].length;
418                        tile.posy = (tile.yIndex * this.tileSize) + this.y + motion.y;
419                    }
420
421                    if (tile.posy > this.height) {
422                        visible = false;
423                    }
424                }
425
426                // initialize the image object for this quadrant
427                if (!this.initialized) {
428                    this.assignTileImage(tile, true);
429                    tile.element.style.top = tile.posy + 'px';
430                    tile.element.style.left = tile.posx + 'px';
431                }
432
433                // display the image if visible
434                if (visible) {
435                    this.assignTileImage(tile);
436                }
437
438                // seems to need this no matter what
439                tile.element.style.top = tile.posy + 'px';
440                tile.element.style.left = tile.posx + 'px';
441            }
442        }
443
444        // reset the x, y coordinates of the viewer according to motion
445        if (reset) {
446            this.x += motion.x;
447            this.y += motion.y;
448        }
449    },
450
451    /**
452     * Determine the source image of the specified tile based
453     * on the zoom level and position of the tile.  If forceBlankImage
454     * is specified, the source should be automatically set to the
455     * null tile image.  This method will also setup an onload
456     * routine, delaying the appearance of the tile until it is fully
457     * loaded, if configured to do so.
458     */
459    assignTileImage : function(tile, forceBlankImage) {
460        var tileImgId, src;
461        var useBlankImage = (forceBlankImage ? true : false);
462
463        // check if image has been scrolled too far in any particular direction
464        // and if so, use the null tile image
465        if (!useBlankImage) {
466            var left = tile.xIndex < 0;
467            var high = tile.yIndex < 0;
468            var right = tile.xIndex >= Math.ceil(this.scaleSize.x / this.tileSize);
469            var low = tile.yIndex >= Math.ceil(this.scaleSize.y / this.tileSize);
470            if (high || left || low || right) {
471                useBlankImage = true;
472            }
473        }
474
475        if (useBlankImage) {
476            tileImgId = 'blank:' + tile.qx + ':' + tile.qy;
477            src = this.cache['blank'].src;
478        }
479        else {
480            tileImgId = src = this.tileUrlProvider.assembleUrl(tile.xIndex, tile.yIndex, this.zoomLevel);
481        }
482
483        // only remove tile if identity is changing
484        if (tile.element != null &&
485            tile.element.parentNode != null &&
486            tile.element.relativeSrc != src) {
487            this.well.removeChild(tile.element);
488        }
489
490        var tileImg = this.cache[tileImgId];
491        // create cache if not exist
492        if (tileImg == null) {
493            tileImg = this.cache[tileImgId] = this.createPrototype(src);
494        }
495
496        if (useBlankImage || !PanoJS.USE_LOADER_IMAGE || tileImg.complete || (tileImg.image && tileImg.image.complete)) {
497            tileImg.onload = function() {};
498            if (tileImg.image) {
499                tileImg.image.onload = function() {};
500            }
501
502            if (tileImg.parentNode == null) {
503                tile.element = this.well.appendChild(tileImg);
504            }
505        }
506        else {
507            var loadingImgId = 'loading:' + tile.qx + ':' + tile.qy;
508            var loadingImg = this.cache[loadingImgId];
509            if (loadingImg == null) {
510                loadingImg = this.cache[loadingImgId] = this.createPrototype(this.cache['loading'].src);
511            }
512
513            loadingImg.targetSrc = tileImgId;
514
515            var well = this.well;
516            tile.element = well.appendChild(loadingImg);
517            tileImg.onload = function() {
518                // make sure our destination is still present
519                if (loadingImg.parentNode && loadingImg.targetSrc == tileImgId) {
520                    tileImg.style.top = loadingImg.style.top;
521                    tileImg.style.left = loadingImg.style.left;
522                    well.replaceChild(tileImg, loadingImg);
523                    tile.element = tileImg;
524                }
525
526                tileImg.onload = function() {};
527                return false;
528            };
529
530            // konqueror only recognizes the onload event on an Image
531            // javascript object, so we must handle that case here
532            if (!PanoJS.DOM_ONLOAD) {
533                tileImg.image = new Image();
534                tileImg.image.onload = tileImg.onload;
535                tileImg.image.src = tileImg.src;
536            }
537        }
538    },
539
540    createPrototype : function(src) {
541        var img = document.createElement('img');
542        img.src = src;
543        img.relativeSrc = src;
544        img.className = PanoJS.TILE_STYLE_CLASS;
545        img.style.width = this.tileSize + 'px';
546        img.style.height = this.tileSize + 'px';
547        return img;
548    },
549
550    addViewerMovedListener : function(listener) {
551        this.viewerMovedListeners.push(listener);
552    },
553
554    addViewerZoomedListener : function(listener) {
555        this.viewerZoomedListeners.push(listener);
556    },
557
558    /**
559     * Notify listeners of a zoom event on the viewer.
560     */
561    notifyViewerZoomed : function() {
562        var percentage = (100/(this.maxZoomLevel + 1)) * (this.zoomLevel + 1);
563        for (var i = 0; i < this.viewerZoomedListeners.length; i++) {
564            this.viewerZoomedListeners[i].viewerZoomed(
565                new PanoJS.ZoomEvent(this.x, this.y, this.zoomLevel, percentage)
566            );
567        }
568    },
569
570    /**
571     * Notify listeners of a move event on the viewer.
572     */
573    notifyViewerMoved : function(coords) {
574        if (typeof coords == 'undefined') {
575            coords = { 'x' : 0, 'y' : 0 };
576        }
577
578        for (var i = 0; i < this.viewerMovedListeners.length; i++) {
579            this.viewerMovedListeners[i].viewerMoved(
580                new PanoJS.MoveEvent(
581                    this.x + (coords.x - this.mark.x),
582                    this.y + (coords.y - this.mark.y)
583                )
584            );
585        }
586    },
587
588    zoom : function(direction) {
589        // ensure we are not zooming out of range
590        if (this.zoomLevel + direction < 0) {
591            if (PanoJS.MSG_BEYOND_MIN_ZOOM) {
592                alert(PanoJS.MSG_BEYOND_MIN_ZOOM);
593            }
594            return;
595        }
596        else if (this.zoomLevel + direction > this.maxZoomLevel) {
597            if (PanoJS.MSG_BEYOND_MAX_ZOOM) {
598                alert(PanoJS.MSG_BEYOND_MAX_ZOOM);
599            }
600            return;
601        }
602
603        this.blank();
604
605        var coords = { 'x' : Math.floor(this.width / 2), 'y' : Math.floor(this.height / 2) };
606
607        var before = {
608            'x' : (coords.x - this.x),
609            'y' : (coords.y - this.y)
610        };
611
612        var after = {
613            'x' : Math.floor(before.x * Math.pow(2, direction)),
614            'y' : Math.floor(before.y * Math.pow(2, direction))
615        };
616
617        this.x = coords.x - after.x;
618        this.y = coords.y - after.y;
619        this.zoomLevel += direction;
620        this.calcScale();
621        this.positionTiles();
622
623        this.notifyViewerZoomed();
624    },
625
626    /**
627     * Clear all the tiles from the well for a complete reinitialization of the
628     * viewer. At this point the viewer is not considered to be initialized.
629     */
630    clear : function() {
631        this.blank();
632        this.initialized = false;
633        this.tiles = [];
634    },
635
636    /**
637     * Remove all tiles from the well, which effectively "hides"
638     * them for a repaint.
639     */
640    blank : function() {
641        for (imgId in this.cache) {
642            var img = this.cache[imgId];
643            img.onload = function() {};
644            if (img.image) {
645                img.image.onload = function() {};
646            }
647
648            if (img.parentNode != null) {
649                this.well.removeChild(img);
650            }
651        }
652    },
653
654    /**
655     * Method specifically for handling a mouse move event.  A direct
656     * movement of the viewer can be achieved by calling positionTiles() directly.
657     */
658    moveViewer : function(coords) {
659        this.positionTiles({ 'x' : (coords.x - this.mark.x), 'y' : (coords.y - this.mark.y) });
660        this.notifyViewerMoved(coords);
661    },
662
663    /**
664     * Make the specified coords the new center of the image placement.
665     * This method is typically triggered as the result of a double-click
666     * event.  The calculation considers the distance between the center
667     * of the viewable area and the specified (viewer-relative) coordinates.
668     * If absolute is specified, treat the point as relative to the entire
669     * image, rather than only the viewable portion.
670     */
671    recenter : function(coords, absolute) {
672        if (absolute) {
673            coords.x += this.x;
674            coords.y += this.y;
675        }
676
677        var motion = {
678            'x' : Math.floor((this.width / 2) - coords.x),
679            'y' : Math.floor((this.height / 2) - coords.y)
680        };
681
682        if (motion.x == 0 && motion.y == 0) {
683            return;
684        }
685
686        if (PanoJS.USE_SLIDE) {
687            var target = motion;
688            var x, y;
689            // handle special case of vertical movement
690            if (target.x == 0) {
691                x = 0;
692                y = this.slideAcceleration;
693            }
694            else {
695                var slope = Math.abs(target.y / target.x);
696                x = Math.round(Math.pow(Math.pow(this.slideAcceleration, 2) / (1 + Math.pow(slope, 2)), .5));
697                y = Math.round(slope * x);
698            }
699
700            motion = {
701                'x' : Math.min(x, Math.abs(target.x)) * (target.x < 0 ? -1 : 1),
702                'y' : Math.min(y, Math.abs(target.y)) * (target.y < 0 ? -1 : 1)
703            };
704        }
705
706        this.positionTiles(motion, true);
707        this.notifyViewerMoved();
708
709        if (!PanoJS.USE_SLIDE) {
710            return;
711        }
712
713        var newcoords = {
714            'x' : coords.x + motion.x,
715            'y' : coords.y + motion.y
716        };
717
718        var self = this;
719        // TODO: use an exponential growth rather than linear (should also depend on how far we are going)
720        // FIXME: this could be optimized by calling positionTiles directly perhaps
721        this.slideAcceleration += PanoJS.SLIDE_ACCELERATION_FACTOR;
722        this.slideMonitor = setTimeout(function() { self.recenter(newcoords); }, PanoJS.SLIDE_DELAY );
723    },
724
725    resize : function() {
726        // IE fires a premature resize event
727        if (!this.initialized) {
728            return;
729        }
730
731        var newWidth = this.viewer.offsetWidth;
732        var newHeight = this.viewer.offsetHeight;
733
734        this.viewer.style.display = 'none';
735        this.clear();
736
737        var before = {
738            'x' : Math.floor(this.width / 2),
739            'y' : Math.floor(this.height / 2)
740        };
741
742        if (this.border >= 0) {
743            this.fitToWindow(this.border);
744        }
745        else {
746            this.width = newWidth;
747            this.height = newHeight;
748        }
749
750        this.prepareTiles();
751
752        var after = {
753            'x' : Math.floor(this.width / 2),
754            'y' : Math.floor(this.height / 2)
755        };
756
757        if (this.border >= 0) {
758            this.x += (after.x - before.x);
759            this.y += (after.y - before.y);
760        }
761        this.positionTiles();
762        this.viewer.style.display = '';
763        this.initialized = true;
764        this.notifyViewerMoved();
765    },
766
767    /**
768     * Resolve the coordinates from this mouse event by subtracting the
769     * offset of the viewer in the browser window (or frame).  This does
770     * take into account the scroll offset of the page.
771     */
772    resolveCoordinates : function(e) {
773        return {
774            'x' : (e.pageX || (e.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft))) - this.left,
775            'y' : (e.pageY || (e.clientY + (document.documentElement.scrollTop || document.body.scrollTop))) - this.top
776        };
777    },
778
779    press : function(coords) {
780        this.activate(true);
781        this.mark = coords;
782    },
783
784    release : function(coords) {
785        this.activate(false);
786        var motion = {
787            'x' : (coords.x - this.mark.x),
788            'y' : (coords.y - this.mark.y)
789        };
790
791        this.x += motion.x;
792        this.y += motion.y;
793        this.mark = { 'x' : 0, 'y' : 0 };
794    },
795
796    /**
797     * Activate the viewer into motion depending on whether the mouse is pressed or
798     * not pressed.  This method localizes the changes that must be made to the
799     * layers.
800     */
801    activate : function(pressed) {
802        this.pressed = pressed;
803        this.surface.style.cursor = (pressed ? PanoJS.GRABBING_MOUSE_CURSOR : PanoJS.GRAB_MOUSE_CURSOR);
804        this.surface.onmousemove = (pressed ? PanoJS.mouseMovedHandler : function() {});
805    },
806
807    /**
808     * Check whether the specified point exceeds the boundaries of
809     * the viewer's primary image.
810     */
811    pointExceedsBoundaries : function(coords) {
812        return (coords.x < this.x ||
813            coords.y < this.y ||
814            coords.x > (this.tileSize * Math.pow(2, this.zoomLevel) + this.x) ||
815            coords.y > (this.tileSize * Math.pow(2, this.zoomLevel) + this.y));
816    },
817
818    // QUESTION: where is the best place for this method to be invoked?
819    resetSlideMotion : function() {
820        // QUESTION: should this be > 0 ?
821        if (this.slideMonitor != 0) {
822            clearTimeout(this.slideMonitor);
823            this.slideMonitor = 0;
824        }
825
826        this.slideAcceleration = 0;
827    }
828};
829
830PanoJS.TileUrlProvider = function(baseUri, image) {
831    this.baseUri = baseUri;
832    this.image   = image;
833};
834
835PanoJS.TileUrlProvider.prototype = {
836    assembleUrl: function(xIndex, yIndex, zoom) {
837        return this.baseUri + '?tile=' +
838               zoom + '-' + xIndex + '-' + yIndex +
839               '&image=' + encodeURIComponent(this.image);
840    }
841};
842
843PanoJS.mousePressedHandler = function(e) {
844    e = e ? e : window.event;
845    // only grab on left-click
846    if (e.button < 2) {
847        var self = this.backingBean;
848        var coords = self.resolveCoordinates(e);
849        if (self.pointExceedsBoundaries(coords)) {
850            e.cancelBubble = true;
851        }
852        else {
853            self.press(coords);
854        }
855    }
856
857    // NOTE: MANDATORY! must return false so event does not propagate to well!
858    return false;
859};
860
861PanoJS.mouseReleasedHandler = function(e) {
862    e = e ? e : window.event;
863    var self = this.backingBean;
864    if (self.pressed) {
865        // OPTION: could decide to move viewer only on release, right here
866        self.release(self.resolveCoordinates(e));
867    }
868};
869
870PanoJS.mouseMovedHandler = function(e) {
871    e = e ? e : window.event;
872    var self = this.backingBean;
873    self.moveCount++;
874    if (self.moveCount % PanoJS.MOVE_THROTTLE == 0) {
875        self.moveViewer(self.resolveCoordinates(e));
876    }
877};
878
879PanoJS.zoomInHandler = function(e) {
880    e = e ? e : window.event;
881    var self = this.parentNode.parentNode.backingBean;
882    self.zoom(1);
883    return false;
884};
885
886PanoJS.zoomOutHandler = function(e) {
887    e = e ? e : window.event;
888    var self = this.parentNode.parentNode.backingBean;
889    self.zoom(-1);
890    return false;
891};
892
893PanoJS.doubleClickHandler = function(e) {
894    e = e ? e : window.event;
895    var self = this.backingBean;
896    coords = self.resolveCoordinates(e);
897    if (!self.pointExceedsBoundaries(coords)) {
898        self.resetSlideMotion();
899        self.recenter(coords);
900    }
901};
902
903PanoJS.keyboardMoveHandler = 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 == 38)
908                viewer.positionTiles({'x': 0,'y': -PanoJS.MOVE_THROTTLE}, true);
909        if (e.keyCode == 39)
910                viewer.positionTiles({'x': -PanoJS.MOVE_THROTTLE,'y': 0}, true);
911        if (e.keyCode == 40)
912                viewer.positionTiles({'x': 0,'y': PanoJS.MOVE_THROTTLE}, true);
913        if (e.keyCode == 37)
914                viewer.positionTiles({'x': PanoJS.MOVE_THROTTLE,'y': 0}, true);
915    }
916};
917
918PanoJS.keyboardZoomHandler = function(e) {
919    e = e ? e : window.event;
920    for (var i = 0; i < PanoJS.VIEWERS.length; i++) {
921        var viewer = PanoJS.VIEWERS[i];
922        if (e.keyCode == 109)
923                viewer.zoom(-1);
924        if (e.keyCode == 107)
925                viewer.zoom(1);
926    }
927};
928
929PanoJS.MoveEvent = function(x, y) {
930    this.x = x;
931    this.y = y;
932};
933
934PanoJS.ZoomEvent = function(x, y, level, percentage) {
935    this.x = x;
936    this.y = y;
937    this.percentage = percentage;
938    this.level = level;
939};
940
941addInitEvent(function(){
942    var panos = getElementsByClass('panoview_plugin',document,'div');
943    for(var i=0; i<panos.length; i++){
944        var pano = panos[i];
945        var opts = getElementsByClass('options',pano,'div')[0];
946        var conf;
947        eval('conf ='+opts.innerHTML); //JSON
948
949        var viewerBean = new PanoJS(pano, conf);
950        viewerBean.init();
951    }
952});
953