/**
 * 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 = [];
	this.viewerMoveStopedListeners = [];
	// 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.tileSize = (options.tileSize ? options.tileSize : GSIV.TILE_SIZE);

	// 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.currentX = 0;
	this.currentY = 0;
	this.border = -1;
	this.mark = { 'x' : 0, 'y' : 0 };
	this.pressed = false;
	this.tiles = [];
	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;

	// add to viewer registry
	GSIV.VIEWERS[GSIV.VIEWERS.length] = this;

	this.disableScroll = true;
}

GSIV.disableScroll = true;
// 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 = 'Dosegli ste najbolj oddaljen pogled.';//'Cannot zoom out past the current level.';
//GSIV.MSG_BEYOND_MAX_ZOOM = 'Dosegli ste največjo povečavo.'; //'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 = 'blank.gif';
GSIV.LOADING_TILE_IMAGE = 'blank.gif';
GSIV.INITIAL_PAN = { 'x' : .5, 'y' : .5 };
GSIV.USE_LOADER_IMAGE = true;
GSIV.USE_SLIDE = true;
GSIV.USE_KEYBOARD = true;

//custom consts
GSIV.WORLD_X_OFFSET = 360000;
GSIV.WORLD_Y_OFFSET = 240000;
                        //pixel in m      //zoom 12 //tile size
GSIV.WORLDUNITCONST = (2.54 / 100 / 96) * 1000 * 256; // 68; //must be same value as in picture generator

// 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 = '-moz-grab, crosshair'; //'hand'; //'crosshair'; //(navigator.userAgent.search(/KHTML|Opera/i) >= 0 ? 'pointer' : (document.attachEvent ? 'url(_NewMap/assets/gfx/grab.cur)' : '-moz-grab'));
//GSIV.GRABBING_MOUSE_CURSOR = '-moz-grabbing, pointer'; //(navigator.userAgent.search(/KHTML|Opera/i) >= 0 ? 'move' : (document.attachEvent ? 'url(_NewMap/assets/gfx/grabbing.cur)' : '-moz-grabbing'));

GSIV.GRAB_MOUSE_CURSOR =  (document.attachEvent ? 'url(_NewMap/assets/gfx/grab.cur), crosshair' : '-moz-grab');
GSIV.GRABBING_MOUSE_CURSOR = (document.attachEvent ? 'url(_NewMap/assets/gfx/grabbing.cur), pointer' : '-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;
}

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);
        }

        calcHeight -= 85;
        calcWidth -= 20;

        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';
    },

    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;
        }

        var fullSize = this.tileSize;
        // explicit set of zoom level
        if (this.zoomLevel >= 0 && this.zoomLevel <= this.maxZoomLevel) {
            fullSize = this.tileSize * Math.pow(2, this.zoomLevel);
        }
        // calculate the zoom level based on what fits best in window
        else {
            this.zoomLevel = -1;
            fullSize = this.tileSize / 2;
            do {
                this.zoomLevel += 1;
                fullSize *= 2;
            } while (fullSize < Math.max(this.width, this.height));
        }

        // move top level up and to the left so that the image is centered
        this.x = Math.floor((fullSize - this.width) * -this.initialPan.x);
        this.y = Math.floor((fullSize - this.height) * -this.initialPan.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.name == 'navigator' || control.name == 'zoomerMap' || control.name == 'zoomGaugeMap' || control.name == 'moveGaugeMap') {
                        for (var control2 = control.firstChild; control2; control2 = control2.nextSibling) {
                            if (control2.className) {
                                control2.onclick = GSIV[control2.className + 'Handler'];
                            }
                        }
                    }
                    if (control.className) {
                        control.onclick = GSIV[control.className + 'Handler'];
                    }
                }
            }
        }

        this.viewer.backingBean = this;
        this.surface.style.cursor = GSIV.GRAB_MOUSE_CURSOR;
        this.prepareTiles();
        this.initialized = true;
    },

    prepareTiles: function() {
        var rows = Math.ceil(this.height / this.tileSize) + 1;
        var cols = Math.ceil(this.width / this.tileSize) + 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 tileLayerCol = [];
                var tile = {
                    'element': null,
                    'posx': 0,
                    'posy': 0,
                    'xIndex': c,
                    'yIndex': r,
                    'LayerIdx': 0,
                    'qx': c,
                    'qy': r
                };

                tileLayerCol.push(tile);

                if (transparentTiles) //ie7, ff we can have trans layer
                {
                    var tile2 = {
                        'element': null,
                        'posx': 0,
                        'posy': 0,
                        'xIndex': c,
                        'yIndex': r,
                        'LayerIdx': 1,
                        'qx': c,
                        'qy': r
                    };

                    tileLayerCol.push(tile2);
                }
                tileCol.push(tileLayerCol);


            }

            this.tiles.push(tileCol);
        }

        this.surface.onmousemove = GSIV.mouseMovedHandlerNoGrab;
        this.surface.onmousedown = GSIV.mousePressedHandler;
        this.surface.onmouseup = this.surface.onmouseout = GSIV.mouseReleasedHandler;
        this.surface.ondblclick = GSIV.doubleClickHandler;
        this.surface.onmousewheel = GSIV.mouseWheelHandler;
        if (window.addEventListener)
            this.surface.addEventListener('DOMMouseScroll', GSIV.mouseWheelHandler, false); /** DOMMouseScroll is for mozilla. */


        //this.surface.oncontextmenu = "function() {alert(1); return false;};"
        //this.surface.onclick = GSIV.clickHandler;
        if (GSIV.USE_KEYBOARD) {
            window.onkeypress = GSIV.keyboardMoveHandler;
            window.onkeydown = GSIV.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 };
        }

        for (var c = 0; c < this.tiles.length; c++) {
            for (var r = 0; r < this.tiles[c].length; r++) {
                for (var l = 0; l < this.tiles[c][r].length; l++) {
                    var tile = this.tiles[c][r][l];

                    tile.posx = (tile.xIndex * this.tileSize) + this.x + motion.x;
                    tile.posy = (tile.yIndex * this.tileSize) + 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.tileSize) + this.x + motion.x;
                        } while (tile.posx > this.width);

                        if (tile.posx + this.tileSize < 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.tileSize) {
                            tile.xIndex += this.tiles.length;
                            tile.posx = (tile.xIndex * this.tileSize) + 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.tileSize) + this.y + motion.y;
                        } while (tile.posy > this.height);

                        if (tile.posy + this.tileSize < 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.tileSize) {
                            tile.yIndex += this.tiles[c].length;
                            tile.posy = (tile.yIndex * this.tileSize) + 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);
                        /*if (tile.LayerIdx == 1)
                        tile.element.style.zIndex = '100';*/

                        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;
            this.currentX = this.x;
            this.currentY = this.y;
        }
        else {
            this.currentX = this.x + motion.x;
            this.currentY = this.y + motion.y;
        }



    },

    /**
    * 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(2, this.zoomLevel);
            var low = tile.yIndex >= Math.pow(2, this.zoomLevel);
            if (high || left || low || right) {
                useBlankImage = true;
            }
        }

        if (useBlankImage) {
            tileImgId = 'blank:' + tile.qx + ':' + tile.qy + ':' + tile.LayerIdx;
            src = this.cache['blank'].src;
        }
        else {
            if (tile.LayerIdx == 1 && (trajekt == false && icons == "0" && routeID == "" || transparentTiles == false)) {
                //empty transparent layer
                tileImgId = 'blank:' + tile.qx + ':' + tile.qy + ':' + tile.LayerIdx;
                src = this.cache['blank'].src;
            }
            else
                tileImgId = src = this.tileUrlProvider.assembleUrl(tile.xIndex, tile.yIndex, this.zoomLevel, tile.LayerIdx);
        }

        // 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, tile.LayerIdx);
        }

        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 + ':' + tile.LayerIdx;
            var loadingImg = this.cache[loadingImgId];
            if (loadingImg == null) {
                loadingImg = this.cache[loadingImgId] = this.createPrototype(this.cache['loading'].src, tile.LayerIdx);
            }

            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, layer) {
        var img = document.createElement('img');
        img.src = src;
        //img.relativeSrc = src;
        img.className = GSIV.TILE_STYLE_CLASS;
        img.style.width = this.tileSize + 'px';
        img.style.height = this.tileSize + 'px';
        /*
        //tooo slow use just one transparent layer
        if (layer == 1 && src != this.cache['blank'].src)
        {
        img.src = this.cache['blank'].src;
        img.relativeSrc = this.cache['blank'].src;
        // runtimeStyle (better?) style
        img.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='"+src+"', sizingMethod='scale', enabled='true')" ;
        }*/
        return img;
    },

    addViewerMovedListener: function(listener) {
        this.viewerMovedListeners.push(listener);
    },

    addViewerMoveStopedListener: function(listener) {
        this.viewerMoveStopedListeners.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)
				)
			);
        }
    },

    /**
    * Notify listeners of a move event on the viewer.
    */
    notifyViewerMoveStoped: function(coords) {
        if (typeof coords == 'undefined') {
            coords = { 'x': 0, 'y': 0 };
        }

        for (var i = 0; i < this.viewerMoveStopedListeners.length; i++) {
            this.viewerMoveStopedListeners[i].viewerMoveStoped(
				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) {
            direction = 0 - this.zoomLevel;
            if (this.zoomLevel == 0) {
                alert(GSIV.MSG_BEYOND_MIN_ZOOM);
                return;
            }
        }
        else if (this.zoomLevel + direction > this.maxZoomLevel) {
            direction = this.maxZoomLevel - this.zoomLevel;
            if (this.maxZoomLevel == this.zoomLevel) {
                alert(GSIV.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.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) {
        this.positionTiles({ 'x': (coords.x - this.mark.x), 'y': (coords.y - this.mark.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) {
            this.notifyViewerMoveStoped();
            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); }, GSIV.SLIDE_DELAY);
    },

    resize: function() {
        // IE fires a premature resize event
        if (!this.initialized) {
            return;
        }

        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();

        //set new left offset since the relation between viewer and browser size changed
        if (document.body.offsetWidth > this.width)
            this.left = (((document.body.offsetWidth + 29) - this.width) / 2);
        else
            this.left = 50;

    },

    /**
    * 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) {

        //uncomet to track mouse position
//        window.status =
//        "mouse: " + ((e.pageX || (e.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft))) - /*this.left*/0)
//        + " left offset: " + this.left;

        //set new viewer coordinates
        return {
            'x': ((e.pageX || (e.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft))) - this.left)/* + (maximized ? 355 : 0)*/, //(maximized ? 160 : 0),    // hardcoded corrections when maximized
            'y': ((e.pageY || (e.clientY + (document.documentElement.scrollTop || document.body.scrollTop))) - this.top) + (maximized ? 365 : 0)//(maximized ? 403 : 0)
        }
    },

    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)
        };

        this.x += motion.x;
        this.y += motion.y;
        this.mark = { 'x': 0, 'y': 0 };

        this.positionTiles({ 'x': 0, 'y': 0 }, true);  //bugfixDD
        this.notifyViewerMoved();
        this.notifyViewerMoveStoped();
    },

    moveToPosition: function(xc, yc, zoom2, disableSlide) {

        if (zoom2 < 0) {
            alert(GSIV.MSG_BEYOND_MIN_ZOOM);
            zoom2 = 0;
            //return;
        }
        else if (zoom2 > this.maxZoomLevel) {
            alert(GSIV.MSG_BEYOND_MAX_ZOOM);
            zoom2 = this.maxZoomLevel;
            //return;
        }

        this.zoomLevel = zoom2;
        this.notifyViewerZoomed();

        var unit = GSIV.WORLDUNITCONST * Math.pow(2, 12 - this.zoomLevel);
        var xInd = (xc - GSIV.WORLD_X_OFFSET) / unit;
        var yInd = (GSIV.WORLD_Y_OFFSET - yc) / unit;

        var slideOld = GSIV.USE_SLIDE;

        if (this.zoomLevel > 5 || disableSlide)
            GSIV.USE_SLIDE = false;

        this.recenter({ 'x': xInd * this.tileSize, 'y': yInd * this.tileSize }, true);

        GSIV.USE_SLIDE = slideOld;
    },

    getCenterWorldCoord: function() {
        var xc = (this.x * (-1)) + this.width / 2;
        var yc = (this.y * (-1)) + this.height / 2;

        var a = this.getWorldCoord(xc, yc);
        //window.status = a.x + " - " + a.y + " - " + " - " + a.cx + " - " + a.cy;
        return a;
    },

    getCurrentWorldCoord: function(xc, yc) {
        var xc = (this.x * (-1)) + xc;
        var yc = (this.y * (-1)) + yc;

        var a = this.getWorldCoord(xc, yc);
        //window.status = a.x + " - " + a.y + " - " + " - " + a.cx + " - " + a.cy;
        return a;
    },

    getWorldCoord: function(xc, yc) {
        var unit = GSIV.WORLDUNITCONST * Math.pow(2, 12 - this.zoomLevel);

        var wx = Math.round((xc / this.tileSize) * unit + GSIV.WORLD_X_OFFSET);
        var wy = GSIV.WORLD_Y_OFFSET - Math.round((yc / this.tileSize) * unit);

        return { 'x': wx, 'y': wy, 'cx': xc, 'cy': yc };
    },

    getImageCoord: function(xc, yc) {
        var unit = GSIV.WORLDUNITCONST * Math.pow(2, 12 - this.zoomLevel);

        var x = Math.round(this.tileSize * (xc - GSIV.WORLD_X_OFFSET) / unit);
        var y = Math.round((GSIV.WORLD_Y_OFFSET - yc) * this.tileSize / unit);

        return { 'x': x, 'y': y, 'cx': xc, 'cy': yc };
    },

    getScreenCoord: function(xc, yc) {
        var a = this.getImageCoord(xc, yc);
        //not finished yet
        var x = (this.x) + a.x;
        var y = (this.y) + a.y;

        return { 'x': x, 'y': y, 'cx': xc, 'cy': yc };
    },



    /**
    * 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 ? GSIV.GRABBING_MOUSE_CURSOR : GSIV.GRAB_MOUSE_CURSOR);
        this.surface.onmousemove = (pressed ? GSIV.mouseMovedHandler : GSIV.mouseMovedHandlerNoGrab);
    },

    /**
    * Check whether the specified point exceeds the boundaries of
    * the viewer's primary image.
    */
    pointExceedsBoundaries: function(coords) {
        return false;
        return (coords.x < this.x ||
			coords.y < this.y ||
			coords.x > (this.tileSize * Math.pow(2, this.zoomLevel) + this.x) ||
			coords.y > (this.tileSize * Math.pow(2, this.zoomLevel) + this.y));
    },

    // 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;
    }

};

GSIV.TileUrlProvider = function(baseUri, prefix, extension) {
	this.baseUri = baseUri;
	this.prefix = prefix;
	this.extension = extension;
}

GSIV.TileUrlProvider.prototype = {
	assembleUrl: function(xIndex, yIndex, zoom, layer) {
	var curZoom = 1000 * Math.pow(2,12 - zoom);
	
	var unit = GSIV.WORLDUNITCONST * Math.pow(2,12 - zoom);
	var x = Math.round((xIndex * unit + (xIndex+1)*unit)/2) + GSIV.WORLD_X_OFFSET;
	var y = GSIV.WORLD_Y_OFFSET - Math.round((yIndex * unit + (yIndex+1)*unit)/2);
	
	var params = "zoom="+ curZoom +"&x="+x+"&y="+y+"&size=" + 256;
	
	if (layer != 1 && layer != '1')
	{
	    //layer 0 --> map
	    params += "&theme=" + theme + "S:Tile" + "&xIdx=" + xIndex + "&yIdx=" + yIndex + "&zoomIdx=" + zoom; 
	}
	else
	{
	    //layer 1 --> additional stuff: trajekt, path,...
	    params += "&theme=TransparentViewS:Tile&icon=" + icons + "&RouteID="+routeID;
	    if (trajekt)
	        params+="&t=1";
	}
	
	//var address="mapgetmap.aspx?" + encrypt("type=map&"+params);
	var address="";
	if ((xIndex + yIndex) % 2 == 0)
	    address="mapgetmapFast.ashx?" + encrypt("type=map&"+params);
	else
	    address= secondMapUrl + "mapgetmapFast.ashx?" + encrypt("type=map&"+params);
	    

	return address;
		/*return this.baseUri + '/' +
			this.prefix + zoom + '-' + xIndex + '-' + yIndex + '.' + this.extension +
			(GSIV.REVISION_FLAG ? '?r=' + GSIV.REVISION_FLAG : '');*/
	}
}

GSIV.mousePressedHandler = function(e) {
	e = e ? e : window.event;
	// only grab on left-click
	if (e.button < 2) {
		var self = this.backingBean;
		var coords = self.resolveCoordinates(e);
		if (self.pointExceedsBoundaries(coords)) {
			e.cancelBubble = true;
		}
		else {
			self.press(coords);
		}
	}
	
	// NOTE: MANDATORY! must return false so event does not propagate to well!
	e.returnValue = false; 
    e.cancelBubble = true;
	return false;
};

disableScroll = true;
GSIV.disableScroll = true;
this.disableScroll = true;

GSIV.mouseWheelHandler = function(e) {
    e = e ? e : window.event;
    if (disableScroll == true) {
        return;
    }
    delta = 0;
    if (e.wheelDelta) { /* IE/Opera. */
        delta = e.wheelDelta / 120;
        /** In Opera 9, delta differs in sign as compared to IE.
        */
        if (window.opera)
            delta = -delta;
    } else if (e.detail) { /** Mozilla case. */
        /** In Mozilla, sign of delta is different than in IE.
        * Also, delta is multiple of 3.
        */
        delta = -e.detail / 3;
    }
    /** If delta is nonzero, handle it.
    * Basically, delta is now positive if wheel was scrolled up,
    * and negative, if wheel was scrolled down.
    */
    if (delta) {
        var self = this.backingBean;
        if (delta < 0)
            self.zoom(getZoomStep(-1));
        else
            self.zoom(getZoomStep(1));
    }

    /** Prevent default actions caused by mouse wheel.
    * That might be ugly, but we handle scrolls somehow
    * anyway, so don't bother here..
    */
    if (e.preventDefault)
        e.preventDefault();
    e.returnValue = false;
};

GSIV.mouseReleasedHandler = function(e) {
    e = e ? e : window.event;

    var self = this.backingBean;
    if (self.pressed) {
        disableScroll = false;
        var maxZoomReached = false;
        // OPTION: could decide to move viewer only on release, right here
        var coord = self.resolveCoordinates(e);
        if ((self.mark.x == coord.x && self.mark.y == coord.y) || e.button >= 2) {
            //only click

            if (e.button == 4) {
                hideContextMenu();
                var slideOld = GSIV.USE_SLIDE;
                GSIV.USE_SLIDE = false;
                self.recenter(self.resolveCoordinates(e));
                GSIV.USE_SLIDE = slideOld;
                self.zoom(getZoomStep(-1));

            }
            else if (e.button >= 2) {

                //  var self = this.backingBean;
                //  self.zoom(getZoomStep(-1));
                e.returnValue = false;
                e.cancelBubble = true;
                showContextMenu(coord, e);
                return false;
            }
            else {
                hideContextMenu();
                var slideOld = GSIV.USE_SLIDE;
                GSIV.USE_SLIDE = false;
                self.recenter(self.resolveCoordinates(e));
                GSIV.USE_SLIDE = slideOld;
                if (self.maxZoomLevel != self.zoomLevel) {     // solves unintended map movement problem caused by alert dialog
                    self.zoom(getZoomStep(1));
                } else {
                    maxZoomReached = true;
                }
            }
        }
        self.release(coord);
        if (maxZoomReached) {
            alert(GSIV.MSG_BEYOND_MAX_ZOOM);
        }
    }
    else {
        if (e.button == 4) {
            hideContextMenu();
            var slideOld = GSIV.USE_SLIDE;
            GSIV.USE_SLIDE = false;
            self.recenter(self.resolveCoordinates(e));
            GSIV.USE_SLIDE = slideOld;
            self.zoom(getZoomStep(-1));

        }
        else if (e.button >= 2) {
            var coord = self.resolveCoordinates(e);
            //    var self = this.backingBean;
            //    self.zoom(getZoomStep(-1));
            e.returnValue = false;
            e.cancelBubble = true;
            showContextMenu(coord, e);
            return false;
        }
    }

    e.returnValue = false;
    e.cancelBubble = true;
    return false;
};

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.mouseMovedHandlerNoGrab = function(e) {
	e = e ? e : window.event;
	var self = this.backingBean;
	
    var mc = self.resolveCoordinates(e);
    checkIconPosition(mc.x, mc.y);
};

GSIV.zoomInHandler = function(e) {
	e = e ? e : window.event;
	var self = this.parentNode.parentNode.parentNode.backingBean;
	self.zoom(getZoomStep(1));
	return false;
};

GSIV.zoomOutHandler = function(e) {
	e = e ? e : window.event;
	var self = this.parentNode.parentNode.parentNode.backingBean;
	self.zoom(getZoomStep(-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.resetSlideMotion();
		self.recenter(coords);
	}
};

GSIV.clickHandler = function(e) {
	e = e ? e : window.event;
	var self = this.backingBean;
	coords = self.resolveCoordinates(e);
	if (!self.pointExceedsBoundaries(coords)) {
		//self.resetSlideMotion();
		//self.recenter(coords);
		//setTimeout(function() { self.zoom(getZoomStep(1)); }, 2000 );
		/*
		var slideOld = GSIV.USE_SLIDE;
		GSIV.USE_SLIDE = false;
	    self.recenter(coords);
	    GSIV.USE_SLIDE = slideOld;
		self.zoom(getZoomStep(1));*/
	}
	return false;
};

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);
				viewer.notifyViewerMoved();
	    }
		if (e.keyCode == 39)
		{
				viewer.positionTiles({'x': -GSIV.MOVE_THROTTLE,'y': 0}, true);
				viewer.notifyViewerMoved();
		}
		if (e.keyCode == 40)
		{
				viewer.positionTiles({'x': 0,'y': -GSIV.MOVE_THROTTLE}, true);
				viewer.notifyViewerMoved();
		}
		if (e.keyCode == 37)
		{
				viewer.positionTiles({'x': GSIV.MOVE_THROTTLE,'y': 0}, true);
				viewer.notifyViewerMoved();
		}
	}
}

GSIV.moveDownHandler = function(e) {
	e = e ? e : window.event;
	for (var i = 0; i < GSIV.VIEWERS.length; i++) {
		var viewer = GSIV.VIEWERS[i];
		viewer.positionTiles({'x': 0,'y': -GSIV.MOVE_THROTTLE}, true);
		viewer.notifyViewerMoved();
		//viewer.notifyViewerMoveStoped();
	}
}

GSIV.moveUpHandler = function(e) {
	e = e ? e : window.event;
	for (var i = 0; i < GSIV.VIEWERS.length; i++) {
		var viewer = GSIV.VIEWERS[i];
		viewer.positionTiles({'x': 0,'y': GSIV.MOVE_THROTTLE}, true);
		viewer.notifyViewerMoved();
		//viewer.notifyViewerMoveStoped();
	}
}

GSIV.moveLeftHandler = function(e) {
	e = e ? e : window.event;
	for (var i = 0; i < GSIV.VIEWERS.length; i++) {
		var viewer = GSIV.VIEWERS[i];
		viewer.positionTiles({'x': GSIV.MOVE_THROTTLE,'y': 0}, true);
		viewer.notifyViewerMoved();
		//viewer.notifyViewerMoveStoped();
	}
}

GSIV.moveRightHandler = function(e) {
	e = e ? e : window.event;
	for (var i = 0; i < GSIV.VIEWERS.length; i++) {
		var viewer = GSIV.VIEWERS[i];
		viewer.positionTiles({'x': -GSIV.MOVE_THROTTLE,'y': 0}, true);
		viewer.notifyViewerMoved();
		//viewer.notifyViewerMoveStoped();
	}
}

GSIV.moveRightSlideHandler = function(e) {
   
    for (var i = 0; i < GSIV.VIEWERS.length; i++) {
		var viewer = GSIV.VIEWERS[i];
	    viewer.resetSlideMotion();
	    viewer.recenter({'x': viewer.width, 'y':viewer.height/2});
	}
}

GSIV.moveLeftSlideHandler = function(e) {
    for (var i = 0; i < GSIV.VIEWERS.length; i++) {
		var viewer = GSIV.VIEWERS[i];
	    viewer.resetSlideMotion();
	    viewer.recenter({'x': 0, 'y':viewer.height/2});
	}
}

GSIV.moveUpSlideHandler = function(e) {
    for (var i = 0; i < GSIV.VIEWERS.length; i++) {
		var viewer = GSIV.VIEWERS[i];
	    viewer.resetSlideMotion();
	    viewer.recenter({'x': viewer.width/2, 'y':0});
	}
}

GSIV.moveDownSlideHandler = function(e) {
    for (var i = 0; i < GSIV.VIEWERS.length; i++) {
		var viewer = GSIV.VIEWERS[i];
	    viewer.resetSlideMotion();
	    viewer.recenter({'x': viewer.width/2, 'y':viewer.height});
	}
}

GSIV.moveDefaultPosHandler = function(e) {
    for (var i = 0; i < GSIV.VIEWERS.length; i++) {
		var viewer = GSIV.VIEWERS[i];
		
		var slideOld = GSIV.USE_SLIDE;
        
       // if (viewer.zoomLevel > 4)
            GSIV.USE_SLIDE = false;
        
    	viewer.zoomLevel = 1
    	viewer.notifyViewerZoomed();
    	viewer.recenter({'x': 256, 'y':256}, true);
	    
	    GSIV.USE_SLIDE = slideOld;
	}
}





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(getZoomStep(-1));
		if (e.keyCode == 107)
				viewer.zoom(getZoomStep(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;
};

