/**********************************************************************
 *
 * $Id: kaTool.js,v 1.32 2006/07/20 13:46:48 pspencer Exp $
 *
 * purpose: an API for mapViewer tools with a default navigation tool provided
 *
 * author: Paul Spencer (pspencer@dmsolutions.ca)
 *
 * The original kaTool code was written by DM Solutions Group.
 *
 * TODO:
 * 
 **********************************************************************
 *
 * Copyright (c) 2005, DM Solutions Group Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
 * DEALINGS IN THE SOFTWARE.
 *
 **********************************************************************/

/*
 * includes code inspired by code in WindowManager.js
 * Copyright 2005 MetaCarta, Inc., released under the BSD License
 */

//globally 
var kaCurrentTool = null;

/**
 * @class
 * kaTool API.<br>
 * An API for building tools that work with mapViewer.
 * To create a new tool, you need to have included this file first.  Next
 * create a function to instantiate your new tool.  All object construction
 * functions must include a parameter that references the mapViewer object on which
 * they operate.<br>
 * The object construction function must call the kaTool constructor using the
 * following syntax:<br><br>
 * kaTool.apply( this, [oKaMap] );<br><br>
 * where oKaMap is the name of the parameter to the constructor function.
 * You should then set the tool's name (this.name) and overload any functions
 * for mouse handling etc.
 *
 * @constructor kaTool
 * kaTool is the base class for tools which operate on an instance of mapViewer.
 * @param {Object} oKaMap the instance of mapViewer on which this tool should operate
 */
function kaTool( oKaMap ) {
    /** the instance of mapViewer on which this tool operates */
    this.mapViewer = oKaMap;
    /** the name of this tool */
    this.name = 'kaTool';
    /** info tools get all events all the time */
    this.bInfoTool = false;
    
    // Default for mouse wheel: zoom in or out. (Mod D Badke)
    //this.wheelPlus = new Array(oKaMap, oKaMap.zoomOut, null);
    //this.wheelMinus = new Array(oKaMap, oKaMap.zoomIn, null);
    
    this.mapViewer.registerTool( this );
};

/**
 * is this an info tool or a regular tool?
 */
kaTool.prototype.isInfoTool = function() {
    return this.bInfoTool;
};

/**
 * activate this tool.  Activating the tool causes any existing tools to be
 * deactivated.
 */
kaTool.prototype.activate = function() {
    this.mapViewer.activateTool( this );
    document.kaCurrentTool = this;
};

/**
 * deactivate this tool.  
 */
kaTool.prototype.deactivate = function() {
    this.mapViewer.deactivateTool( this );
    document.kaCurrentTool = null;
};

/**
 * handle mouse movement over the viewport of the mapViewer.  This
  * method does nothing and should be overloaded by subclasses.
 * @param {Event} e the mouse event object
 */
kaTool.prototype.onmousemove = function(e) {
    return false;
};

/**
 * handle mouse down events on the viewport of the mapViewer.  This
  * method does nothing and should be overloaded by subclasses.
 * @param {Event} e the mouse event object
 */
kaTool.prototype.onmousedown = function(e) {
    return false;
};

/**
 * handle mouse up events on the viewport of the mapViewer.  This
  * method does nothing and should be overloaded by subclasses.
 * @param {Event} e the mouse event object
 */
kaTool.prototype.onmouseup = function(e) {
    return false;
};

/**
 * handle mouse doubleclicks on the viewport of the mapViewer.  This
 * method does nothing and should be overloaded by subclasses.
 * @param {Event} e the mouse event object
 */
kaTool.prototype.ondblclick = function(e) {
    return false;
};

/**
 * Set the object and function to call when the mouse wheel
 * event triggers. The parameters are saved in the tool object
 * this is called for, and only affect that tool.
 * 
 * @param {array} plusSet  - parameters for positive wheel scroll
 * @param {array} minusSet - parameters for negative wheel scroll
 * 
 * plusSet and minusSet are arrays of structure:
 *   [0] - object to apply function for or null
 *   [1] - function to apply
 *   [2] - function arguments or null for no arguments
 * 
 * Set minusSet and/or plusSet to null to disable that scroll direction.
 * 
 * eg: --disable mouse wheel for navigator tool:
 * 				 myKaNavigator.setMouseWheel(null, null);
 * 
 *     --set navigator tool to zoom (same as default action):
 *         myKaNavigator.setMouseWheel([myKaMap, myKaMap.zoomIn, null],
 * 					 											     [myKaMap, myKaMap.zoomOut, null]);
 * 
 * (New D Badke)
 */
kaTool.prototype.setMouseWheel = function(minusSet, plusSet) {
	this.wheelMinus = minusSet;
	this.wheelPlus = plusSet;
};

/**
 * Handle mouse wheel events over the viewport of the mapViewer.  This
 * applies a function set by the setMouseWheel function. If the function
 * is null, do nothing. (Mod D Badke)
 * 
 * @param {Event} e the mouse event object
 */
kaTool.prototype.onmousewheel = function(e) {
    e = (e)?e:((event)?event:null);
    var wheelDelta = e.wheelDelta ? e.wheelDelta : e.detail*-1;
    var wheelSet = null;
    if (wheelDelta > 0) 
    	wheelSet = this.wheelPlus;
    else
    	wheelSet = this.wheelMinus;
   
  	if (wheelSet) {
  		obj = (wheelSet[0]) ? wheelSet[0] : null;
  		func = (wheelSet[1]) ? wheelSet[1] : null;
  		args = (wheelSet[2]) ? wheelSet[2] : null;
  		if (func) {
			if (args) {
				func.apply(obj, args);
			} else{
			    func.apply(obj);
			}
  		}
    }
};

/**
 * adjust a page-relative pixel position into a mapViewer relative
 * pixel position
 *
 * @param {Integer} x the x page coordinate to convert
 * @param {Integer} y the y page coordinate to convert
 * @return {Array} return an array containing the converted coordinates
 */
kaTool.prototype.adjustPixPosition = function( x, y ) {
    var obj = this.mapViewer.domObj;
    var offsetLeft = 0;
    var offsetTop = 0;
    while (obj) {
        offsetLeft += parseInt(obj.offsetLeft);
        offsetTop += parseInt(obj.offsetTop);
        obj = obj.offsetParent;
    }
    
    var pX = parseInt(this.mapViewer.theInsideLayer.style.left) + 
             offsetLeft - this.mapViewer.xOrigin - x;
    var pY = parseInt(this.mapViewer.theInsideLayer.style.top) + 
             offsetTop - this.mapViewer.yOrigin - y;
             
    return [pX,pY];
};

/*
 * key press events are directed to the HTMLDocument rather than the
 * div on which we really wanted them to happen.  So we set the document
 * keypress handler to this function and redirect it to the mapViewer core
 * keypress handler, which will eventually reach the onkeypress handler
 * of our current tool ... which by default is the keyboard navigation.
 *
 * To get the keyboard events in the first place, add the following when you
 * want the keypress events to be captured
 *
 * if (isIE4) document.onkeydown = kaTool_redirect_onkeypress;
 * document.onkeypress = kaTool_redirect_onkeypress;
 */
function kaTool_redirect_onkeypress(e) {
    if (document.kaCurrentTool) {
        document.kaCurrentTool.onkeypress(e);
    }
};

/**
 * handle keypress events.  Keypress events are normally dispatched
 * here rather than in a sub-class.
 * @param {Event} e the keypress event object
 */
kaTool.prototype.onkeypress = function(e) {
    e = (e)?e:((event)?event:null);
    if (e) {
        var charCode=(e.charCode)?e.charCode:e.keyCode;
        var b=true;
        var nStep = 16;
        switch(charCode) {
          case 38://up
            this.mapViewer.moveBy(0,nStep);
            this.mapViewer.triggerEvent( EV_EXTENTS_CHANGED, this.mapViewer.getGeoExtents() );
       		this.mapViewer.triggerEvent( EV_MAP_PANNED);
            break;
          case 40:
            this.mapViewer.moveBy(0,-nStep);
            this.mapViewer.triggerEvent( EV_EXTENTS_CHANGED, this.mapViewer.getGeoExtents() );
       		this.mapViewer.triggerEvent( EV_MAP_PANNED);
            break;
          case 37:
            this.mapViewer.moveBy(nStep,0);
            this.mapViewer.triggerEvent( EV_EXTENTS_CHANGED, this.mapViewer.getGeoExtents() );
       		this.mapViewer.triggerEvent( EV_MAP_PANNED);
            break;
          case 39:
            this.mapViewer.moveBy(-nStep,0);
            this.mapViewer.triggerEvent( EV_EXTENTS_CHANGED, this.mapViewer.getGeoExtents() );
       		this.mapViewer.triggerEvent( EV_MAP_PANNED);
            break;
          case 33:
            this.mapViewer.slideBy(0, this.mapViewer.viewportHeight/2);
       		this.mapViewer.triggerEvent( EV_MAP_PANNED);
            break;
          case 34:
            this.mapViewer.slideBy(0,-this.mapViewer.viewportHeight/2);
       		this.mapViewer.triggerEvent( EV_MAP_PANNED);
            break;
          case 36:
            this.mapViewer.slideBy(this.mapViewer.viewportWidth/2,0);
       		this.mapViewer.triggerEvent( EV_MAP_PANNED);
            break;
          case 35:
            this.mapViewer.slideBy(-this.mapViewer.viewportWidth/2,0);
       		this.mapViewer.triggerEvent( EV_MAP_PANNED);
            break;
          case 43: //ascii +
          case 61: //ascii =
            this.mapViewer.zoomIn();
       		this.mapViewer.triggerEvent( EV_MAP_ZOOMED);
            break;
         case 45:
            this.mapViewer.zoomOut();
       		this.mapViewer.triggerEvent( EV_MAP_ZOOMED);
            break;
          default:
            b=false;
        }
        if (b) {
            return this.cancelEvent(e);
        }
        return true;
    }
};

/**
 * handle the mouse moving over the mapViewer viewport.  This is a method does
 * nothing and should be overloaded in a subclass
 * @param {Event} e the mouse event
 */
kaTool.prototype.onmouseover = function(e) {
    return false;
};

/**
 * handle the mouse leaving the mapViewer viewport.  This is a method
 * releases the keypress handler and should be called from any
 * sub class that overloads this method.
 * @param {Event} e the mouse event
 */
kaTool.prototype.onmouseout = function(e) {
    if (this.mapViewer.isIE4) {
        document.onkeydown = null;
    }
    document.onkeypress = null;
    return false;
};

/**
 * provide a cross-platform method of cancelling events, including
 * stopping of event bubbling and propagation.
 * @param {Event} e the event to cancel
 */
kaTool.prototype.cancelEvent = function(e) {
    e = (e)?e:((event)?event:null);
    e.cancelBubble = true;
    e.returnValue = false;
    if (e.stopPropogation) {
        e.stopPropogation();
    }
    if (e.preventDefault) {
        e.preventDefault();
    }
    return false;
};

/**
 * Construct a new kaNavigator instance on a given mapViewer instance
 *
 * @class
 * kaNavigator is a general purpose navigation tool for mapViewer instances.
 * It provides panning through click-and-drag, and various keyboard
 * navigation.
 *
 * @base kaTool
 * @constructor
 * @param {Object} oKaMap the mapViewer instance to provide navigation for
 * @author Paul Spencer
 */
function kaNavigator( oKaMap ) {
    kaTool.apply( this, [oKaMap] );
    /** the name of this tool */
    this.name = 'kaNavigator';
    /** the cursor to use when the map is not being dragged */
	this.cursorNormal = ["url('"+this.mapViewer.mapVieverUrl+"images/grab.cur'),move", '-moz-grab', 'grab', 'move'];
	/** the cursor to use when the map is being dragged */
	this.cursorDrag = ["url('"+this.mapViewer.mapVieverUrl+"images/grabbing.cur'),move", '-moz-grabbing', 'grabbing', 'move'];
	/** the cursor to use over the map (by default) */
    this.cursor = this.cursorNormal;

    /** the image to use for this tool when it is active on the map */
    this.activeImage = this.mapViewer.server + 'images/button_pan_3.png';

    /** the image to use for this tool when it is disabled */
    this.disabledImage = this.mapViewer.server + 'images/button_pan_2.png';
    
    /** track the last x position of the mouse for panning */
    this.lastx = null;
    /** track the last y position of the mouse for panning */
    this.lasty = null;
    /** track whether the mouse is down or up for panning support */
    this.bMouseDown = false;
    
    for (var p in kaTool.prototype) {
        if (!kaNavigator.prototype[p])
            kaNavigator.prototype[p]= kaTool.prototype[p];
    }
};

/**
 * handle the mouse leaving the viewport.  If the mouse is down, stop
 * dragging.
 * @param {Event} e the mouse event object
 */
kaNavigator.prototype.onmouseout = function(e) {
	//this.bMouseDown = false;
	e = (e)?e:((event)?event:null);
    if (!e.target) e.target = e.srcElement;
    if (e.target.id == this.mapViewer.domObj.id) {
        this.bMouseDown = false;
		
		this.cursor = this.cursorNormal;
		this.mapViewer.setCursor(this.cursorNormal);
        
        return kaTool.prototype.onmouseout.apply(this, [e]);
    }
};

/**
 * handle the mouse moving inside viewport.  If the mouse is down, move
 * the map.
 * @param {Event} e the mouse event object
 */
kaNavigator.prototype.onmousemove = function(e) {
    e = (e)?e:((event)?event:null);
    
    if (!this.bMouseDown) {
        return false;
    }
    
    if (!this.mapViewer.layersHidden) {
        this.mapViewer.hideLayers();
    }
    var newTop = safeParseInt(this.mapViewer.theInsideLayer.style.top);
    var newLeft = safeParseInt(this.mapViewer.theInsideLayer.style.left);

    var x = e.pageX || (e.clientX +
          (document.documentElement.scrollLeft || document.body.scrollLeft));
    var y = e.pageY || (e.clientY +
                (document.documentElement.scrollTop || document.body.scrollTop));
    newTop = newTop - this.lasty + y;
    newLeft = newLeft - this.lastx + x;

    this.mapViewer.theInsideLayer.style.top=newTop + 'px';
    this.mapViewer.theInsideLayer.style.left=newLeft + 'px';
	
    this.mapViewer.checkWrap.apply(this.mapViewer, []);

    this.lastx=x;
    this.lasty=y;
    return false;
};

/**
 * handle mouse down events.  Start tracking mouse movement for panning.
 * @param {Event} e the mouse event object
 */
kaNavigator.prototype.onmousedown = function(e) {
    e = (e)?e:((event)?event:null);
    if (e.button==2) {
        return this.cancelEvent(e);
    } else {
		this.cursor = this.cursorDrag;
    	this.mapViewer.setCursor(this.cursorDrag);
        if (this.mapViewer.isIE4) {
            document.onkeydown = kaTool_redirect_onkeypress;
            this.mapViewer.theInsideLayer.setCapture();
        }
        document.onkeypress = kaTool_redirect_onkeypress;
        
        this.bMouseDown=true;
        var x = e.pageX || (e.clientX +
             (document.documentElement.scrollLeft || document.body.scrollLeft));
        var y = e.pageY || (e.clientY +
             (document.documentElement.scrollTop || document.body.scrollTop));
        this.lastx=x;
        this.lasty=y;
        this.startx = this.lastx;
        this.starty = this.lasty;
        
        e.cancelBubble = true;
        e.returnValue = false;
        if (e.stopPropogation) e.stopPropogation();
        if (e.preventDefault) e.preventDefault();
        return false;
    }
};

var gDblClickTimer = null;
/**
 * handle the mouse up events.  Stop tracking mouse movement.
 * @param {Event} e the mouse event object
 */
kaNavigator.prototype.onmouseup = function(e) {
	this.cursor = this.cursorNormal;
    this.mapViewer.setCursor(this.cursorNormal);
	
    e = (e)?e:((event)?event:null);
    this.bMouseDown=false;
    var x = e.pageX || (e.clientX +
        (document.documentElement.scrollLeft || document.body.scrollLeft));
    var y = e.pageY || (e.clientY +
        (document.documentElement.scrollTop || document.body.scrollTop));
    if (Math.abs(x-this.startx) < 2 &&
        Math.abs(y-this.starty) < 2) {
        if (!gDblClickTimer) {
            gDblClickTimer = window.setTimeout(bind(this.dispatchMapClicked, this, x, y), 250);
        }
    } else {
        gDblClickTimer = null;
        this.mapViewer.showLayers();
        this.mapViewer.triggerEvent(EV_EXTENTS_CHANGED, this.mapViewer.getGeoExtents());        
  		this.mapViewer.triggerEvent( EV_MAP_PANNED);
    }
    
    if (this.mapViewer.isIE4) {
        this.mapViewer.theInsideLayer.releaseCapture();
    }
    
    return false;
};


kaNavigator.prototype.dispatchMapClicked = function(px,py) {
    var p = this.mapViewer.pixToGeo( px,py );
    this.mapViewer.triggerEvent(EV_MAP_CLICKED, p);
};

/**
 * handle a double-click event by sliding the map to the point that was clicked.
 * @param {Event} e the mouse event object
 */
kaNavigator.prototype.ondblclick = function(evt) 
{
    evt = (evt) ? evt : ((event) ? event : null);
    var elem = (evt.target) ? evt.target : ((evt.srcElement) ? evt.srcElement : null);
    if (elem.nodeType == 3) {
       elem = elem.parentNode;
    }
    
    var mousePosPx = getPositionedEventCoords(evt, elem);
    
    var targetElmPos = getRelativeElementPosition(elem, this.mapViewer.domObj);
   
    var dx = safeParseInt(mousePosPx[0] + targetElmPos[0] - this.mapViewer.viewportWidth / 2);
    var dy = safeParseInt(mousePosPx[1] + targetElmPos[1] - this.mapViewer.viewportHeight / 2);
    
    // test 
    /*var center = this.mapViewer.getCenter();
    var zeropos = this.mapViewer.pixToGeo(0, 0);
    var mousepos = this.mapViewer.pixToGeo(mousePosPx[0], mousePosPx[1]);
    var centerpos = this.mapViewer.pixToGeo(this.mapViewer.viewportWidth / 2, this.mapViewer.viewportHeight / 2);*/
    //this.cellSize

    var dx = safeParseInt(mousePosPx[0] + targetElmPos[0] - this.mapViewer.viewportWidth / 2);
    var dy = safeParseInt(mousePosPx[1] + targetElmPos[1] - this.mapViewer.viewportHeight / 2);

    // geo position of mouse and raising event
    var center = this.mapViewer.getCenter();
    var mousepos = this.mapViewer.pixToGeo(center[0]+dx,center[1]+dy);    
    var scl = this.mapViewer.getCurrentScale();
    this.mapViewer.triggerEvent(EV_MAP_DOUBLECLICKED, mousepos[0], mousepos[1], scl);
    
    // shift map 
    this.mapViewer.slideBy(-dx, -dy);
    // todo - slide calls pan by setTimeout - pan event can not be raised here
};


function getPositionedEventCoords(evt, evtTarget) 
{
    var coords = [0,0];
    if (evt.offsetX) {
        coords[0] = evt.offsetX;
        coords[1] = evt.offsetY;
    }
    else if (evt.layerX) {
        var borders = {left:parseInt(getElementStyle(evtTarget, 
                       "borderLeftWidth", "border-left-width")),
                       top:parseInt(getElementStyle(evtTarget, 
                       "borderTopWidth", "border-top-width"))};
        coords[0] = evt.layerX - borders.left;
        coords[1] = evt.layerY - borders.top;
    } 
    return coords;
};

// returns position in px of element relative to its container element referncialElement. 
// If referncialElement is not container of element, returns position relative to document.
function getRelativeElementPosition(element, referncialElement) {
    var offsetTrail = element;
    var offsetLeft = 0;
    var offsetTop = 0;
    while (offsetTrail != null && offsetTrail!=referncialElement) {
        offsetLeft += offsetTrail.offsetLeft;
        offsetTop += offsetTrail.offsetTop;
        offsetTrail = offsetTrail.offsetParent;
    }
    if (navigator.userAgent.indexOf("Mac") != -1 && 
        typeof document.body.leftMargin != "undefined") {
        offsetLeft += document.body.leftMargin;
        offsetTop += document.body.topMargin;
    }
    return [offsetLeft, offsetTop];
}


   
function getElementStyle(elem, IEStyleAttr, CSSStyleAttr) {
    if (elem.currentStyle) {
        return elem.currentStyle[IEStyleAttr];
    } else if (window.getComputedStyle) {
        var compStyle = window.getComputedStyle(elem, "");
        return compStyle.getPropertyValue(CSSStyleAttr);
    }
    return "";
};




//TODO: this is a temporary patch until we add prototype/scriptaculous support.
function bind(m,o) {
    var __method = arguments[0];
    var __object = arguments[1];
    var args = [];
    for (var i=2; i<arguments.length; i++) { args.push(arguments[i]) }
    return function() { return __method.apply(__object, args); }
}
