/* Scripts for creating SVG apps, converting clientX/Y to viewBox coordinates and for displaying tooltips Copyright (C) <2002-2007> 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 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"; } };