﻿/**
* Panoramic JavaScript Image Viewer (PanoJS) 1.0.2
*
* Generates a draggable and zoomable viewer for images that would
* be otherwise too large for a browser window.  Examples would include
* maps or high resolution document scans.
*
* Images must be precut into tiles, such as by the accompanying tilemaker.py
* python library.
*
* <div class="viewer">
*   <div class="well"><!-- --></div>
*   <div class="surface"><!-- --></div>
*   <div class="controls">
*     <a href="#" class="zoomIn">+</a>
*     <a href="#" class="zoomOut">-</a>
*   </div>
* </div>
* 
* The "well" node is where generated IMG elements are appended. It
* should have the CSS rule "overflow: hidden", to occlude image tiles
* that have scrolled out of view.
* 
* The "surface" node is the transparent mouse-responsive layer of the
* image viewer, and should match the well in size.
*
* var viewerBean = new PanoJS(element, 'tiles', 256, 3, 1);
*
* To disable the image toolbar in IE, be sure to add the following:
* <meta http-equiv="imagetoolbar" content="no" />
*
* Copyright (c) 2005 Michal Migurski <mike-gsv@teczno.com>
*                    Dan Allen <dan.allen@mojavelinux.com>
* 
* Redistribution and use in source form, with or without modification,
* are permitted provided that the following conditions are met:
* 1. Redistributions of source code must retain the above copyright
*    notice, this list of conditions and the following disclaimer.
* 2. The name of the author may not be used to endorse or promote products
*    derived from this software without specific prior written permission.
* 
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @author Michal Migurski <mike-gsv@teczno.com>
* @author Dan Allen <dan.allen@mojavelinux.com>
*
* NOTE: if artifacts are appearing, then positions include half-pixels
* TODO: additional jsdoc and package jsmin
* TODO: Tile could be an object
*/

var offsetx = 12;
var offsety = 8;
var isCallingService = false;
var expanded = false;

function PanoJS(viewer, options) {

    // listeners that are notified on a move (pan) event
    this.viewerMovedListeners = [];
    // listeners that are notified on a zoom event
    this.viewerZoomedListeners = [];

    if (typeof viewer == 'string') {
        this.viewer = document.getElementById(viewer);
    }
    else {
        this.viewer = viewer;
    }

    if (typeof options == 'undefined') {
        options = {};
    }

    if (typeof options.tileUrlProvider != 'undefined' &&
		PanoJS.isInstance(options.tileUrlProvider, PanoJS.TileUrlProvider)) {
        this.tileUrlProvider = options.tileUrlProvider;
    }
    else {
        this.tileUrlProvider = new PanoJS.TileUrlProvider(
			options.tileBaseUri ? options.tileBaseUri : PanoJS.TILE_BASE_URI,
			options.tilePrefix ? options.tilePrefix : PanoJS.TILE_PREFIX,
			options.tileExtension ? options.tileExtension : PanoJS.TILE_EXTENSION
		);
    }

    //this.tileSize = (options.tileSize ? options.tileSize : PanoJS.TILE_SIZE);

    this.tileWidth = (options.tileWidth ? options.tileWidth : PanoJS.TILE_WIDTH);
    this.tileHeight = (options.tileHeight ? options.tileHeight : PanoJS.TILE_HEIGHT);

    // assign and do some validation on the zoom levels to ensure sanity
    this.zoomLevel = (typeof options.initialZoom == 'undefined' ? -1 : parseInt(options.initialZoom));
    this.maxZoomLevel = (typeof options.maxZoom == 'undefined' ? 0 : Math.abs(parseInt(options.maxZoom)));
    if (this.zoomLevel > this.maxZoomLevel) {
        this.zoomLevel = this.maxZoomLevel;
    }

    this.initialPan = (options.initialPan ? options.initialPan : PanoJS.INITIAL_PAN);

    this.initialized = false;
    this.surface = null;
    this.well = null;
    this.width = 0;
    this.height = 0;
    this.top = 0;
    this.left = 0;
    this.x = 0;
    this.y = 0;
    this.border = -1;
    this.mark = { 'x': 0, 'y': 0 };
    this.pressed = false;
    this.tiles = [];
    this.cache = {};
    
    this.expanded = false;
    this.expandedImage = options.expandedImage;
    this.collapsedImage = options.collapsedImage;
    this.imgExpand = options.imgExpand;

    var blankTile = options.blankTile ? options.blankTile : PanoJS.BLANK_TILE_IMAGE;
    var loadingTile = options.loadingTile ? options.loadingTile : PanoJS.LOADING_TILE_IMAGE;
    this.cache['blank'] = new Image();
    this.cache['blank'].src = blankTile;
    if (blankTile != loadingTile) {
        this.cache['loading'] = new Image();
        this.cache['loading'].src = loadingTile;
    }
    else {
        this.cache['loading'] = this.cache['blank'];
    }

    // employed to throttle the number of redraws that
    // happen while the mouse is moving
    this.moveCount = 0;
    this.slideMonitor = 0;
    this.slideAcceleration = 0;

    // add to viewer registry
    PanoJS.VIEWERS[PanoJS.VIEWERS.length] = this;
}

// project specific variables
PanoJS.PROJECT_NAME = 'PanoJS';
PanoJS.PROJECT_VERSION = '1.0.0';
PanoJS.REVISION_FLAG = '';

// CSS definition settings
PanoJS.SURFACE_STYLE_CLASS = 'surface';
PanoJS.WELL_STYLE_CLASS = 'well';
PanoJS.CONTROLS_STYLE_CLASS = 'controls'
PanoJS.TILE_STYLE_CLASS = 'tile';

// language settings
PanoJS.MSG_BEYOND_MIN_ZOOM = 'Cannot zoom out past the current level.';
PanoJS.MSG_BEYOND_MAX_ZOOM = 'Cannot zoom in beyond the current level.';

// defaults if not provided as constructor options
PanoJS.TILE_BASE_URI = 'tiles';
PanoJS.TILE_PREFIX = 'tile-';
PanoJS.TILE_EXTENSION = 'jpg';
PanoJS.TILE_WIDTH = 185;
PanoJS.TILE_HEIGHT = 139;
PanoJS.BLANK_TILE_IMAGE = '/controls/suffolk/images/blank.gif';
PanoJS.LOADING_TILE_IMAGE = 'blank.gif';
PanoJS.INITIAL_PAN = { 'x': .5, 'y': .5 };
PanoJS.USE_LOADER_IMAGE = true;
PanoJS.USE_SLIDE = true;
PanoJS.USE_KEYBOARD = true;
PanoJS.Multiplier = 4;

// performance tuning variables
PanoJS.MOVE_THROTTLE = 3;
PanoJS.SLIDE_DELAY = 40;
PanoJS.SLIDE_ACCELERATION_FACTOR = 5;

// the following are calculated settings
PanoJS.DOM_ONLOAD = (navigator.userAgent.indexOf('KHTML') >= 0 ? false : true);
PanoJS.GRAB_MOUSE_CURSOR = (navigator.userAgent.search(/KHTML|Opera/i) >= 0 ? 'pointer' : (document.attachEvent ? 'url(/controls/suffolk/images/grab.cur)' : '-moz-grab'));
PanoJS.GRABBING_MOUSE_CURSOR = (navigator.userAgent.search(/KHTML|Opera/i) >= 0 ? 'move' : (document.attachEvent ? 'url(/controls/suffolk/images/grabbing.cur)' : '-moz-grabbing'));

PanoJS.MinW = 750;
PanoJS.MinH = 562;
PanoJS.fullSizeW = 6000;
PanoJS.fullSizeH = 4498;

PanoJS.expanded = false;
PanoJS.imgExpand = '';
PanoJS.expandedImage = '';
PanoJS.collapsedImage = '';

PanoJS.CURRENT_TYPOLOGY = '';
PanoJS.CURRENT_TYPOLOGY_NUMBER = '';
PanoJS.IS_HOTSPOT = false;

// registry of all known viewers
PanoJS.VIEWERS = [];

// utility functions
PanoJS.isInstance = function (object, clazz) {
    // FIXME: can this just be replaced with instanceof operator? It has been reported that __proto__ is specific to Netscape
    while (object != null) {
        if (object == clazz.prototype) {
            return true;
        }

        object = object.__proto__;
    }

    return false;
}

PanoJS.prototype = {
    calcFullSize: function () {
        this.fullSizeW = PanoJS.MinW * Math.pow(2, (this.zoomLevel - 1));
        this.fullSizeH = PanoJS.MinH * Math.pow(2, (this.zoomLevel - 1));
    },
    /**
    * Resize the viewer to fit snug inside the browser window (or frame),
    * spacing it from the edges by the specified border.
    *
    * This method should be called prior to init()
    * FIXME: option to hide viewer to prevent scrollbar interference
    */
    fitToWindow: function (border) {
        if (typeof border != 'number' || border < 0) {
            border = 0;
        }

        this.border = border;
        var calcWidth = 0;
        var calcHeight = 0;
        if (window.innerWidth) {
            calcWidth = window.innerWidth;
            calcHeight = window.innerHeight;
        }
        else {
            calcWidth = (document.compatMode == 'CSS1Compat' ? document.documentElement.clientWidth : document.body.clientWidth);
            calcHeight = (document.compatMode == 'CSS1Compat' ? document.documentElement.clientHeight : document.body.clientHeight);
        }

        calcWidth = Math.max(calcWidth - 2 * border, 0);
        calcHeight = Math.max(calcHeight - 2 * border, 0);
        if (calcWidth % 2) {
            calcWidth--;
        }

        if (calcHeight % 2) {
            calcHeight--;
        }

        this.width = calcWidth;
        this.height = calcHeight;
        this.viewer.style.width = this.width + 'px';
        this.viewer.style.height = this.height + 'px';
        this.viewer.style.top = border + 'px';
        this.viewer.style.left = border + 'px';
    },
    centerImage: function () {
        this.x = Math.floor((this.fullSizeW - this.width) * -this.initialPan.x);
        this.y = Math.floor((this.fullSizeH - this.height) * -this.initialPan.y);
    },
    init: function () {
        if (document.attachEvent) {
            document.body.ondragstart = function () { return false; }
        }

        if (this.width == 0 && this.height == 0) {
            this.width = this.viewer.offsetWidth;
            this.height = this.viewer.offsetHeight;
        }

        // move top level up and to the left so that the image is centered
        this.calcFullSize();
        this.centerImage();

        // offset of viewer in the window
        for (var node = this.viewer; node; node = node.offsetParent) {
            this.top += node.offsetTop;
            this.left += node.offsetLeft;
        }

        for (var child = this.viewer.firstChild; child; child = child.nextSibling) {
            if (child.className == PanoJS.SURFACE_STYLE_CLASS) {
                this.surface = child;
                child.backingBean = this;
            }
            else if (child.className == PanoJS.WELL_STYLE_CLASS) {
                this.well = child;
                child.backingBean = this;
            }
            else if (child.className == PanoJS.CONTROLS_STYLE_CLASS) {
                for (var control = child.firstChild; control; control = control.nextSibling) {
                    if (control.className) {
                        control.onclick = PanoJS[control.className + 'Handler'];
                    }
                }
            }
        }

        this.viewer.backingBean = this;
        this.surface.style.cursor = PanoJS.GRAB_MOUSE_CURSOR;

        this.surface.onmousemove = PanoJS.mouseMovedHandler;

        this.prepareTiles();
        this.initialized = true;
    },
    canMove: function (motion) {
        if (this.zoomLevel == 1)
            return null;
        else
            return motion;
    },
    prepareTiles: function () {
        var rows = Math.ceil(this.height / this.tileHeight) + 1;
        var cols = Math.ceil(this.width / this.tileWidth) + 1;

        for (var c = 0; c < cols; c++) {
            var tileCol = [];

            for (var r = 0; r < rows; r++) {
                /**
                * element is the DOM element associated with this tile
                * posx/posy are the pixel offsets of the tile
                * xIndex/yIndex are the index numbers of the tile segment
                * qx/qy represents the quadrant location of the tile
                */
                var tile = {
                    'element': null,
                    'posx': 0,
                    'posy': 0,
                    'xIndex': c,
                    'yIndex': r,
                    'qx': c,
                    'qy': r
                };

                tileCol.push(tile);
            }

            this.tiles.push(tileCol);
        }

        this.surface.onmousedown = PanoJS.mousePressedHandler;
        this.surface.onmouseup = this.surface.onmouseout = PanoJS.mouseReleasedHandler;
        this.surface.ondblclick = PanoJS.doubleClickHandler;

        document.oncontextmenu = PanoJS.mouseRightPressedHandler;

        if (PanoJS.USE_KEYBOARD) {
            window.onkeypress = PanoJS.keyboardMoveHandler;
            window.onkeydown = PanoJS.keyboardZoomHandler;
        }

        this.positionTiles();
    },

    /**
    * Position the tiles based on the x, y coordinates of the
    * viewer, taking into account the motion offsets, which
    * are calculated by a motion event handler.
    */
    positionTiles: function (motion, reset) {
        // default to no motion, just setup tiles
        if (typeof motion == 'undefined') {
            motion = { 'x': 0, 'y': 0 };
        } else {
            motion = this.canMove(motion);
        }

        if (motion == null) { return; }

        for (var c = 0; c < this.tiles.length; c++) {
            for (var r = 0; r < this.tiles[c].length; r++) {
                var tile = this.tiles[c][r];

                tile.posx = (tile.xIndex * this.tileWidth) + this.x + motion.x;
                tile.posy = (tile.yIndex * this.tileHeight) + this.y + motion.y;

                var visible = true;

                if (tile.posx > this.width) {
                    // tile moved out of view to the right
                    // consider the tile coming into view from the left
                    do {
                        tile.xIndex -= this.tiles.length;
                        tile.posx = (tile.xIndex * this.tileWidth) + this.x + motion.x;
                    } while (tile.posx > this.width);

                    if (tile.posx + this.tileWidth < 0) {
                        visible = false;
                    }

                } else {
                    // tile may have moved out of view from the left
                    // if so, consider the tile coming into view from the right
                    while (tile.posx < -this.tileWidth) {
                        tile.xIndex += this.tiles.length;
                        tile.posx = (tile.xIndex * this.tileWidth) + this.x + motion.x;
                    }

                    if (tile.posx > this.width) {
                        visible = false;
                    }
                }

                if (tile.posy > this.height) {
                    // tile moved out of view to the bottom
                    // consider the tile coming into view from the top
                    do {
                        tile.yIndex -= this.tiles[c].length;
                        tile.posy = (tile.yIndex * this.tileHeight) + this.y + motion.y;
                    } while (tile.posy > this.height);

                    if (tile.posy + this.tileHeight < 0) {
                        visible = false;
                    }

                } else {
                    // tile may have moved out of view to the top
                    // if so, consider the tile coming into view from the bottom
                    while (tile.posy < -this.tileHeight) {
                        tile.yIndex += this.tiles[c].length;
                        tile.posy = (tile.yIndex * this.tileHeight) + this.y + motion.y;
                    }

                    if (tile.posy > this.height) {
                        visible = false;
                    }
                }

                // initialize the image object for this quadrant
                if (!this.initialized) {
                    this.assignTileImage(tile, true);
                    tile.element.style.top = tile.posy + 'px';
                    tile.element.style.left = tile.posx + 'px';
                }

                // display the image if visible
                if (visible) {
                    this.assignTileImage(tile);
                }

                // seems to need this no matter what
                tile.element.style.top = tile.posy + 'px';
                tile.element.style.left = tile.posx + 'px';
            }
        }

        // reset the x, y coordinates of the viewer according to motion
        if (reset) {
            this.x += motion.x;
            this.y += motion.y;

            if (this.x > 0) { this.x = 0; }
            if (this.y > 0) { this.y = 0; }

            this.positionTiles(null, null);
        }
    },

    /**
    * Determine the source image of the specified tile based
    * on the zoom level and position of the tile.  If forceBlankImage
    * is specified, the source should be automatically set to the
    * null tile image.  This method will also setup an onload
    * routine, delaying the appearance of the tile until it is fully
    * loaded, if configured to do so.
    */
    assignTileImage: function (tile, forceBlankImage) {
        var tileImgId, src;
        var useBlankImage = (forceBlankImage ? true : false);

        // check if image has been scrolled too far in any particular direction
        // and if so, use the null tile image
        if (!useBlankImage) {
            var left = tile.xIndex < 0;
            var high = tile.yIndex < 0;
            var right = tile.xIndex >= Math.pow(PanoJS.Multiplier, this.zoomLevel);
            var low = tile.yIndex >= Math.pow(PanoJS.Multiplier, this.zoomLevel);
            var wider = (tile.xIndex * PanoJS.TILE_WIDTH) > this.fullSizeW;
            var higher = (tile.yIndex * PanoJS.TILE_HEIGHT) > this.fullSizeH;

            if (high || left || low || right || higher || wider) {
                useBlankImage = true;
            }
        }

        if (useBlankImage) {
            tileImgId = 'blank:' + tile.qx + ':' + tile.qy;
            src = this.cache['blank'].src;
        }
        else {
            tileImgId = src = this.tileUrlProvider.assembleUrl(tile.xIndex, tile.yIndex, this.zoomLevel);
        }

        // only remove tile if identity is changing
        if (tile.element != null &&
			tile.element.parentNode != null &&
			tile.element.relativeSrc != src) {
            this.well.removeChild(tile.element);
        }

        var tileImg = this.cache[tileImgId];
        // create cache if not exist
        if (tileImg == null) {
            tileImg = this.cache[tileImgId] = this.createPrototype(src);
        }

        if (useBlankImage || !PanoJS.USE_LOADER_IMAGE || tileImg.complete || (tileImg.image && tileImg.image.complete)) {
            tileImg.onload = function () { };
            if (tileImg.image) {
                tileImg.image.onload = function () { };
            }

            if (tileImg.parentNode == null) {
                tile.element = this.well.appendChild(tileImg);
            }
        }
        else {
            var loadingImgId = 'loading:' + tile.qx + ':' + tile.qy;
            var loadingImg = this.cache[loadingImgId];
            if (loadingImg == null) {
                loadingImg = this.cache[loadingImgId] = this.createPrototype(this.cache['loading'].src);
            }

            loadingImg.targetSrc = tileImgId;

            var well = this.well;
            tile.element = well.appendChild(loadingImg);
            tileImg.onload = function () {
                // make sure our destination is still present
                if (loadingImg.parentNode && loadingImg.targetSrc == tileImgId) {
                    tileImg.style.top = loadingImg.style.top;
                    tileImg.style.left = loadingImg.style.left;
                    well.replaceChild(tileImg, loadingImg);
                    tile.element = tileImg;
                }

                tileImg.onload = function () { };
                return false;
            }

            // konqueror only recognizes the onload event on an Image
            // javascript object, so we must handle that case here
            if (!PanoJS.DOM_ONLOAD) {
                tileImg.image = new Image();
                tileImg.image.onload = tileImg.onload;
                tileImg.image.src = tileImg.src;
            }
        }
    },

    createPrototype: function (src) {
        var img = document.createElement('img');
        img.src = src;
        img.relativeSrc = src;
        img.className = PanoJS.TILE_STYLE_CLASS;
        img.style.width = this.tileWidth + 'px';
        img.style.height = this.tileHeight + 'px';
        return img;
    },

    addViewerMovedListener: function (listener) {
        this.viewerMovedListeners.push(listener);
    },

    addViewerZoomedListener: function (listener) {
        this.viewerZoomedListeners.push(listener);
    },

    /**
    * Notify listeners of a zoom event on the viewer.
    */
    notifyViewerZoomed: function () {
        var percentage = (100 / (this.maxZoomLevel + 1)) * (this.zoomLevel + 1);
        for (var i = 0; i < this.viewerZoomedListeners.length; i++) {
            this.viewerZoomedListeners[i].viewerZoomed(
				new PanoJS.ZoomEvent(this.x, this.y, this.zoomLevel, percentage)
			);
        }
    },

    /**
    * Notify listeners of a move event on the viewer.
    */
    notifyViewerMoved: function (coords) {
        if (typeof coords == 'undefined') {
            coords = { 'x': 0, 'y': 0 };
        }

        for (var i = 0; i < this.viewerMovedListeners.length; i++) {
            this.viewerMovedListeners[i].viewerMoved(
				new PanoJS.MoveEvent(
					this.x + (coords.x - this.mark.x),
					this.y + (coords.y - this.mark.y)
				)
			);
        }
    },

    zoom: function (direction) {
        // ensure we are not zooming out of range
        if (this.zoomLevel + direction < 1) {
            if (PanoJS.MSG_BEYOND_MIN_ZOOM) {
                alert(PanoJS.MSG_BEYOND_MIN_ZOOM);
            }
            return;
        }
        else if (this.zoomLevel + direction > this.maxZoomLevel) {
            if (PanoJS.MSG_BEYOND_MAX_ZOOM) {
                alert(PanoJS.MSG_BEYOND_MAX_ZOOM);
            }
            return;
        }

        this.blank();

        var coords = { 'x': Math.floor(this.width / 2), 'y': Math.floor(this.height / 2) };

        var before = {
            'x': (coords.x - this.x),
            'y': (coords.y - this.y)
        };

        var after = {
            'x': Math.floor(before.x * Math.pow(2, direction)),
            'y': Math.floor(before.y * Math.pow(2, direction))
        };

        this.x = coords.x - after.x;
        this.y = coords.y - after.y;
        this.zoomLevel += direction;

        this.calcFullSize();

        if ((direction == -1 && this.zoomLevel == 1)) {
            this.centerImage();
        }

        var multiplier = (Math.pow(2, this.zoomLevel) / 2);
        var right = ((this.x + (this.width * -1)) * -1); var bottom = ((this.y + (this.height * -1)) * -1);
        var maxRight = PanoJS.MinW * multiplier; var maxBottom = PanoJS.MinH * multiplier;

        if (this.x >= 0) { this.x = 0; } if (this.y >= 0) { this.y = 0; }
        if (right > maxRight) { this.x = (maxRight - this.width) * -1; } if (bottom > maxBottom) { this.y = (maxBottom - this.height) * -1; }

        this.positionTiles();
        this.notifyViewerZoomed();

        if (this.zoomLevel == 4) {
            $get('imgScale').style.display = 'block';
        } else {
            $get('imgScale').style.display = 'none';
        }
    },

    refreshAfterExpand: function (oldWidth, oldHeight) {
        //this.init();
        this.viewer.style.width = this.width + 'px';
        this.viewer.style.height = this.height + 'px';

        this.expanded = !this.expanded;
        this.viewer.style.display = 'none';
        this.clear();

        var before = {
            'x': Math.floor(oldWidth / 2),
            'y': Math.floor(oldHeight / 2)
        };

        if (this.border >= 0) {
            this.fitToWindow(this.border);
        }

        this.prepareTiles();

        var after = {
            'x': Math.floor(this.width / 2),
            'y': Math.floor(this.height / 2)
        };

        this.x += (after.x - before.x);
        this.y += (after.y - before.y);
        this.positionTiles();
        this.viewer.style.display = '';
        this.initialized = true;
        this.notifyViewerMoved();
    },
    expandMap: function () {
        var oldWidth = this.width;
        var oldHeight = this.height;

        if (this.expanded) {
            $('.well').hide();

            $('.mapcontainer').width(740);
            this.left = 462;
            this.width = 740;
            $get(this.imgExpand).src = this.expandedImage;
            this.refreshAfterExpand(oldWidth, oldHeight);

            $('.rightcolumnmap').animate({ width: 743 }, $.proxy(function () {
                $('.well').show();
                $('.leftcolumn').fadeIn('slow');
            }, this));

        } else {
            $('.leftcolumn').fadeOut('slow', $.proxy(function () {
                $('.well').hide();

                $('.rightcolumnmap').animate({ width: 987 }, $.proxy(function () {
                    $('.mapcontainer').width(984);
                    this.left = 218;
                    this.width = 984;
                    $get(this.imgExpand).src = this.collapsedImage;
                    this.refreshAfterExpand(oldWidth, oldHeight);
                    $('.well').show();
                }, this));
            }, this));
        }
    },

    /** 
    * Clear all the tiles from the well for a complete reinitialization of the
    * viewer. At this point the viewer is not considered to be initialized.
    */
    clear: function () {
        this.blank();
        this.initialized = false;
        this.tiles = [];
    },

    /**
    * Remove all tiles from the well, which effectively "hides"
    * them for a repaint.
    */
    blank: function () {
        for (imgId in this.cache) {
            var img = this.cache[imgId];
            img.onload = function () { };
            if (img.image) {
                img.image.onload = function () { };
            }

            if (img.parentNode != null) {
                this.well.removeChild(img);
            }
        }
    },

    /**
    * Method specifically for handling a mouse move event.  A direct
    * movement of the viewer can be achieved by calling positionTiles() directly.
    */
    moveViewer: function (coords) {
        //this.positionTiles({ 'x': (coords.x - this.mark.x), 'y': (coords.y - this.mark.y) });
        //this.notifyViewerMoved(coords);

        var x = coords.x - this.mark.x; var y = coords.y - this.mark.y;
        var multiplier = (Math.pow(2, this.zoomLevel) / 2);
        var right = ((this.x + (this.width * -1) + x) * -1); var bottom = ((this.y + (this.height * -1) + y) * -1);
        var maxRight = (PanoJS.MinW * multiplier); var maxBottom = (PanoJS.MinH * multiplier);

        if (x + this.x > 0) { x = 0; this.x = 0; coords.x = 0; } if (y + this.y > 0) { y = 0; this.y = 0; coords.y = 0; }
        if (right > maxRight) { x = 0; this.x = (maxRight - this.width) * -1; } if (bottom > maxBottom) { y = 0; this.y = (maxBottom - this.height) * -1; }

        this.positionTiles({ 'x': x, 'y': y });
        this.notifyViewerMoved(coords);
    },

    /**
    * Make the specified coords the new center of the image placement.
    * This method is typically triggered as the result of a double-click
    * event.  The calculation considers the distance between the center
    * of the viewable area and the specified (viewer-relative) coordinates.
    * If absolute is specified, treat the point as relative to the entire
    * image, rather than only the viewable portion.
    */
    recenter: function (coords, absolute) {
        if (absolute) {
            coords.x += this.x;
            coords.y += this.y;
        }

        var motion = {
            'x': Math.floor((this.width / 2) - coords.x),
            'y': Math.floor((this.height / 2) - coords.y)
        };

        if (motion.x == 0 && motion.y == 0) {
            return;
        }

        if (PanoJS.USE_SLIDE) {
            var target = motion;
            var x, y;
            // handle special case of vertical movement
            if (target.x == 0) {
                x = 0;
                y = this.slideAcceleration;
            }
            else {
                var slope = Math.abs(target.y / target.x);
                x = Math.round(Math.pow(Math.pow(this.slideAcceleration, 2) / (1 + Math.pow(slope, 2)), .5));
                y = Math.round(slope * x);
            }

            motion = {
                'x': Math.min(x, Math.abs(target.x)) * (target.x < 0 ? -1 : 1),
                'y': Math.min(y, Math.abs(target.y)) * (target.y < 0 ? -1 : 1)
            }
        }

        this.positionTiles(motion, true);
        this.notifyViewerMoved();

        if (!PanoJS.USE_SLIDE) {
            return;
        }

        var newcoords = {
            'x': coords.x + motion.x,
            'y': coords.y + motion.y
        };

        var self = this;
        // TODO: use an exponential growth rather than linear (should also depend on how far we are going)
        // FIXME: this could be optimized by calling positionTiles directly perhaps
        this.slideAcceleration += PanoJS.SLIDE_ACCELERATION_FACTOR;
        this.slideMonitor = setTimeout(function () { self.recenter(newcoords); }, PanoJS.SLIDE_DELAY);
    },

    resize: function () {
        // IE fires a premature resize event
        if (!this.initialized) {
            return;
        }

        var newWidth = this.viewer.offsetWidth;
        var newHeight = this.viewer.offsetHeight;

        this.viewer.style.display = 'none';
        this.clear();

        var before = {
            'x': Math.floor(this.width / 2),
            'y': Math.floor(this.height / 2)
        };

        if (this.border >= 0) {
            this.fitToWindow(this.border);
        }
        else {
            this.width = newWidth;
            this.height = newHeight;
        }

        this.prepareTiles();

        var after = {
            'x': Math.floor(this.width / 2),
            'y': Math.floor(this.height / 2)
        };

        if (this.border >= 0) {
            this.x += (after.x - before.x);
            this.y += (after.y - before.y);
        }
        this.positionTiles();
        this.viewer.style.display = '';
        this.initialized = true;
        this.notifyViewerMoved();
    },

    /**
    * Resolve the coordinates from this mouse event by subtracting the
    * offset of the viewer in the browser window (or frame).  This does
    * take into account the scroll offset of the page.
    */
    resolveCoordinates: function (e) {
        return {
            'x': (e.pageX || (e.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft))) - this.left,
            'y': (e.pageY || (e.clientY + (document.documentElement.scrollTop || document.body.scrollTop))) - this.top
        }
    },

    press: function (coords) {
        this.activate(true);
        this.mark = coords;
    },

    release: function (coords) {
        this.activate(false);
        var motion = {
            'x': (coords.x - this.mark.x),
            'y': (coords.y - this.mark.y)
        };

        var multiplier = (Math.pow(2, this.zoomLevel) / 2);
        var right = ((this.x + (this.width * -1) + motion.x) * -1); var bottom = ((this.y + (this.height * -1) + motion.y) * -1);
        var maxRight = PanoJS.MinW * multiplier; var maxBottom = PanoJS.MinH * multiplier;

        if (this.x + motion.x >= 0) { motion.x = 0; this.x = 0; } if (this.y + motion.y >= 0) { motion.y = 0; this.y = 0; }
        if (right > maxRight) { motion.x = 0; this.x = (maxRight - this.width) * -1; } if (bottom > maxBottom) { motion.y = 0; this.y = (maxBottom - this.height) * -1; }

        this.x += motion.x; this.y += motion.y;
        this.mark = { 'x': 0, 'y': 0 };
    },

    /**
    * Activate the viewer into motion depending on whether the mouse is pressed or
    * not pressed.  This method localizes the changes that must be made to the
    * layers.
    */
    activate: function (pressed) {
        this.pressed = pressed;
        this.surface.style.cursor = (pressed ? PanoJS.GRABBING_MOUSE_CURSOR : PanoJS.GRAB_MOUSE_CURSOR);
        //this.surface.onmousemove = (pressed ? PanoJS.mouseMovedHandler : function () { });
    },

    /**
    * Check whether the specified point exceeds the boundaries of
    * the viewer's primary image.
    */
    pointExceedsBoundaries: function (coords) {
        //return (coords.x < this.x ||
        //	coords.y < this.y ||
        //	coords.x > (this.tileWidth * Math.pow(2, this.zoomLevel) + this.x) ||
        //	coords.y > (this.tileHeight * Math.pow(2, this.zoomLevel) + this.y));

        var multiplier = (Math.pow(2, this.zoomLevel) / 2);
        var maxRight = PanoJS.MinW * multiplier; var maxBottom = PanoJS.MinH * multiplier;

        return (coords.x < this.x || coords.x > (this.fullSizeW + this.x) || coords.y < this.y || coords.y > (this.fullSizeH + this.y));
    },

    // QUESTION: where is the best place for this method to be invoked?
    resetSlideMotion: function () {
        // QUESTION: should this be > 0 ?	
        if (this.slideMonitor != 0) {
            clearTimeout(this.slideMonitor);
            this.slideMonitor = 0;
        }

        this.slideAcceleration = 0;
    }
};

PanoJS.TileUrlProvider = function (baseUri, prefix, extension) {
    this.baseUri = baseUri;
    this.prefix = prefix;
    this.extension = extension;
}

PanoJS.TileUrlProvider.prototype = {
    assembleUrl: function (xIndex, yIndex, zoom) {
        return this.baseUri + '/' +
			this.prefix + zoom + '-' + xIndex + '-' + yIndex + '.' + this.extension +
			(PanoJS.REVISION_FLAG ? '?r=' + PanoJS.REVISION_FLAG : '');
    }
}

PanoJS.mousePressedHandler = function (e) {
    e = e ? e : window.event;
    // only grab on left-click
    if (e.button < 2 && !PanoJS.IS_HOTSPOT) {
        var self = this.backingBean;
        var coords = self.resolveCoordinates(e);
        if (self.pointExceedsBoundaries(coords)) {
            e.cancelBubble = true;
        }
        else {
            self.press(coords);
        }
    }
    else if ((e.button == 2) || ((e.button < 2) && PanoJS.IS_HOTSPOT)) {
        e.cancelBubble = true;
        Coordinates.GetCharacteristics(PanoJS.CURRENT_TYPOLOGY_NUMBER, PanoJS.GetCharacteristicsCallComplete, PanoJS.OnCallError);
    }

    // NOTE: MANDATORY! must return false so event does not propagate to well!
    return false;
};

PanoJS.mouseReleasedHandler = function (e) {
    e = e ? e : window.event;
    var self = this.backingBean;
    if (self.pressed) {
        // OPTION: could decide to move viewer only on release, right here
        self.release(self.resolveCoordinates(e));
    }
};

PanoJS.mouseMovedHandler = function (e) {
    e = e ? e : window.event;
    var self = this.backingBean;
    self.moveCount++;
    if (self.pressed) {
        if (self.moveCount % PanoJS.MOVE_THROTTLE == 0) {
            self.moveViewer(self.resolveCoordinates(e));
        }
    } else {
        var shouldUpdate = false;

        if (navigator.userAgent.indexOf("Firefox") != -1) {
            shouldUpdate = !self.pressed && !isCallingService;
        }
        else {
            shouldUpdate = !self.pressed && self.moveCount % 8 == 0 && !isCallingService;
        }

        /* calculate the x & y against the whole image */
        var coords = self.resolveCoordinates(e);
        var y = ((coords.y + (self.y * -1)));
        var x = ((coords.x + (self.x * -1)));

        if (shouldUpdate) {
            var multiplier = 1;
            switch (self.zoomLevel) {
                case 1:
                    multiplier = 4;
                    break;
                case 2:
                    multiplier = 2;
                    break;
                case 3:
                    multiplier = 1;
                    break;
                case 4:
                    multiplier = 0.5;
                    break;
            }

            y *= multiplier; x *= multiplier;

            isCallingService = true;

            Coordinates.GetToolTip(x.toFixed(0), y.toFixed(0), PanoJS.OnCallComplete, PanoJS.OnCallError);
        }
    }
};

PanoJS.OnCallComplete = function(result, txtresult, methodName) {
    var values = result.split(':');

    if (result == '') {
        $('.typologyToolTip').hide();
    }
    else {
        $('.typologyToolTip').html(values[0] + '. ' + values[1]);
        $('.typologyToolTip').show();
    }

    if (PanoJS.VIEWERS[0].zoomLevel == 3 && values[2] != null && values[2].toLowerCase() == 'true') {
        PanoJS.VIEWERS[0].surface.style.cursor = 'pointer';

        PanoJS.CURRENT_TYPOLOGY = values[1]
        PanoJS.CURRENT_TYPOLOGY_NUMBER = values[0]
        PanoJS.IS_HOTSPOT = true;

    } else {
        PanoJS.VIEWERS[0].surface.style.cursor = 'default';

        PanoJS.CURRENT_TYPOLOGY = values[1]
        PanoJS.CURRENT_TYPOLOGY_NUMBER = values[0]
        PanoJS.IS_HOTSPOT = false;
    }

    isCallingService = false;
};

PanoJS.OnCallError = function(error, userContext, methodName) {
    if (error !== null) { alert(error.get_message()); }
    isCallingService = false;
}

PanoJS.GetCharacteristicsCallComplete = function (result, txtresult, methodName) {
    if (result != null && result != '') {
        $('.infoBackground').show();
        $('.typologyInfo > h4').html('<strong>' + PanoJS.CURRENT_TYPOLOGY_NUMBER + '. ' + PanoJS.CURRENT_TYPOLOGY + '</strong>');
        $('.typologyCharacteristics').html(result);
        $('.typologyInfo').fadeIn();
    }
}

PanoJS.mouseRightPressedHandler = function (e) {
    e = e ? e : window.event;

    if (e.button == 0 || e.button == 2) {
        e.cancelBubble = true;
        return false;
    }
}

PanoJS.zoomInHandler = function (e) {
    e = e ? e : window.event;
    var self = this.parentNode.parentNode.backingBean;
    self.zoom(1);
    return false;
};

PanoJS.zoomOutHandler = function (e) {
    e = e ? e : window.event;
    var self = this.parentNode.parentNode.backingBean;
    self.zoom(-1);
    return false;
};

PanoJS.doubleClickHandler = function (e) {
    e = e ? e : window.event;
    var self = this.backingBean;
    coords = self.resolveCoordinates(e);
    if (!self.pointExceedsBoundaries(coords)) {
        self.resetSlideMotion();
        self.recenter(coords);
    }
};

PanoJS.keyboardMoveHandler = function (e) {
    e = e ? e : window.event;
    for (var i = 0; i < PanoJS.VIEWERS.length; i++) {
        var viewer = PanoJS.VIEWERS[i];
        if (e.keyCode == 38)
            viewer.positionTiles({ 'x': 0, 'y': -PanoJS.MOVE_THROTTLE }, true);
        if (e.keyCode == 39)
            viewer.positionTiles({ 'x': -PanoJS.MOVE_THROTTLE, 'y': 0 }, true);
        if (e.keyCode == 40)
            viewer.positionTiles({ 'x': 0, 'y': PanoJS.MOVE_THROTTLE }, true);
        if (e.keyCode == 37)
            viewer.positionTiles({ 'x': PanoJS.MOVE_THROTTLE, 'y': 0 }, true);
    }
}

PanoJS.keyboardZoomHandler = function (e) {
    e = e ? e : window.event;
    for (var i = 0; i < PanoJS.VIEWERS.length; i++) {
        var viewer = PanoJS.VIEWERS[i];
        if (e.keyCode == 109)
            viewer.zoom(-1);
        if (e.keyCode == 107)
            viewer.zoom(1);
    }
}

PanoJS.MoveEvent = function (x, y) {
    this.x = x;
    this.y = y;
};

PanoJS.ZoomEvent = function (x, y, level, percentage) {
    this.x = x;
    this.y = y;
    this.percentage = percentage;
    this.level = level;
};
