/**
 * Giant Scalable Image Viewer (GSIV) 1.0
 *
 * 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 GSIV(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
 */
function GSIV(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' &&
		GSIV.isInstance(options.tileUrlProvider, GSIV.TileUrlProvider)) {
		this.tileUrlProvider = options.tileUrlProvider;
	}
	else {
		this.tileUrlProvider = new GSIV.TileUrlProvider(
			options.tileBaseUri ? options.tileBaseUri : GSIV.TILE_BASE_URI,
			options.tilePrefix ? options.tilePrefix : GSIV.TILE_PREFIX,
			options.tileExtension ? options.tileExtension : GSIV.TILE_EXTENSION
		);
	}

	this.tileWidth = (options.tileWidth ? options.tileWidth : GSIV.TILE_SIZE);
	this.tileHeight = (options.tileHeight ? options.tileHeight : GSIV.TILE_SIZE);
	this.debug = (options.debug ? options.debug : false);

	// 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 : GSIV.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.fullSizeW = 0;
	this.fullSizeH = 0;
	this.minSizeW = options.minSizeW;
	this.minSizeH = options.minSizeH;
	this.onHotspot = false;
	this.mouseOverHotspots = [];
	
	this.border = -1;
	this.mark = { 'x' : 0, 'y' : 0 };
	this.pressed = false;
	this.tiles = [];
	this.hotspots = [];
	this.cache = {};
	var blankTile = options.blankTile ? options.blankTile : GSIV.BLANK_TILE_IMAGE;
	var loadingTile = options.loadingTile ? options.loadingTile : GSIV.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;
	
	/* added for DS project */
	this.imageLocation = 'default';
	this.scaleImage = options.scaleImage;
	this.scaleImageLocation = options.scaleImageLocation;

	// add to viewer registry
	GSIV.VIEWERS[GSIV.VIEWERS.length] = this;
}

// project specific variables
GSIV.PROJECT_NAME = 'GSIV';
GSIV.PROJECT_VERSION = '1.0.0';
GSIV.REVISION_FLAG = '';

// CSS definition settings
GSIV.SURFACE_STYLE_CLASS = 'surface';
GSIV.WELL_STYLE_CLASS = 'well';
GSIV.CONTROLS_STYLE_CLASS = 'controls'
GSIV.TILE_STYLE_CLASS = 'tile';

// language settings
GSIV.MSG_BEYOND_MIN_ZOOM = 'Cannot zoom out past the current level.';
GSIV.MSG_BEYOND_MAX_ZOOM = 'Cannot zoom in beyond the current level.';

// defaults if not provided as constructor options
GSIV.TILE_BASE_URI = 'tiles';
GSIV.TILE_PREFIX = 'tile-';
GSIV.TILE_EXTENSION = 'jpg';
GSIV.TILE_SIZE = 256;
GSIV.BLANK_TILE_IMAGE = 'norfolk.jpg';
GSIV.LOADING_TILE_IMAGE = 'norfolk.jpg';
GSIV.INITIAL_PAN = { 'x' : .5, 'y' : .5 };
GSIV.USE_LOADER_IMAGE = true;
GSIV.USE_SLIDE = true;
GSIV.USE_KEYBOARD = true;

GSIV.DEFAULT_X = -100;
GSIV.DEFAULT_Y = 0;

// performance tuning variables
GSIV.MOVE_THROTTLE = 3;
GSIV.SLIDE_DELAY = 40;
GSIV.SLIDE_ACCELERATION_FACTOR = 5;

// the following are calculated settings
GSIV.DOM_ONLOAD = (navigator.userAgent.indexOf('KHTML') >= 0 ? false : true);
GSIV.GRAB_MOUSE_CURSOR = (navigator.userAgent.search(/KTML|Opera/i) >= 0 ? 'pointer' : (document.attachEvent ? '/grab.cur' : '-moz-grab'));
GSIV.GRABBING_MOUSE_CURSOR = (navigator.userAgent.search(/KTML|Opera/i) >= 0 ? 'move' : (document.attachEvent ? '/grabbing.cur' : '-moz-grabbing'));

// registry of all known viewers
GSIV.VIEWERS = [];

// utility functions
GSIV.isInstance = function(object, clazz) {
	while (object != null) {
		if (object == clazz.prototype) {
			return true;
		}

		object = object.__proto__;
	}

	return false;
}

var tileNames = new Array(8);
tileNames[0] = new Array('N01_TF60c', 'N02_TF60d', 'N03_TF80c', 'N04_TF80d', 'N05_TG00c', 'N06_TG00d', 'N07_TG20c', 'N08_TG20d', 'N09_TG40c', 'N10_TG40d');
tileNames[1] = new Array('x01', 'x02', 'x03', 'x04', 'x05', 'x06', 'x07', '01_TM28b', '02_TM48A', '03_TM48B');
tileNames[2] = new Array('04_TL68C', '05_TL68D', '06_TL88C', '07_TL88D', '08_TM08C', '09_TM08D', '10_TM28C', '11_TM28D', '12_TM48C', '13_TM48D');
tileNames[3] = new Array('14_TL66A', '15_TL66B', '16_TL86A', '17_TL86B', '18_TM06A', '19_TM06B', '20_TM26A', '21_TM26B', '22_TM46A', '23_TM46B');
tileNames[4] = new Array('24_TL66C', '25_TL66D', '26_TL86C', '27_TL86D', '28_TM06C', '29_TM06D', '30_TM26C', '31_TM26D', '32_TM46C', 'x08');
tileNames[5] = new Array('33_TL64A', '34_TL64B', '35_TL84A', '36_TL84B', '37_TM04A', '38_TM04B', '39_TM24A', '40_TM24B', '41_TM44A', 'x09');
tileNames[6] = new Array('42_TL64B', '43_TL64C', '44_TL84C', '45_TL84D', '46_TM04C', '47_TM04D', '48_TM24C', '49_TM24D', '50_TM44C', 'x10');
tileNames[7] = new Array('x11', 'x12', '51_TL82A', '52_TL82B', '53_TM02A', '54_TM02B', '55_TM22A', '56_TM22B', 'x13', 'x14');

GSIV.prototype = {

    /**
    * 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';
    },
    calcFullSize: function() {
        //this.fullSizeW = this.minSizeW * Math.pow(2, (this.zoomLevel - 1));
        //this.fullSizeH = this.minSizeH * Math.pow(2, (this.zoomLevel - 1));

        this.fullSizeW = this.minSizeW * (this.maxZoomLevel + 1);
        this.fullSizeH = this.minSizeH * (this.maxZoomLevel + 1);
    },
    showLoading: function(show) {
        if (show)
            $get('status').style.display = '';
        else if (!show)
            $get('status').style.display = 'none';
    },
    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;
        }

        this.calcFullSize();

        //move top level up and to the left so that the image is centerfsed
        this.x = GSIV.DEFAULT_X;
        this.y = GSIV.DEFAULT_Y;

        // 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 == GSIV.SURFACE_STYLE_CLASS) {
                this.surface = child;
                child.backingBean = this;
            }
            else if (child.className == GSIV.WELL_STYLE_CLASS) {
                this.well = child;
                child.backingBean = this;
            }
            else if (child.className == GSIV.CONTROLS_STYLE_CLASS) {
                for (var control = child.firstChild; control; control = control.nextSibling) {
                    if (control.className) {
                        control.onclick = GSIV[control.className + 'Handler'];
                    }
                }
            }
        }

        this.viewer.backingBean = this;
        //this.surface.style.cursor = GSIV.GRAB_MOUSE_CURSOR;
        this.prepareTiles();

        this.getHotspots();
        this.initialized = true;
    },
    prepareTiles: function() {
        var rows = Math.ceil(this.height / this.tileWidth) + 1;
        var cols = Math.ceil(this.width / this.tileHeight) + 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 = GSIV.mousePressedHandler;
        this.surface.onmouseup = this.surface.onmouseout = GSIV.mouseReleasedHandler;
        this.surface.ondblclick = GSIV.doubleClickHandler;

        if (GSIV.USE_KEYBOARD) {
            window.onkeypress = GSIV.keyboardMoveHandler;
            window.onkeydown = GSIV.keyboardZoomHandler;
        }

        this.positionTiles();
    },
    clearHotspots: function() {
        var j;
        for (j = this.hotspots.length - 1; j >= 0; j--) {
            this.well.removeChild(this.hotspots[j].element);
        }

        Array.clear(this.hotspots);
    },
    getHotspots: function() {
        if (this.zoomLevel > 0) {
            this.showLoading(true);

            var self = this;

            var x = (self.x * -1); var y = (self.y * -1);
            var multiplier = Math.pow(2, ((this.zoomLevel - this.maxZoomLevel) * -1));

            var width = (this.width * multiplier); var height = (this.height * multiplier);
            x = (x * multiplier); y = (y * multiplier);

            Mapping.GetHotspots(x, y, width, height, this.zoomLevel, OnCallComplete, OnCallError, this);
        } else {
            this.clearHotspots();
        }
    },
    positionHotpot: function(hotspot, motion) {
        if (typeof motion == 'undefined') {
            motion = { 'x': 0, 'y': 0 };
        }

        hotspot.posx = hotspot.hotspot.X;
        hotspot.posy = hotspot.hotspot.Y;

        var multiplier = Math.pow(2, ((this.zoomLevel - this.maxZoomLevel) * -1));

        hotspot.posx = hotspot.posx / multiplier;
        hotspot.posy = hotspot.posy / multiplier;

        var x = parseInt((hotspot.posx + (this.x + motion.x)));
        var y = parseInt((hotspot.posy + (this.y + motion.y)));

        hotspot.element.style.top = y + 'px';
        hotspot.element.style.left = x + 'px';
    },
    positionHotspots: function(motion, reset) {
        if (this.hotspots != null) {
            for (var i = 0; i < this.hotspots.length; i++) {
                var hotspot = this.hotspots[i];
                this.positionHotpot(hotspot, motion);
            }

            this.showLoading(false);
        }
    },
    /**
    * 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 == null) {
            motion = { 'x': 0, 'y': 0 };
        }

        this.positionHotspots(motion);

        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.
    */
    getMaxTiles: function() {
        var result = {
            'width': 2,
            'height': 2
        };
        return result;
    },
    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(this.getMaxTiles.width, this.zoomLevel + 1);
            var low = tile.yIndex >= Math.pow(this.getMaxTiles.height, this.zoomLevel + 1);

            var wider = (tile.xIndex * GSIV.tileWidth) > this.fullSizeW;
            var higher = (tile.yIndex * this.tileHeight) > this.fullSizeH;

            if (high || left || low || right || wider || higher) {
                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, this.imageLocation);
        }

        // 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 || !GSIV.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 (!GSIV.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.src = '';
        img.relativeSrc = src;
        img.className = GSIV.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 GSIV.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 GSIV.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 < 0) {
            alert(GSIV.MSG_BEYOND_MIN_ZOOM);
            return;
        }
        else if (this.zoomLevel + direction > this.maxZoomLevel) {
            alert(GSIV.MSG_BEYOND_MAX_ZOOM);
            return;
        }

        this.blank();

        this.zoomLevel += direction;

        if (this.zoomLevel == 0 && this.imageLocation == 'os') {
            this.imageLocation = 'default';
        }

        if (this.zoomLevel > 0) {
            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;

            $get(this.scaleImage).src = this.scaleImageLocation + this.zoomLevel + '.gif';
            $get('scale').style.display = '';

        } else {
            this.x = GSIV.DEFAULT_X;
            this.y = GSIV.DEFAULT_Y;

            $get('scale').style.display = 'none';
        }

        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 = (tileNames[0].length * this.tileWidth) * multiplier; var maxBottom = (tileNames.length * this.tileHeight) * 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.getHotspots();
        this.positionTiles();
        this.notifyViewerZoomed();
    },

    /** 
    * 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) {
        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 = (tileNames[0].length * this.tileWidth) * multiplier; var maxBottom = (tileNames.length * this.tileHeight) * 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, zoom) {
        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.x = 0; }
        //if (motion.y > 0) { motion.y = 0; }

        if (motion.x == 0 && motion.y == 0) {
            if (zoom) {
                this.zoom(1);
            }
            return;
        }

        if (GSIV.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 (!GSIV.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 += GSIV.SLIDE_ACCELERATION_FACTOR;
        this.slideMonitor = setTimeout(function() { self.recenter(newcoords, absolute, zoom); }, GSIV.SLIDE_DELAY);
    },

    resize: function() {
        // IE fires a premature resize event
        if (!this.initialized) {
            return;
        }

        var widthDiff = this.viewer.offsetWidth - this.width;
        var heightDiff = this.viewer.offsetHeight - this.height;

        this.left -= widthDiff;
        this.top -= heightDiff;

        this.width = this.viewer.offsetWidth;
        this.height = 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);
        }


        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();
    },

    /**
    * 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 = (tileNames[0].length * this.tileWidth) * multiplier; var maxBottom = (tileNames.length * this.tileHeight) * 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 };
    },
    isIE: function() {
        return /msie/i.test(navigator.userAgent) && !/opera/i.test(navigator.userAgent);
    },
    /**
    * 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;

        if (pressed) {
            this.surface.className = 'surface grabbing';
        } else {
            this.surface.className = 'surface grab';
        }

        if (!this.isIE()) {
            this.surface.style.cursor = (pressed ? GSIV.GRABBING_MOUSE_CURSOR : GSIV.GRAB_MOUSE_CURSOR);
        }
        this.surface.onmousemove = (pressed ? GSIV.mouseMovedHandler : GSIV.mouseMoved);
        //document.title = this.surface.className;
    },

    /**
    * Check whether the specified point exceeds the boundaries of
    * the viewer's primary image.
    */
    pointExceedsBoundaries: function(coords) {
        var x = (coords.x + (this.x * -1));
        var y = (coords.y + (this.y * -1));

        var multiplier = (Math.pow(2, this.zoomLevel) / 2);
        var tileNoX = parseInt(((x / multiplier) / this.tileWidth)); var tileNoY = parseInt(((y / multiplier) / this.tileHeight));

        var onTile = false;

        if (tileNoX >= 0 && tileNoY >= 0) {
            if (tileNoY < tileNames.length) {
                if (tileNoX < tileNames[tileNoY].length) {
                    onTile = true;
                }
            }
        }

        return (coords.x < this.x || coords.y < this.y || !onTile);
    },
    // QUESTION: where is the best place for this method to be invoked?
    resetSlideMotion: function() {
        if (this.slideMonitor != 0) {
            clearTimeout(this.slideMonitor);
            this.slideMonitor = 0;
        }

        this.slideAcceleration = 0;
    },
    isOnHotspot: function(x, y) {
        if (this.isIE()) {
            this.surface.style.cursor = '';
        } else {
            this.surface.style.cursor = GSIV.GRAB_MOUSE_CURSOR;
        }
        this.onHotspot = false;
        Array.clear(this.mouseOverHotspots);

        this.surface.title = '';

        if (this.zoomLevel > 0) {
            if (this.hotspots.length > 0) {
                var multiplier = Math.pow(2, ((this.zoomLevel - this.maxZoomLevel) * -1));
                var hotspotSize = 22 * multiplier; var j; var hx; var hy; var bx; var by;

                for (j = this.hotspots.length - 1; j >= 0; j--) {
                    hx = this.hotspots[j].hotspot.X; hy = this.hotspots[j].hotspot.Y;
                    bx = hx + hotspotSize; by = hy + hotspotSize;

                    if ((hx >= (x - hotspotSize) && bx <= (x + hotspotSize)) && (hy >= (y - hotspotSize) && by <= (y + hotspotSize))) {
                        this.surface.style.cursor = 'pointer';
                        Array.add(this.mouseOverHotspots, this.hotspots[j].hotspot);
                        this.onHotspot = true;
                        this.surface.title = this.hotspots[j].hotspot.Title;
                        //$('.surface').tooltip();
                    }
                }

                window.status = x + ' ' + y;
            }
        }
        else {
            this.surface.style.cursor = 'pointer';
            this.onHotspot = true;
        }

    }
};

GSIV.TileUrlProvider = function(baseUri, prefix, extension) {
	this.baseUri = baseUri;
	this.prefix = prefix;
	this.extension = extension;
}

GSIV.TileUrlProvider.prototype = {
	assembleUrl: function(xIndex, yIndex, zoom, location) {
	var url = this.baseUri + '?x=' + xIndex + '&y=' + yIndex + '&z=' + zoom + '&t=' + location;
		return url;
	}
}

GSIV.showInfo = function(e) {
    var hotspot = [];
    Array.add(hotspot, this.hotspot);
    GSIV.showInformation(hotspot);
}

GSIV.showInformation = function(hotspots, callreset) {
    if (hotspots.length == 1) {
        if (hotspots[0].CategoryId == '2848d2ac-7a93-11dd-a6ab-554956d89593' || hotspots[0].CategoryId == '4d8b80a4-2560-4ab0-ad8a-c850b3477993') {
            $get('divMultiple').style.display = 'none';
            $get('divInformation').style.display = 'none';
            $get('divWalk').style.display = ''; var control;
            /* set the title */
            if (hotspots[0].Image != null) { $get('informationWImage').src = hotspots[0].Image; }
            if (hotspots[0].Link != null) { $get('aInformationWLink').href = hotspots[0].Link; $get('informationWLink').style.display = ''; } else { $get('informationWLink').style.display = 'none'; }
            if (hotspots[0].Details == null) { $get('informationWText').innerHTML = '' } else { $get('informationWText').innerHTML = hotspots[0].Details };
            $get('informationWTitle').innerHTML = hotspots[0].Title;
            $get('email').href = 'mailto:discoversuffolk@suffolk.gov.uk?Subject=' + hotspots[0].Title;
            $get('email').innerHTML = 'discoversuffolk@suffolk.gov.uk';
            control = $get('informationWCategory');
            control.src = hotspots[0].CategoryImage; control.alt = hotspots[0].CategoryName; control.style.display = '';
            $get('ctl00_ctl00_cphContent_ucMapNavigation_ucInformation_tcWalks_tpAddComment_hfHotspotID').value = hotspots[0].HotspotId;

            if (callreset !== false) { ResetTabContainer(); }

            if (hotspots[0].WalkNo != null) { $get('informationWNo').innerHTML = hotspots[0].WalkNo; $get('walkno').style.display = ''; }
            else { $get('informationWNo').innerHTML = ''; $get('walkno').style.display = 'none'; }
            if (hotspots[0].Distance != null) { $get('informationWDistance').innerHTML = hotspots[0].Distance; $get('distance').style.display = ''; }
            else { $get('informationWDistance').innerHTML = ''; $get('distance').style.display = 'none'; }
            if (hotspots[0].Parish != null) { $get('informationWParish').innerHTML = hotspots[0].Parish; $get('parish').style.display = ''; }
            else { $get('informationWParish').innerHTML = ''; $get('parish').style.display = 'none'; }
            //if (hotspots[0].Grade != null) { $get('informationWGrade').innerHTML = hotspots[0].Grade; $get('grade').style.display = ''; }
            //else { $get('informationWGrade').innerHTML = ''; $get('grade').style.display = 'none'; }
            if (hotspots[0].Duration != null) { $get('informationWDuration').innerHTML = hotspots[0].Duration; $get('duration').style.display = ''; }
            else { $get('informationWDuration').innerHTML = ''; $get('duration').style.display = 'none'; }
            if (hotspots[0].CategoryId == '2848d2ac-7a93-11dd-a6ab-554956d89593') { $get('walkcycle').innerHTML = 'Walk No.:&nbsp;'; }
            else if (hotspots[0].CategoryId == '4d8b80a4-2560-4ab0-ad8a-c850b3477993') { $get('walkcycle').innerHTML = 'Route No.:&nbsp;'; }
        }
        else if (hotspots[0].CategoryId != '2848d2ac-7a93-11dd-a6ab-554956d89593' || hotspots[0].CategoryId != '4d8b80a4-2560-4ab0-ad8a-c850b3477993') {
            $get('divMultiple').style.display = 'none';
            $get('divWalk').style.display = 'none';
            $get('divInformation').style.display = ''; var control;
            /* set the title */
            if (hotspots[0].Image != null) { $get('informationImage').src = hotspots[0].Image; }
            if (hotspots[0].Link != null) { $get('aInformationLink').href = hotspots[0].Link; $get('informationLink').style.display = ''; }
            else { $get('informationLink').style.display = 'none'; }
            $get('informationTitle').innerHTML = hotspots[0].Title;
            if (hotspots[0].Details == null) { $get('informationText').innerHTML = '' } else { $get('informationText').innerHTML = hotspots[0].Details };
            //if (hotspots[0].Details == null) { $get('informationText').innerText = '' } else { $get('informationText').innerText = hotspots[0].Details };
            control = $get('informationCategory');
            control.src = hotspots[0].CategoryImage;
            control.alt = hotspots[0].CategoryName;
            control.style.display = '';
        }
    } else if (hotspots.length > 1) {
        $get('divInformation').style.display = 'none'; $get('divWalk').style.display = 'none';
        $get('divMultiple').style.display = ''; var control;

        /* list the available hotspots */
        $get('informationMTitle').innerHTML = 'Please choose an item'; $get('informationMCategory').style.display = 'none';
        var ul = document.createElement('ul'); var i = 0;

        for (i = 0; i < hotspots.length; i++) {
            var li = document.createElement('li'); var link = document.createElement('a');
            link.appendChild(document.createTextNode(hotspots[i].Title));

            link.hotspot = hotspots[i];
            $(link).click(function() { GSIV.showInfo.call(this); });

            li.appendChild(link); ul.appendChild(li);
        }

        $('#informationMText').empty();
        $get('informationMText').appendChild(ul);
    }
    var count = hotspots.length;
}

GSIV.mousePressedHandler = function(e) {
    e = e ? e : window.event;

    // only grab on left-click
    if (e.button == 2) {
        //$get('divInformation').style.display = '';
    } else if (e.button < 2) {
        var self = this.backingBean;
        var coords = self.resolveCoordinates(e);
        if (self.pointExceedsBoundaries(coords)) {
            e.cancelBubble = true;
        }
        else {
            if (self.zoomLevel > 0 && !self.onHotspot) {
                self.press(coords);
            }
            else if (self.zoomLevel > 0 && self.onHotspot) {
                GSIV.showInformation(self.mouseOverHotspots);
            } else if (self.zoomLevel == 0) {

                var maxX = 785; var newCoords;

                GSIV.USE_SLIDE = false;

                if ((coords.x > 0 && coords.x <= 393) && (coords.y > 0 && coords.y <= 238)) {
                    //alert('top left');
                    newCoords = { 'x': 193, 'y': 230 };
                } else if ((coords.x > 393 && coords.x <= 785) && (coords.y > 0 && coords.y <= 238)) {
                    //alert('top right');
                    newCoords = { 'x': 585, 'y': 230 };
                }
                if ((coords.x > 0 && coords.x <= 393) && (coords.y > 238 && coords.y <= 475)) {
                    //alert('bottom left');
                    newCoords = { 'x': 193, 'y': 485 };
                } else if ((coords.x > 393 && coords.x <= 785) && (coords.y > 238 && coords.y <= 475)) {
                    //alert('bottom right');
                    newCoords = { 'x': 585, 'y': 485 };
                }

                self.recenter(newCoords, 0, false); self.zoom(1);
                GSIV.USE_SLIDE = true;
            }
        }
    }

    // NOTE: MANDATORY! must return false so event does not propagate to well!
    return false;
};

GSIV.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));
        self.getHotspots();
    }
};

GSIV.mouseMoved = function(e) {
    e = e ? e : window.event;
    var self = this.backingBean;
    self.moveCount++;

    /* 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);

    var left = (self.x * -1); var top = (self.y * -1);

    var multiplier = (Math.pow(2, self.zoomLevel) / 2);
    var multiplier2 = Math.pow(2, ((self.zoomLevel - self.maxZoomLevel) * -1));

    var newX = parseInt(x / multiplier); var newY = parseInt(y / multiplier);
    var tileNoX = parseInt((newX / self.tileWidth)); var tileNoY = parseInt((newY / self.tileHeight));

    x = x * multiplier2; y = y * multiplier2;
    newX = x - (1600 * tileNoX); newY = y - (1600 * tileNoY);

    self.isOnHotspot(x, y);    

    /* debug information */
    if (self.debug == 'True') {
        if (tileNoX >= 0 && tileNoY >= 0) {
            var tileName = '';
            if (tileNoY < tileNames.length) {
                if (tileNoX < tileNames[tileNoY].length) {
                    tileName = tileNames[tileNoY][tileNoX];
                }
            }

            window.status = 'tileX: '
                + newX + ' (' + x + ')' + ' tileY: ' + newY + ' (' + y + ') ' + tileName + ' '
                + self.x * -1 + ' ' + self.y * -1 + ' ' + self.width + ' ' + self.height + ' '
                + left + ' ' + top + ' ';
        }
    }
}

function OnCallComplete(result, userContext, methodName) {
    var hotSpots = Sys.Serialization.JavaScriptSerializer.deserialize(result, true);
    var found = false;

    // remove any unwanted hotspots
    if (userContext.hotspots.length > 0) {
        for (j = userContext.hotspots.length - 1; j >= 0; j--) {
            found = false;
            for (i = 0; i < hotSpots.length; i++) {
                if (hotSpots[i].HotspotId == userContext.hotspots[j].hotspot.HotspotId) {
                    found = true;
                    break;
                }
            }

            if (!found) {
                userContext.well.removeChild(userContext.hotspots[j].element);
                Array.removeAt(userContext.hotspots, j);
            }
        }
    }
    

    // add new hotspots to the list
    for (i = 0; i < hotSpots.length; i++) {
        found = false;

        for (j = 0; j < userContext.hotspots.length; j++) {
            if (hotSpots[i].HotspotId == userContext.hotspots[j].hotspot.HotspotId) {
                found = true;
                break;
            }
        }

        if (!found) {
            var hotspot = {
                'hotspot': hotSpots[i],
                'element': null,
                'posx': 0,
                'posy': 0
            };
            
            var img = document.createElement('img');
            img.src = hotSpots[i].CategoryImage;
            img.className = 'hotspot';
            img.alt = hotSpots[i].Title;
            hotspot.element = img;
            userContext.hotspots.push(hotspot);
            
            userContext.well.appendChild(img);

            userContext.positionHotpot(hotspot);
        }
    }

    userContext.showLoading(false);
}
function OnCallError(error, userContext, methodName) {
    if (error !== null) {
        alert(error.get_message());
    }
}

GSIV.mouseMovedHandler = function(e) {
	e = e ? e : window.event;
	var self = this.backingBean;
	self.moveCount++;
	if (self.moveCount % GSIV.MOVE_THROTTLE == 0) {
		self.moveViewer(self.resolveCoordinates(e));
	}
};

GSIV.zoomInHandler = function(e) {
	e = e ? e : window.event;
	var self = this.parentNode.parentNode.backingBean;
	self.zoom(1);
	return false;
};

GSIV.zoomOutHandler = function(e) {
	e = e ? e : window.event;
	var self = this.parentNode.parentNode.backingBean;
	self.zoom(-1);
	return false;
};

GSIV.doubleClickHandler = function(e) {
    e = e ? e : window.event;
    var self = this.backingBean;
    coords = self.resolveCoordinates(e);
    if (!self.pointExceedsBoundaries(coords) && self.zoomLevel > 0) {
        var useSlide = GSIV.USE_SLIDE;
        var canZoom = self.zoomLevel + 1 <= self.maxZoomLevel;
        //GSIV.USE_SLIDE = !canZoom;

        self.resetSlideMotion();
        self.recenter(coords, null, canZoom);
        //GSIV.USE_SLIDE = useSlide;
    }
};

GSIV.keyboardMoveHandler = function(e) {
	e = e ? e : window.event;
	for (var i = 0; i < GSIV.VIEWERS.length; i++) {
		var viewer = GSIV.VIEWERS[i];
		if (e.keyCode == 38)
				viewer.positionTiles({'x': 0,'y': -GSIV.MOVE_THROTTLE}, true);
		if (e.keyCode == 39)
				viewer.positionTiles({'x': -GSIV.MOVE_THROTTLE,'y': 0}, true);
		if (e.keyCode == 40)
				viewer.positionTiles({'x': 0,'y': GSIV.MOVE_THROTTLE}, true);
		if (e.keyCode == 37)
				viewer.positionTiles({'x': GSIV.MOVE_THROTTLE,'y': 0}, true);
	}
}

GSIV.keyboardZoomHandler = function(e) {
	e = e ? e : window.event;
	for (var i = 0; i < GSIV.VIEWERS.length; i++) {
		var viewer = GSIV.VIEWERS[i];
		if (e.keyCode == 109)
				viewer.zoom(-1);
		if (e.keyCode == 107)
				viewer.zoom(1);
	}
}

GSIV.MoveEvent = function(x, y) {
	this.x = x;
	this.y = y;
};

GSIV.ZoomEvent = function(x, y, level, percentage) {
	this.x = x;
	this.y = y;
	this.percentage = percentage;
	this.level = level;
};