xref: /plugin/panoview/script.js (revision 27bbde004b4e000a9433308c2ec2bd5417ea0989)
1/**
2 * Panoramic JavaScript Image Viewer (PanoJS) 1.0.2
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    this.tileUrlProvider = new PanoJS.TileUrlProvider(options.tileBaseUri, options.image);
79
80    this.tileSize = (options.tileSize ? options.tileSize : PanoJS.TILE_SIZE);
81
82    // assign and do some validation on the zoom levels to ensure sanity
83    this.zoomLevel = (typeof options.initialZoom == 'undefined' ? -1 : parseInt(options.initialZoom));
84    this.maxZoomLevel = (typeof options.maxZoom == 'undefined' ? 0 : Math.abs(parseInt(options.maxZoom)));
85    if (this.zoomLevel > this.maxZoomLevel) {
86        this.zoomLevel = this.maxZoomLevel;
87    }
88
89    this.imgsize = { 'x' : 0, 'y' : 0 };
90    this.imgsize.x = (typeof options.imageWidth == 'undefined' ? -1 : parseInt(options.imageWidth));
91    this.imgsize.y = (typeof options.imageHeight == 'undefined' ? -1 : parseInt(options.imageHeight));
92
93    this.initialized = false;
94    this.surface = null;
95    this.well = null;
96    this.width = 0;
97    this.height = 0;
98    this.top = 0;
99    this.left = 0;
100    this.x = 0;
101    this.y = 0;
102    this.border = -1;
103    this.mark = { 'x' : 0, 'y' : 0 };
104    this.pressed = false;
105    this.tiles = [];
106    this.cache = {};
107    var blankTile = options.blankTile ? options.blankTile : PanoJS.BLANK_TILE_IMAGE;
108    var loadingTile = options.loadingTile ? options.loadingTile : PanoJS.LOADING_TILE_IMAGE;
109    this.cache['blank'] = new Image();
110    this.cache['blank'].src = blankTile;
111    if (blankTile != loadingTile) {
112        this.cache['loading'] = new Image();
113        this.cache['loading'].src = loadingTile;
114    }
115    else {
116        this.cache['loading'] = this.cache['blank'];
117    }
118
119    // employed to throttle the number of redraws that
120    // happen while the mouse is moving
121    this.moveCount = 0;
122    this.slideMonitor = 0;
123    this.slideAcceleration = 0;
124
125    // add to viewer registry
126    PanoJS.VIEWERS[PanoJS.VIEWERS.length] = this;
127}
128
129// project specific variables
130PanoJS.PROJECT_NAME = 'PanoJS';
131PanoJS.PROJECT_VERSION = '1.0.0';
132PanoJS.REVISION_FLAG = '';
133
134// CSS definition settings
135PanoJS.SURFACE_STYLE_CLASS = 'surface';
136PanoJS.WELL_STYLE_CLASS = 'well';
137PanoJS.CONTROLS_STYLE_CLASS = 'controls'
138PanoJS.TILE_STYLE_CLASS = 'tile';
139
140// language settings
141PanoJS.MSG_BEYOND_MIN_ZOOM = 'Cannot zoom out past the current level.';
142PanoJS.MSG_BEYOND_MAX_ZOOM = 'Cannot zoom in beyond the current level.';
143
144// defaults if not provided as constructor options
145PanoJS.TILE_BASE_URI = 'tiles';
146PanoJS.TILE_PREFIX = 'tile-';
147PanoJS.TILE_EXTENSION = 'jpg';
148PanoJS.TILE_SIZE = 256;
149PanoJS.BLANK_TILE_IMAGE = 'blank.gif';
150PanoJS.LOADING_TILE_IMAGE = 'blank.gif';
151PanoJS.INITIAL_PAN = { 'x' : .5, 'y' : .5 };
152PanoJS.USE_LOADER_IMAGE = true;
153PanoJS.USE_SLIDE = true;
154PanoJS.USE_KEYBOARD = true;
155
156// performance tuning variables
157PanoJS.MOVE_THROTTLE = 3;
158PanoJS.SLIDE_DELAY = 40;
159PanoJS.SLIDE_ACCELERATION_FACTOR = 5;
160
161// the following are calculated settings
162PanoJS.DOM_ONLOAD = (navigator.userAgent.indexOf('KHTML') >= 0 ? false : true);
163PanoJS.GRAB_MOUSE_CURSOR = (navigator.userAgent.search(/KHTML|Opera/i) >= 0 ? 'pointer' : (document.attachEvent ? 'url(grab.cur)' : '-moz-grab'));
164PanoJS.GRABBING_MOUSE_CURSOR = (navigator.userAgent.search(/KHTML|Opera/i) >= 0 ? 'move' : (document.attachEvent ? 'url(grabbing.cur)' : '-moz-grabbing'));
165
166// registry of all known viewers
167PanoJS.VIEWERS = [];
168
169// utility functions
170PanoJS.isInstance = function(object, clazz) {
171    // FIXME: can this just be replaced with instanceof operator? It has been reported that __proto__ is specific to Netscape
172    while (object != null) {
173        if (object == clazz.prototype) {
174            return true;
175        }
176
177        object = object.__proto__;
178    }
179
180    return false;
181}
182
183PanoJS.prototype = {
184
185    /**
186     * Resize the viewer to fit snug inside the browser window (or frame),
187     * spacing it from the edges by the specified border.
188     *
189     * This method should be called prior to init()
190     * FIXME: option to hide viewer to prevent scrollbar interference
191     */
192    fitToWindow : function(border) {
193        if (typeof border != 'number' || border < 0) {
194            border = 0;
195        }
196
197        this.border = border;
198        var calcWidth = 0;
199        var calcHeight = 0;
200        if (window.innerWidth) {
201            calcWidth = window.innerWidth;
202            calcHeight = window.innerHeight;
203        }
204        else {
205            calcWidth = (document.compatMode == 'CSS1Compat' ? document.documentElement.clientWidth : document.body.clientWidth);
206            calcHeight = (document.compatMode == 'CSS1Compat' ? document.documentElement.clientHeight : document.body.clientHeight);
207        }
208
209        calcWidth = Math.max(calcWidth - 2 * border, 0);
210        calcHeight = Math.max(calcHeight - 2 * border, 0);
211        if (calcWidth % 2) {
212            calcWidth--;
213        }
214
215        if (calcHeight % 2) {
216            calcHeight--;
217        }
218
219        this.width = calcWidth;
220        this.height = calcHeight;
221        this.viewer.style.width = this.width + 'px';
222        this.viewer.style.height = this.height + 'px';
223        this.viewer.style.top = border + 'px';
224        this.viewer.style.left = border + 'px';
225    },
226
227    init : function() {
228        if (document.attachEvent) {
229            document.body.ondragstart = function() { return false; }
230        }
231
232        if (this.width == 0 && this.height == 0) {
233            this.width = this.viewer.offsetWidth;
234            this.height = this.viewer.offsetHeight;
235        }
236
237        var fullSize = this.tileSize;
238        // explicit set of zoom level
239        if (this.zoomLevel >= 0 && this.zoomLevel <= this.maxZoomLevel) {
240            fullSize = this.tileSize * Math.pow(2, this.zoomLevel);
241        }
242        // calculate the zoom level based on what fits best in window
243        else {
244            this.zoomLevel = -1;
245            fullSize = this.tileSize / 2;
246            do {
247                this.zoomLevel += 1;
248                fullSize *= 2;
249            } while (fullSize < Math.max(this.width, this.height));
250            // take into account picture smaller than window size
251            if (this.zoomLevel > this.maxZoomLevel) {
252                var diff = this.zoomLevel - this.maxZoomLevel;
253                this.zoomLevel = this.maxZoomLevel;
254                fullSize /= Math.pow(2, diff);
255            }
256        }
257
258//fixme fullsize not used anymore?
259
260        // calculate the center
261        this.x = 0;
262        this.y = 0;
263        var inv = Math.max(this.imgsize.x,this.imgsize.y)  /
264                  (this.tileSize * Math.pow(2, this.zoomLevel));
265        if(this.imgsize.x){
266            var xx = (this.imgsize.x / inv)*0.5;
267            this.x = Math.floor(xx - this.width * 0.5) * -1;
268        }
269        if(this.imgsize.y){
270            var yy = (this.imgsize.y / inv)*0.5;
271            this.y = Math.floor(yy - this.height * 0.5) * -1;
272        }
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            }
286            else if (child.className == PanoJS.WELL_STYLE_CLASS) {
287                this.well = child;
288                child.backingBean = this;
289                // empty well
290                child.innerHTML = '';
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    prepareTiles : function() {
309        var rows = Math.ceil(this.height / this.tileSize) + 1;
310        var cols = Math.ceil(this.width / this.tileSize) + 1;
311
312        for (var c = 0; c < cols; c++) {
313            var tileCol = [];
314
315            for (var r = 0; r < rows; r++) {
316                /**
317                 * element is the DOM element associated with this tile
318                 * posx/posy are the pixel offsets of the tile
319                 * xIndex/yIndex are the index numbers of the tile segment
320                 * qx/qy represents the quadrant location of the tile
321                 */
322                var tile = {
323                    'element' : null,
324                    'posx' : 0,
325                    'posy' : 0,
326                    'xIndex' : c,
327                    'yIndex' : r,
328                    'qx' : c,
329                    'qy' : r
330                };
331
332                tileCol.push(tile);
333            }
334
335            this.tiles.push(tileCol);
336        }
337
338        this.surface.onmousedown = PanoJS.mousePressedHandler;
339        this.surface.onmouseup = this.surface.onmouseout = PanoJS.mouseReleasedHandler;
340        this.surface.ondblclick = PanoJS.doubleClickHandler;
341        if (PanoJS.USE_KEYBOARD) {
342            window.onkeypress = PanoJS.keyboardMoveHandler;
343            window.onkeydown = PanoJS.keyboardZoomHandler;
344        }
345
346        this.positionTiles();
347    },
348
349    /**
350     * Position the tiles based on the x, y coordinates of the
351     * viewer, taking into account the motion offsets, which
352     * are calculated by a motion event handler.
353     */
354    positionTiles : function(motion, reset) {
355        // default to no motion, just setup tiles
356        if (typeof motion == 'undefined') {
357            motion = { 'x' : 0, 'y' : 0 };
358        }
359
360        for (var c = 0; c < this.tiles.length; c++) {
361            for (var r = 0; r < this.tiles[c].length; r++) {
362                var tile = this.tiles[c][r];
363
364                tile.posx = (tile.xIndex * this.tileSize) + this.x + motion.x;
365                tile.posy = (tile.yIndex * this.tileSize) + this.y + motion.y;
366
367                var visible = true;
368
369                if (tile.posx > this.width) {
370                    // tile moved out of view to the right
371                    // consider the tile coming into view from the left
372                    do {
373                        tile.xIndex -= this.tiles.length;
374                        tile.posx = (tile.xIndex * this.tileSize) + this.x + motion.x;
375                    } while (tile.posx > this.width);
376
377                    if (tile.posx + this.tileSize < 0) {
378                        visible = false;
379                    }
380
381                } else {
382                    // tile may have moved out of view from the left
383                    // if so, consider the tile coming into view from the right
384                    while (tile.posx < -this.tileSize) {
385                        tile.xIndex += this.tiles.length;
386                        tile.posx = (tile.xIndex * this.tileSize) + this.x + motion.x;
387                    }
388
389                    if (tile.posx > this.width) {
390                        visible = false;
391                    }
392                }
393
394                if (tile.posy > this.height) {
395                    // tile moved out of view to the bottom
396                    // consider the tile coming into view from the top
397                    do {
398                        tile.yIndex -= this.tiles[c].length;
399                        tile.posy = (tile.yIndex * this.tileSize) + this.y + motion.y;
400                    } while (tile.posy > this.height);
401
402                    if (tile.posy + this.tileSize < 0) {
403                        visible = false;
404                    }
405
406                } else {
407                    // tile may have moved out of view to the top
408                    // if so, consider the tile coming into view from the bottom
409                    while (tile.posy < -this.tileSize) {
410                        tile.yIndex += this.tiles[c].length;
411                        tile.posy = (tile.yIndex * this.tileSize) + this.y + motion.y;
412                    }
413
414                    if (tile.posy > this.height) {
415                        visible = false;
416                    }
417                }
418
419                // initialize the image object for this quadrant
420                if (!this.initialized) {
421                    this.assignTileImage(tile, true);
422                    tile.element.style.top = tile.posy + 'px';
423                    tile.element.style.left = tile.posx + 'px';
424                }
425
426                // display the image if visible
427                if (visible) {
428                    this.assignTileImage(tile);
429                }
430
431                // seems to need this no matter what
432                tile.element.style.top = tile.posy + 'px';
433                tile.element.style.left = tile.posx + 'px';
434            }
435        }
436
437        // reset the x, y coordinates of the viewer according to motion
438        if (reset) {
439            this.x += motion.x;
440            this.y += motion.y;
441        }
442    },
443
444    /**
445     * Determine the source image of the specified tile based
446     * on the zoom level and position of the tile.  If forceBlankImage
447     * is specified, the source should be automatically set to the
448     * null tile image.  This method will also setup an onload
449     * routine, delaying the appearance of the tile until it is fully
450     * loaded, if configured to do so.
451     */
452    assignTileImage : function(tile, forceBlankImage) {
453        var tileImgId, src;
454        var useBlankImage = (forceBlankImage ? true : false);
455
456        // check if image has been scrolled too far in any particular direction
457        // and if so, use the null tile image
458        if (!useBlankImage) {
459            var left = tile.xIndex < 0;
460            var high = tile.yIndex < 0;
461            var right = tile.xIndex >= Math.pow(2, this.zoomLevel);
462            var low = tile.yIndex >= Math.pow(2, this.zoomLevel);
463            if (high || left || low || right) {
464                useBlankImage = true;
465            }
466        }
467
468        if (useBlankImage) {
469            tileImgId = 'blank:' + tile.qx + ':' + tile.qy;
470            src = this.cache['blank'].src;
471        }
472        else {
473            tileImgId = src = this.tileUrlProvider.assembleUrl(tile.xIndex, tile.yIndex, this.zoomLevel);
474        }
475
476        // only remove tile if identity is changing
477        if (tile.element != null &&
478            tile.element.parentNode != null &&
479            tile.element.relativeSrc != src) {
480            this.well.removeChild(tile.element);
481        }
482
483        var tileImg = this.cache[tileImgId];
484        // create cache if not exist
485        if (tileImg == null) {
486            tileImg = this.cache[tileImgId] = this.createPrototype(src);
487        }
488
489        if (useBlankImage || !PanoJS.USE_LOADER_IMAGE || tileImg.complete || (tileImg.image && tileImg.image.complete)) {
490            tileImg.onload = function() {};
491            if (tileImg.image) {
492                tileImg.image.onload = function() {};
493            }
494
495            if (tileImg.parentNode == null) {
496                tile.element = this.well.appendChild(tileImg);
497            }
498        }
499        else {
500            var loadingImgId = 'loading:' + tile.qx + ':' + tile.qy;
501            var loadingImg = this.cache[loadingImgId];
502            if (loadingImg == null) {
503                loadingImg = this.cache[loadingImgId] = this.createPrototype(this.cache['loading'].src);
504            }
505
506            loadingImg.targetSrc = tileImgId;
507
508            var well = this.well;
509            tile.element = well.appendChild(loadingImg);
510            tileImg.onload = function() {
511                // make sure our destination is still present
512                if (loadingImg.parentNode && loadingImg.targetSrc == tileImgId) {
513                    tileImg.style.top = loadingImg.style.top;
514                    tileImg.style.left = loadingImg.style.left;
515                    well.replaceChild(tileImg, loadingImg);
516                    tile.element = tileImg;
517                }
518
519                tileImg.onload = function() {};
520                return false;
521            }
522
523            // konqueror only recognizes the onload event on an Image
524            // javascript object, so we must handle that case here
525            if (!PanoJS.DOM_ONLOAD) {
526                tileImg.image = new Image();
527                tileImg.image.onload = tileImg.onload;
528                tileImg.image.src = tileImg.src;
529            }
530        }
531    },
532
533    createPrototype : function(src) {
534        var img = document.createElement('img');
535        img.src = src;
536        img.relativeSrc = src;
537        img.className = PanoJS.TILE_STYLE_CLASS;
538        img.style.width = this.tileSize + 'px';
539        img.style.height = this.tileSize + 'px';
540        return img;
541    },
542
543    addViewerMovedListener : function(listener) {
544        this.viewerMovedListeners.push(listener);
545    },
546
547    addViewerZoomedListener : function(listener) {
548        this.viewerZoomedListeners.push(listener);
549    },
550
551    /**
552     * Notify listeners of a zoom event on the viewer.
553     */
554    notifyViewerZoomed : function() {
555        var percentage = (100/(this.maxZoomLevel + 1)) * (this.zoomLevel + 1);
556        for (var i = 0; i < this.viewerZoomedListeners.length; i++) {
557            this.viewerZoomedListeners[i].viewerZoomed(
558                new PanoJS.ZoomEvent(this.x, this.y, this.zoomLevel, percentage)
559            );
560        }
561    },
562
563    /**
564     * Notify listeners of a move event on the viewer.
565     */
566    notifyViewerMoved : function(coords) {
567        if (typeof coords == 'undefined') {
568            coords = { 'x' : 0, 'y' : 0 };
569        }
570
571        for (var i = 0; i < this.viewerMovedListeners.length; i++) {
572            this.viewerMovedListeners[i].viewerMoved(
573                new PanoJS.MoveEvent(
574                    this.x + (coords.x - this.mark.x),
575                    this.y + (coords.y - this.mark.y)
576                )
577            );
578        }
579    },
580
581    zoom : function(direction) {
582        // ensure we are not zooming out of range
583        if (this.zoomLevel + direction < 0) {
584            if (PanoJS.MSG_BEYOND_MIN_ZOOM) {
585                alert(PanoJS.MSG_BEYOND_MIN_ZOOM);
586            }
587            return;
588        }
589        else if (this.zoomLevel + direction > this.maxZoomLevel) {
590            if (PanoJS.MSG_BEYOND_MAX_ZOOM) {
591                alert(PanoJS.MSG_BEYOND_MAX_ZOOM);
592            }
593            return;
594        }
595
596        this.blank();
597
598        var coords = { 'x' : Math.floor(this.width / 2), 'y' : Math.floor(this.height / 2) };
599
600        var before = {
601            'x' : (coords.x - this.x),
602            'y' : (coords.y - this.y)
603        };
604
605        var after = {
606            'x' : Math.floor(before.x * Math.pow(2, direction)),
607            'y' : Math.floor(before.y * Math.pow(2, direction))
608        };
609
610        this.x = coords.x - after.x;
611        this.y = coords.y - after.y;
612        this.zoomLevel += direction;
613        this.positionTiles();
614
615        this.notifyViewerZoomed();
616    },
617
618    /**
619     * Clear all the tiles from the well for a complete reinitialization of the
620     * viewer. At this point the viewer is not considered to be initialized.
621     */
622    clear : function() {
623        this.blank();
624        this.initialized = false;
625        this.tiles = [];
626    },
627
628    /**
629     * Remove all tiles from the well, which effectively "hides"
630     * them for a repaint.
631     */
632    blank : function() {
633        for (imgId in this.cache) {
634            var img = this.cache[imgId];
635            img.onload = function() {};
636            if (img.image) {
637                img.image.onload = function() {};
638            }
639
640            if (img.parentNode != null) {
641                this.well.removeChild(img);
642            }
643        }
644    },
645
646    /**
647     * Method specifically for handling a mouse move event.  A direct
648     * movement of the viewer can be achieved by calling positionTiles() directly.
649     */
650    moveViewer : function(coords) {
651        this.positionTiles({ 'x' : (coords.x - this.mark.x), 'y' : (coords.y - this.mark.y) });
652        this.notifyViewerMoved(coords);
653    },
654
655    /**
656     * Make the specified coords the new center of the image placement.
657     * This method is typically triggered as the result of a double-click
658     * event.  The calculation considers the distance between the center
659     * of the viewable area and the specified (viewer-relative) coordinates.
660     * If absolute is specified, treat the point as relative to the entire
661     * image, rather than only the viewable portion.
662     */
663    recenter : function(coords, absolute) {
664        if (absolute) {
665            coords.x += this.x;
666            coords.y += this.y;
667        }
668
669        var motion = {
670            'x' : Math.floor((this.width / 2) - coords.x),
671            'y' : Math.floor((this.height / 2) - coords.y)
672        };
673
674        if (motion.x == 0 && motion.y == 0) {
675            return;
676        }
677
678        if (PanoJS.USE_SLIDE) {
679            var target = motion;
680            var x, y;
681            // handle special case of vertical movement
682            if (target.x == 0) {
683                x = 0;
684                y = this.slideAcceleration;
685            }
686            else {
687                var slope = Math.abs(target.y / target.x);
688                x = Math.round(Math.pow(Math.pow(this.slideAcceleration, 2) / (1 + Math.pow(slope, 2)), .5));
689                y = Math.round(slope * x);
690            }
691
692            motion = {
693                'x' : Math.min(x, Math.abs(target.x)) * (target.x < 0 ? -1 : 1),
694                'y' : Math.min(y, Math.abs(target.y)) * (target.y < 0 ? -1 : 1)
695            }
696        }
697
698        this.positionTiles(motion, true);
699        this.notifyViewerMoved();
700
701        if (!PanoJS.USE_SLIDE) {
702            return;
703        }
704
705        var newcoords = {
706            'x' : coords.x + motion.x,
707            'y' : coords.y + motion.y
708        };
709
710        var self = this;
711        // TODO: use an exponential growth rather than linear (should also depend on how far we are going)
712        // FIXME: this could be optimized by calling positionTiles directly perhaps
713        this.slideAcceleration += PanoJS.SLIDE_ACCELERATION_FACTOR;
714        this.slideMonitor = setTimeout(function() { self.recenter(newcoords); }, PanoJS.SLIDE_DELAY );
715    },
716
717    resize : function() {
718        // IE fires a premature resize event
719        if (!this.initialized) {
720            return;
721        }
722
723        var newWidth = this.viewer.offsetWidth;
724        var newHeight = this.viewer.offsetHeight;
725
726        this.viewer.style.display = 'none';
727        this.clear();
728
729        var before = {
730            'x' : Math.floor(this.width / 2),
731            'y' : Math.floor(this.height / 2)
732        };
733
734        if (this.border >= 0) {
735            this.fitToWindow(this.border);
736        }
737        else {
738            this.width = newWidth;
739            this.height = newHeight;
740        }
741
742        this.prepareTiles();
743
744        var after = {
745            'x' : Math.floor(this.width / 2),
746            'y' : Math.floor(this.height / 2)
747        };
748
749        if (this.border >= 0) {
750            this.x += (after.x - before.x);
751            this.y += (after.y - before.y);
752        }
753        this.positionTiles();
754        this.viewer.style.display = '';
755        this.initialized = true;
756        this.notifyViewerMoved();
757    },
758
759    /**
760     * Resolve the coordinates from this mouse event by subtracting the
761     * offset of the viewer in the browser window (or frame).  This does
762     * take into account the scroll offset of the page.
763     */
764    resolveCoordinates : function(e) {
765        return {
766            'x' : (e.pageX || (e.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft))) - this.left,
767            'y' : (e.pageY || (e.clientY + (document.documentElement.scrollTop || document.body.scrollTop))) - this.top
768        }
769    },
770
771    press : function(coords) {
772        this.activate(true);
773        this.mark = coords;
774    },
775
776    release : function(coords) {
777        this.activate(false);
778        var motion = {
779            'x' : (coords.x - this.mark.x),
780            'y' : (coords.y - this.mark.y)
781        };
782
783        this.x += motion.x;
784        this.y += motion.y;
785        this.mark = { 'x' : 0, 'y' : 0 };
786    },
787
788    /**
789     * Activate the viewer into motion depending on whether the mouse is pressed or
790     * not pressed.  This method localizes the changes that must be made to the
791     * layers.
792     */
793    activate : function(pressed) {
794        this.pressed = pressed;
795        this.surface.style.cursor = (pressed ? PanoJS.GRABBING_MOUSE_CURSOR : PanoJS.GRAB_MOUSE_CURSOR);
796        this.surface.onmousemove = (pressed ? PanoJS.mouseMovedHandler : function() {});
797    },
798
799    /**
800     * Check whether the specified point exceeds the boundaries of
801     * the viewer's primary image.
802     */
803    pointExceedsBoundaries : function(coords) {
804        return (coords.x < this.x ||
805            coords.y < this.y ||
806            coords.x > (this.tileSize * Math.pow(2, this.zoomLevel) + this.x) ||
807            coords.y > (this.tileSize * Math.pow(2, this.zoomLevel) + this.y));
808    },
809
810    // QUESTION: where is the best place for this method to be invoked?
811    resetSlideMotion : function() {
812        // QUESTION: should this be > 0 ?
813        if (this.slideMonitor != 0) {
814            clearTimeout(this.slideMonitor);
815            this.slideMonitor = 0;
816        }
817
818        this.slideAcceleration = 0;
819    }
820};
821
822PanoJS.TileUrlProvider = function(baseUri, image) {
823    this.baseUri = baseUri;
824    this.image   = image;
825}
826
827PanoJS.TileUrlProvider.prototype = {
828    assembleUrl: function(xIndex, yIndex, zoom) {
829        return this.baseUri + '?tile=' +
830               zoom + '-' + xIndex + '-' + yIndex +
831               '&image=' + encodeURIComponent(this.image);
832    }
833}
834
835PanoJS.mousePressedHandler = function(e) {
836    e = e ? e : window.event;
837    // only grab on left-click
838    if (e.button < 2) {
839        var self = this.backingBean;
840        var coords = self.resolveCoordinates(e);
841        if (self.pointExceedsBoundaries(coords)) {
842            e.cancelBubble = true;
843        }
844        else {
845            self.press(coords);
846        }
847    }
848
849    // NOTE: MANDATORY! must return false so event does not propagate to well!
850    return false;
851};
852
853PanoJS.mouseReleasedHandler = function(e) {
854    e = e ? e : window.event;
855    var self = this.backingBean;
856    if (self.pressed) {
857        // OPTION: could decide to move viewer only on release, right here
858        self.release(self.resolveCoordinates(e));
859    }
860};
861
862PanoJS.mouseMovedHandler = function(e) {
863    e = e ? e : window.event;
864    var self = this.backingBean;
865    self.moveCount++;
866    if (self.moveCount % PanoJS.MOVE_THROTTLE == 0) {
867        self.moveViewer(self.resolveCoordinates(e));
868    }
869};
870
871PanoJS.zoomInHandler = 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.zoomOutHandler = function(e) {
879    e = e ? e : window.event;
880    var self = this.parentNode.parentNode.backingBean;
881    self.zoom(-1);
882    return false;
883};
884
885PanoJS.doubleClickHandler = function(e) {
886    e = e ? e : window.event;
887    var self = this.backingBean;
888    coords = self.resolveCoordinates(e);
889    if (!self.pointExceedsBoundaries(coords)) {
890        self.resetSlideMotion();
891        self.recenter(coords);
892    }
893};
894
895PanoJS.keyboardMoveHandler = function(e) {
896    e = e ? e : window.event;
897    for (var i = 0; i < PanoJS.VIEWERS.length; i++) {
898        var viewer = PanoJS.VIEWERS[i];
899        if (e.keyCode == 38)
900                viewer.positionTiles({'x': 0,'y': -PanoJS.MOVE_THROTTLE}, true);
901        if (e.keyCode == 39)
902                viewer.positionTiles({'x': -PanoJS.MOVE_THROTTLE,'y': 0}, true);
903        if (e.keyCode == 40)
904                viewer.positionTiles({'x': 0,'y': PanoJS.MOVE_THROTTLE}, true);
905        if (e.keyCode == 37)
906                viewer.positionTiles({'x': PanoJS.MOVE_THROTTLE,'y': 0}, true);
907    }
908}
909
910PanoJS.keyboardZoomHandler = function(e) {
911    e = e ? e : window.event;
912    for (var i = 0; i < PanoJS.VIEWERS.length; i++) {
913        var viewer = PanoJS.VIEWERS[i];
914        if (e.keyCode == 109)
915                viewer.zoom(-1);
916        if (e.keyCode == 107)
917                viewer.zoom(1);
918    }
919}
920
921PanoJS.MoveEvent = function(x, y) {
922    this.x = x;
923    this.y = y;
924};
925
926PanoJS.ZoomEvent = function(x, y, level, percentage) {
927    this.x = x;
928    this.y = y;
929    this.percentage = percentage;
930    this.level = level;
931};
932
933addInitEvent(function(){
934    var panos = getElementsByClass('panoview_plugin',document,'div');
935    for(var i=0; i<panos.length; i++){
936        var pano = panos[i];
937        var opts = getElementsByClass('options',pano,'div')[0];
938        var conf;
939        eval('conf ='+opts.innerHTML); //JSON
940
941        var viewerBean = new PanoJS(pano, conf);
942        viewerBean.init();
943    }
944});
945