/*
Scripts for creating SVG apps, converting clientX/Y to viewBox coordinates
and for displaying tooltips

Copyright (C) <2002-2007>  <Andreas Neumann>
Version 1.2.2, 2007-04-03
neumann@karto.baug.ethz.ch
http://www.carto.net/
http://www.carto.net/neumann/

Credits:
* thanks to Kevin Lindsey for his many examples and providing the ViewBox class

----

Documentation: http://www.carto.net/papers/svg/gui/mapApp/

----

current version: 1.2.2

version history:
1.0 (2006-06-01)
initial version
Was programmed earlier, but now documented

1.1 (2006-06-15)
added properties this.innerWidth, this.innerHeight (wrapper around different behaviour of viewers), added method ".adjustViewBox()" to adjust the viewBox to the this.innerWidth and this.innerHeight of the UA's window

1.2 (2006-10-06)
added two new constructor parameter "adjustVBonWindowResize" and "resizeCallbackFunction". If the first parameter is set to true, the viewBox of this mapApp will always adjust itself to the innerWidth and innerHeight of the browser window or frame containing the SVG application
the "resizeCallbackFunction" can be of type "function", later potentially also of type "object". This function is called every time the mapApp was resized (browser/UA window was resized). It isn't called the first time when the mapApp was initialized
added a new way to detect resize events in Firefox which didn't implement the SVGResize event so far
added several arrays to hold GUI references

1.2.1 (2007-01-09)
fixed an issue in the method .updateTooltip() for cases where an element did not have a tooltip text, attribute or an empty string. Now, the tooltip disappears for these elements instead of triggering a javascript error.

1.2.2 (2007-04-03)
improved the navigator detection to correctly handle Webkit/Safari
added this.htmlAreas and this.tables as new arrays to hold GUI component references

-------


This ECMA script library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library (lesser_gpl.txt); if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

----

original document site: http://www.carto.net/papers/svg/gui/mapApp/
Please contact the author in case you want to use code or ideas commercially.
If you use this code, please include this copyright header, the included full
LGPL 2.1 text and read the terms provided in the LGPL 2.1 license
(http://www.gnu.org/copyleft/lesser.txt)

-------------------------------

Please report bugs and send improvements to neumann@karto.baug.ethz.ch
If you use this code, please link to the original (http://www.carto.net/papers/svg/gui/mapApp/)
somewhere in the source-code-comment or the "about" of your project and give credits, thanks!

*/

//this mapApp object helps to convert clientX/clientY coordinates to the coordinates of the group where the element is within
//normally one can just use .getScreenCTM(), but ASV3 does not implement it, 95% of the code in this function is for ASV3!!!
//credits: Kevin Lindsey for his example at http://www.kevlindev.com/gui/utilities/viewbox/ViewBox.js
function mapApp(adjustVBonWindowResize,resizeCallbackFunction) {
	this.adjustVBonWindowResize = adjustVBonWindowResize;
	this.resizeCallbackFunction = resizeCallbackFunction;
	this.initialized = false;
	if (!document.documentElement.getScreenCTM) {
		//add zoom and pan event event to document element
		//this part is only required for viewers not supporting document.documentElement.getScreenCTM() (e.g. ASV3)
		document.documentElement.addEventListener("SVGScroll",this,false);
		document.documentElement.addEventListener("SVGZoom",this,false);
	}
	//add SVGResize event, note that because FF does not yet support the SVGResize event, there is a workaround
 	try {
  		//browsers with native SVG support
  		window.addEventListener("resize",this,false);
 	}
	catch(er) {
		//SVG UAs, like Batik and ASV/Iex
		document.documentElement.addEventListener("SVGResize",this,false);
	}
	//determine the browser main version
	this.navigator = "Batik";
	if (window.navigator) {
		if (window.navigator.appName.match(/Adobe/gi)) {
			this.navigator = "Adobe";
		}
		if (window.navigator.appName.match(/Netscape/gi)) {
			this.navigator = "Mozilla";
		}
		if (window.navigator.userAgent) {
			if (window.navigator.userAgent.match(/Opera/gi)) {
				this.navigator = "Opera";
			}
			if (window.navigator.userAgent.match(/AppleWebKit/gi) || window.navigator.userAgent.match(/Safari/gi) ) {
				this.navigator = "Safari";
			}
		}
	}
	//we need to call this once to initialize this.innerWidth/this.innerHeight
	this.resetFactors();
	//per default, tooltips are disabled
	this.tooltipsEnabled = false;
	//create new arrays to hold GUI references
	this.Windows = new Array();
	this.checkBoxes = new Array();
	this.radioButtonGroups = new Array();
	this.tabgroups = new Array();
	this.textboxes = new Array();
	this.buttons = new Array();	
	this.selectionLists = new Array();	
	this.comboboxes = new Array();	
	this.sliders = new Array();
	this.scrollbars = new Array();
	this.colourPickers = new Array();
	this.htmlAreas = new Array();
	this.tables = new Array();
}

mapApp.prototype.handleEvent = function(evt) {
	if (evt.type == "SVGResize" || evt.type == "resize" || evt.type == "SVGScroll" || evt.type == "SVGZoom") {
		this.resetFactors();
	}
	if ((evt.type == "mouseover" || evt.type == "mouseout" || evt.type == "mousemove") && this.tooltipsEnabled) {
		this.displayTooltip(evt);
	}
}

mapApp.prototype.resetFactors = function() {
	//set inner width and height
	if (window.innerWidth) {
		this.innerWidth = window.innerWidth;
		this.innerHeight = window.innerHeight;
	}
	else {
		var viewPort = document.documentElement.viewport;
		this.innerWidth = viewPort.width;
		this.innerHeight = viewPort.height;
	}
	if (this.adjustVBonWindowResize) {
		this.adjustViewBox();
	}
	//this code is for ASV3
	if (!document.documentElement.getScreenCTM) {
		var svgroot = document.documentElement;
		this.viewBox = new ViewBox(svgroot);
		var trans = svgroot.currentTranslate;
		var scale = svgroot.currentScale;
		this.m = this.viewBox.getTM();
		//undo effects of zoom and pan
		this.m = this.m.scale( 1/scale );
		this.m = this.m.translate(-trans.x, -trans.y);
	}
	if (this.resizeCallbackFunction && this.initialized) {
		if (typeof(this.resizeCallbackFunction) == "function") {
			this.resizeCallbackFunction();
		}
	}
	this.initialized = true;
}

//set viewBox of document.documentElement to innerWidth and innerHeight
mapApp.prototype.adjustViewBox = function() {
	document.documentElement.setAttributeNS(null,"viewBox","0 0 "+this.innerWidth+" "+this.innerHeight);
}

mapApp.prototype.calcCoord = function(evt,ctmNode) {
	var svgPoint = document.documentElement.createSVGPoint();
	svgPoint.x = evt.clientX;
	svgPoint.y = evt.clientY;
	if (!document.documentElement.getScreenCTM) {
		//undo the effect of transformations
		if (ctmNode) {
			var matrix = getTransformToRootElement(ctmNode);
		}
		else {
			var matrix = getTransformToRootElement(evt.target);			
		}
  		svgPoint = svgPoint.matrixTransform(matrix.inverse().multiply(this.m));
	}
	else {
		//case getScreenCTM is available
		if (ctmNode) {
			var matrix = ctmNode.getScreenCTM();
		}
		else {
			var matrix = evt.target.getScreenCTM();		
		}
  	svgPoint = svgPoint.matrixTransform(matrix.inverse());
	}
  //undo the effect of viewBox and zoomin/scroll
	return svgPoint;
}

mapApp.prototype.calcInvCoord = function(svgPoint) {
	if (!document.documentElement.getScreenCTM) {
		var matrix = getTransformToRootElement(document.documentElement);
	}
	else {
		var matrix = document.documentElement.getScreenCTM();
	}
	svgPoint = svgPoint.matrixTransform(matrix);
	return svgPoint;
}

//initialize tootlips
mapApp.prototype.initTooltips = function(groupId,tooltipTextAttribs,tooltipRectAttribs,xOffset,yOffset,padding) {
	var nrArguments = 6;
	if (arguments.length == nrArguments) {
		this.toolTipGroup = document.getElementById(groupId);
		this.tooltipTextAttribs = tooltipTextAttribs;
		if (!this.tooltipTextAttribs["font-size"]) {
			this.tooltipTextAttribs["font-size"] = 12;
		}	
		this.tooltipRectAttribs = tooltipRectAttribs;
		this.xOffset = xOffset;
		this.yOffset = yOffset;
		this.padding = padding;
		if (!this.toolTipGroup) {
			alert("Error: could not find tooltip group with id '"+groupId+"'. Please specify a correct tooltip parent group id!");
		}
		else {
			//set tooltip group to invisible
			this.toolTipGroup.setAttributeNS(null,"visibility","hidden");
			this.toolTipGroup.setAttributeNS(null,"pointer-events","none");
			this.tooltipsEnabled = true;
			//create tooltip text element
			this.tooltipText = document.createElementNS(svgNS,"text");
			for (var attrib in this.tooltipTextAttribs) {
				value = this.tooltipTextAttribs[attrib];
				if (attrib == "font-size") {
					value += "px";
				}
				this.tooltipText.setAttributeNS(null,attrib,value);
			}
			//create textnode
			var textNode = document.createTextNode("Tooltip");
			this.tooltipText.appendChild(textNode);
			this.toolTipGroup.appendChild(this.tooltipText);
			var bbox = this.tooltipText.getBBox();
			this.tooltipRect = document.createElementNS(svgNS,"rect");
			this.tooltipRect.setAttributeNS(null,"x",bbox.x-this.padding);
			this.tooltipRect.setAttributeNS(null,"y",bbox.y-this.padding);
			this.tooltipRect.setAttributeNS(null,"width",bbox.width+this.padding*2);
			this.tooltipRect.setAttributeNS(null,"height",bbox.height+this.padding*2);
			for (var attrib in this.tooltipRectAttribs) {
				this.tooltipRect.setAttributeNS(null,attrib,this.tooltipRectAttribs[attrib]);
			}
			this.toolTipGroup.insertBefore(this.tooltipRect,this.tooltipText);
		}
	}
	else {
			alert("Error in method 'initTooltips': wrong nr of arguments! You have to pass over "+nrArguments+" parameters.");			
	}
}

mapApp.prototype.addTooltip = function(tooltipNode,tooltipTextvalue,followmouse,checkForUpdates,targetOrCurrentTarget,childAttrib) {
	var nrArguments = 6;
	if (arguments.length == nrArguments) {
		//get reference
		if (typeof(tooltipNode) == "string") {
			tooltipNode = document.getElementById(tooltipNode);
		}
		//check if tooltip attribute present or create one
		if (!tooltipNode.hasAttributeNS(attribNS,"tooltip")) {
			if (tooltipTextvalue) {
				tooltipNode.setAttributeNS(attribNS,"tooltip",tooltipTextvalue);
			}
			else {
				tooltipNode.setAttributeNS(attribNS,"tooltip","Tooltip");			
			}
		}
		//see if we need updates
		if (checkForUpdates) {
			tooltipNode.setAttributeNS(attribNS,"tooltipUpdates","true");		
		}
		//see if we have to use evt.target
		if (targetOrCurrentTarget == "target") {
			tooltipNode.setAttributeNS(attribNS,"tooltipParent","true");
		}
		//add childAttrib
		if (childAttrib) {
			tooltipNode.setAttributeNS(attribNS,"tooltipAttrib",childAttrib);
		}
		//add event listeners
		tooltipNode.addEventListener("mouseover",this,false);
		tooltipNode.addEventListener("mouseout",this,false);
		if (followmouse) {
			tooltipNode.addEventListener("mousemove",this,false);
		}
	}
	else {
		alert("Error in method 'addTooltip()': wrong nr of arguments! You have to pass over "+nrArguments+" parameters.");			
	}
}

mapApp.prototype.displayTooltip = function(evt) {
	var curEl = evt.currentTarget;
	var coords = this.calcCoord(evt,this.toolTipGroup.parentNode);
	if (evt.type == "mouseover") {
		this.toolTipGroup.setAttributeNS(null,"visibility","visible");
		this.toolTipGroup.setAttributeNS(null,"transform","translate("+(coords.x+this.xOffset)+","+(coords.y+this.yOffset)+")");
		this.updateTooltip(evt);
	}
	if (evt.type == "mouseout") {
		this.toolTipGroup.setAttributeNS(null,"visibility","hidden");
	}
	if (evt.type == "mousemove") {
		this.toolTipGroup.setAttributeNS(null,"transform","translate("+(coords.x+this.xOffset)+","+(coords.y+this.yOffset)+")");		
		if (curEl.hasAttributeNS(attribNS,"tooltipUpdates")) {
			this.updateTooltip(evt);
		}
	}
}

mapApp.prototype.updateTooltip = function(evt) {
	var el = evt.currentTarget;
	if (el.hasAttributeNS(attribNS,"tooltipParent")) {
		var attribName = "tooltip";
		if (el.hasAttributeNS(attribNS,"tooltipAttrib")) {
			attribName = el.getAttributeNS(attribNS,"tooltipAttrib");
		}
		el = evt.target;
		var myText = el.getAttributeNS(attribNS,attribName);
	}
	else {
		var myText = el.getAttributeNS(attribNS,"tooltip");
	}
	if (myText) {
		var textArray = myText.split("\\n");
		while(this.tooltipText.hasChildNodes()) {
			this.tooltipText.removeChild(this.tooltipText.lastChild);
		}
		for (var i=0;i<textArray.length;i++) {
			var tspanEl = document.createElementNS(svgNS,"tspan");
			tspanEl.setAttributeNS(null,"x",0);
			var dy = this.tooltipTextAttribs["font-size"];
			if (i == 0) {
				var dy = 0;
			}
			tspanEl.setAttributeNS(null,"dy",dy);
			var textNode = document.createTextNode(textArray[i]);
			tspanEl.appendChild(textNode);
			this.tooltipText.appendChild(tspanEl);
		}
		// set text and rect attributes
		var bbox = this.tooltipText.getBBox();
		this.tooltipRect.setAttributeNS(null,"x",bbox.x-this.padding);
		this.tooltipRect.setAttributeNS(null,"y",bbox.y-this.padding);
		this.tooltipRect.setAttributeNS(null,"width",bbox.width+this.padding*2);
		this.tooltipRect.setAttributeNS(null,"height",bbox.height+this.padding*2);	
	}
	else {
		this.toolTipGroup.setAttributeNS(null,"visibility","hidden");	
	}
}

mapApp.prototype.enableTooltips = function() {
	this.tooltipsEnabled = true;
}

mapApp.prototype.disableTooltips = function() {
	this.tooltipsEnabled = false;
	this.toolTipGroup.setAttributeNS(null,"visibility","hidden");
}

/*************************************************************************/

/*****
*
*   ViewBox.js
*
*   copyright 2002, Kevin Lindsey
*
*****/

ViewBox.VERSION = "1.0";


/*****
*
*   constructor
*
*****/
function ViewBox(svgNode) {
    if ( arguments.length > 0 ) {
        this.init(svgNode);
    }
}


/*****
*
*   init
*
*****/
ViewBox.prototype.init = function(svgNode) {
    var viewBox = svgNode.getAttributeNS(null, "viewBox");
    var preserveAspectRatio = svgNode.getAttributeNS(null, "preserveAspectRatio");
    
    if ( viewBox != "" ) {
        var params = viewBox.split(/\s*,\s*|\s+/);

        this.x      = parseFloat( params[0] );
        this.y      = parseFloat( params[1] );
        this.width  = parseFloat( params[2] );
        this.height = parseFloat( params[3] );
    } else {
        this.x      = 0;
        this.y      = 0;
        this.width  = innerWidth;
        this.height = innerHeight;
    }
    
    this.setPAR(preserveAspectRatio);
    var dummy = this.getTM(); //to initialize this.windowWidth/this.windowHeight
};


/*****
*
*   getTM
*
*****/
ViewBox.prototype.getTM = function() {
    var svgRoot      = document.documentElement;
    var matrix       = document.documentElement.createSVGMatrix();
		//case width/height contains percent
    this.windowWidth = svgRoot.getAttributeNS(null,"width");
    if (this.windowWidth.match(/%/) || this.windowWidth == null) {
    	if (this.windowWidth == null) {
    		if (window.innerWidth) {
    			this.windowWidth = window.innerWidth;
    		}
    		else {
    			this.windowWidth = svgRoot.viewport.width;
    		}
    	}
    	else {
    		var factor = parseFloat(this.windowWidth.replace(/%/,""))/100;
    		if (window.innerWidth) {
    			this.windowWidth = window.innerWidth * factor;
    		}
    		else {
    			this.windowWidth = svgRoot.viewport.width * factor;
    		}
    	}
    }
    else {
    	this.windowWidth = parseFloat(this.windowWidth);
    }
    this.windowHeight = svgRoot.getAttributeNS(null,"height");
    if (this.windowHeight.match(/%/) || this.windowHeight == null) {
    	if (this.windowHeight == null) {
    		if (window.innerHeight) {
    			this.windowHeight = window.innerHeight;
    		}
    		else {
    			this.windowHeight = svgRoot.viewport.height;
    		}
    	}
    	else {
    		var factor = parseFloat(this.windowHeight.replace(/%/,""))/100;
    		if (window.innerHeight) {
    			this.windowHeight = window.innerHeight * factor;
    		}
    		else {
    			this.windowHeight = svgRoot.viewport.height * factor;
    		}
    	}
    }
    else {
    	this.windowHeight = parseFloat(this.windowHeight);
    }
    var x_ratio = this.width  / this.windowWidth;
    var y_ratio = this.height / this.windowHeight;

    matrix = matrix.translate(this.x, this.y);
    if ( this.alignX == "none" ) {
        matrix = matrix.scaleNonUniform( x_ratio, y_ratio );
    } else {
        if ( x_ratio < y_ratio && this.meetOrSlice == "meet" ||
             x_ratio > y_ratio && this.meetOrSlice == "slice"   )
        {
            var x_trans = 0;
            var x_diff  = this.windowWidth*y_ratio - this.width;

            if ( this.alignX == "Mid" )
                x_trans = -x_diff/2;
            else if ( this.alignX == "Max" )
                x_trans = -x_diff;
            
            matrix = matrix.translate(x_trans, 0);
            matrix = matrix.scale( y_ratio );
        }
        else if ( x_ratio > y_ratio && this.meetOrSlice == "meet" ||
                  x_ratio < y_ratio && this.meetOrSlice == "slice"   )
        {
            var y_trans = 0;
            var y_diff  = this.windowHeight*x_ratio - this.height;

            if ( this.alignY == "Mid" )
                y_trans = -y_diff/2;
            else if ( this.alignY == "Max" )
                y_trans = -y_diff;
            
            matrix = matrix.translate(0, y_trans);
            matrix = matrix.scale( x_ratio );
        }
        else
        {
            // x_ratio == y_ratio so, there is no need to translate
            // We can scale by either value
            matrix = matrix.scale( x_ratio );
        }
    }

    return matrix;
}


/*****
*
*   get/set methods
*
*****/

/*****
*
*   setPAR
*
*****/
ViewBox.prototype.setPAR = function(PAR) {
    // NOTE: This function needs to use default values when encountering
    // unrecognized values
    if ( PAR ) {
        var params = PAR.split(/\s+/);
        var align  = params[0];

        if ( align == "none" ) {
            this.alignX = "none";
            this.alignY = "none";
        } else {
            this.alignX = align.substring(1,4);
            this.alignY = align.substring(5,9);
        }

        if ( params.length == 2 ) {
            this.meetOrSlice = params[1];
        } else {
            this.meetOrSlice = "meet";
        }
    } else {
        this.align  = "xMidYMid";
        this.alignX = "Mid";
        this.alignY = "Mid";
        this.meetOrSlice = "meet";
    }
};