blob: ccdaef2f812e4a47edbc5e78f0663532d4439776 [file] [log] [blame]
/*
Scripts for creating SVG apps, converting clientX/Y to viewBox coordinates
and for displaying tooltips
Copyright (C) <2006> <Andreas Neumann>
Version 1.2, 2006-10-06
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
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
-------
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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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.appName.match(/Opera/gi)) {
this.navigator = "Opera";
}
if (window.navigator.appName.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();
}
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");
}
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);
}
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";
}
};