azkaban-memoizeit

Details

build.xml 76(+54 -22)

diff --git a/build.xml b/build.xml
index c695082..9ccb34e 100644
--- a/build.xml
+++ b/build.xml
@@ -6,6 +6,7 @@
 	<property name="dist.jar.dir" value="${basedir}/dist/jars" />
 	<property name="dist.dust.dir" value="${basedir}/dist/dust" />
 	<property name="dist.less.dir" value="${basedir}/dist/less" />
+	<property name="dist.web.dir" value="${basedir}/dist/web" />
 	<property name="dist.classes.dir" value="${basedir}/dist/classes" />
 	<property name="dist.packages.dir" value="${basedir}/dist/packages" />
 	<property name="dist.web.package.dir" value="${dist.packages.dir}/azkaban-web-server" />
@@ -49,27 +50,7 @@
 		<delete dir="${dist.classes.dir}" />
 	</target>
 
-	<target name="build" description="Compile main source tree java files">
-		<delete dir="${dist.classes.dir}" />
-		<mkdir dir="${dist.classes.dir}" />
-		<delete dir="${dist.dust.dir}" />
-		<mkdir dir="${dist.dust.dir}" />
-		<delete dir="${dist.less.dir}" />
-		<mkdir dir="${dist.less.dir}" />
-		
-		<!-- copy non-java files to classes dir to load from classpath -->
-		<copy todir="${dist.classes.dir}">
-			<fileset dir="${java.src.dir}">
-				<exclude name="**/*.java" />
-			</fileset>
-		</copy>
-		
-		<javac fork="true" destdir="${dist.classes.dir}"
-			target="1.6" debug="true" deprecation="false" failonerror="true">
-			<src path="${java.src.dir}" />
-			<classpath refid="main.classpath" />
-		</javac>
-
+	<target name="dust" description="Compile Less css files.">
 		<!-- Compile dustjs templates -->
 		<!-- Note: Because apply does not support multiple srcfile and targetfile
 				 elements, and for and foreach requires ant-contrib, we use targetfile 
@@ -85,7 +66,9 @@
 				<outputmapper id="out" type="glob" from="*.tl" to="${dist.dust.dir}/*.js" />
 			</redirector>
 		</apply>
-
+	</target>
+	
+	<target name="less" description="Compile Less css files.">
 		<!-- Compile LESS to CSS -->
 		<echo message="Compiling LESS style sheets." />
 		<apply dir="${less.src.dir}" executable="lessc" relative="true">
@@ -98,6 +81,55 @@
 		</apply>
 	</target>
 	
+	<target name="build" description="Compile main source tree java files">
+		<delete dir="${dist.classes.dir}" />
+		<mkdir dir="${dist.classes.dir}" />
+		<delete dir="${dist.dust.dir}" />
+		<mkdir dir="${dist.dust.dir}" />
+		<delete dir="${dist.less.dir}" />
+		<mkdir dir="${dist.less.dir}" />
+		
+		<!-- copy non-java files to classes dir to load from classpath -->
+		<copy todir="${dist.classes.dir}">
+			<fileset dir="${java.src.dir}">
+				<exclude name="**/*.java" />
+			</fileset>
+		</copy>
+		
+		<javac fork="true" destdir="${dist.classes.dir}"
+			target="1.6" debug="true" deprecation="false" failonerror="true">
+			<src path="${java.src.dir}" />
+			<classpath refid="main.classpath" />
+		</javac>
+
+		<antcall target="dust"></antcall>
+		<antcall target="less"></antcall>
+	</target>
+	
+	<target name="webmin" description="Copies only the non compiled web resources to dist dir">
+		<copy todir="${dist.web.dir}" overwrite="true">
+			<fileset dir="${web.src.dir}" />
+		</copy>
+	</target>
+	
+	<target name="web" description="Creates web resourses in a dir. Useful for development">
+		<mkdir dir="${dist.web.dir}" />
+		
+		<antcall target="webmin"></antcall>
+		<antcall target="dust"></antcall>
+		<antcall target="less"></antcall>
+		
+		<!-- Copy compiled dust templates -->
+		<copy todir="${dist.web.dir}/js">
+			<fileset dir="${dist.dust.dir}" />
+		</copy>
+
+		<!-- Copy compiled less CSS -->
+		<copy todir="${dist.web.dir}/css">
+			<fileset dir="${dist.less.dir}" />
+		</copy>
+	</target>
+	
 	<target name="jars" depends="build" description="Create azkaban jar">
 		<mkdir dir="${dist.jar.dir}" />
 		<jar destfile="${azkaban.jar}">
diff --git a/src/java/azkaban/execapp/FlowRunnerManager.java b/src/java/azkaban/execapp/FlowRunnerManager.java
index 97f10fe..ecc0847 100644
--- a/src/java/azkaban/execapp/FlowRunnerManager.java
+++ b/src/java/azkaban/execapp/FlowRunnerManager.java
@@ -637,7 +637,7 @@ public class FlowRunnerManager implements EventListener {
 	}
 	
 	public String getRunningFlowIds() {
-		List<Integer> ids = new ArrayList<Integer>(runningFlows.keySet());
+		ArrayList<Integer> ids = new ArrayList<Integer>(runningFlows.keySet());
 		Collections.sort(ids);
 		return ids.toString();
 	}
diff --git a/src/java/azkaban/project/JdbcProjectLoader.java b/src/java/azkaban/project/JdbcProjectLoader.java
index e61c2d7..9553d48 100644
--- a/src/java/azkaban/project/JdbcProjectLoader.java
+++ b/src/java/azkaban/project/JdbcProjectLoader.java
@@ -271,6 +271,7 @@ public class JdbcProjectLoader extends AbstractJdbcLoader implements ProjectLoad
 		}
 	}
 
+	@SuppressWarnings("resource")
 	private void uploadProjectFile(Connection connection, Project project, int version, String filetype, String filename, File localFile, String uploader) throws ProjectManagerException {
 		QueryRunner runner = new QueryRunner();
 		long updateTime = System.currentTimeMillis();
@@ -360,6 +361,7 @@ public class JdbcProjectLoader extends AbstractJdbcLoader implements ProjectLoad
 		return handler;
 	}
 	
+	@SuppressWarnings("resource")
 	private ProjectFileHandler getUploadedFile(Connection connection, int projectId, int version) throws ProjectManagerException {
 		QueryRunner runner = new QueryRunner();
 		ProjectVersionResultHandler pfHandler = new ProjectVersionResultHandler();
diff --git a/src/java/azkaban/scheduler/ScheduleManager.java b/src/java/azkaban/scheduler/ScheduleManager.java
index a5afa44..39e42e9 100644
--- a/src/java/azkaban/scheduler/ScheduleManager.java
+++ b/src/java/azkaban/scheduler/ScheduleManager.java
@@ -58,7 +58,6 @@ public class ScheduleManager implements TriggerAgent {
 	 * 
 	 * @param loader
 	 */
-<<<<<<< HEAD
 	public ScheduleManager (ScheduleLoader loader) 
 	{
 		this.loader = loader;
diff --git a/src/java/azkaban/trigger/builtin/ExecuteFlowAction.java b/src/java/azkaban/trigger/builtin/ExecuteFlowAction.java
index ab1586a..caa07bf 100644
--- a/src/java/azkaban/trigger/builtin/ExecuteFlowAction.java
+++ b/src/java/azkaban/trigger/builtin/ExecuteFlowAction.java
@@ -216,7 +216,7 @@ public class ExecuteFlowAction implements TriggerAction {
 			throw new RuntimeException("Error finding the flow to execute " + flowName);
 		}
 		
-		ExecutableFlow exflow = new ExecutableFlow(flow);
+		ExecutableFlow exflow = new ExecutableFlow(project, flow);
 		exflow.setSubmitUser(submitUser);
 		exflow.addAllProxyUsers(project.getProxyUsers());
 		
diff --git a/src/java/azkaban/trigger/builtin/KillExecutionAction.java b/src/java/azkaban/trigger/builtin/KillExecutionAction.java
index dd53efe..3114fd2 100644
--- a/src/java/azkaban/trigger/builtin/KillExecutionAction.java
+++ b/src/java/azkaban/trigger/builtin/KillExecutionAction.java
@@ -23,6 +23,7 @@ import org.apache.log4j.Logger;
 
 import azkaban.executor.ExecutableFlow;
 import azkaban.executor.ExecutorManagerAdapter;
+import azkaban.executor.Status;
 import azkaban.trigger.TriggerAction;
 
 public class KillExecutionAction implements TriggerAction{
@@ -89,7 +90,7 @@ public class KillExecutionAction implements TriggerAction{
 	public void doAction() throws Exception {
 		ExecutableFlow exFlow = executorManager.getExecutableFlow(execId);
 		logger.info("ready to kill execution " + execId);
-		if(!ExecutableFlow.isFinished(exFlow)) {
+		if(!Status.isStatusFinished(exFlow.getStatus())) {
 			logger.info("Killing execution " + execId);
 			executorManager.cancelFlow(exFlow, "azkaban_sla");
 		}
diff --git a/src/java/azkaban/utils/Emailer.java b/src/java/azkaban/utils/Emailer.java
index dcc749c..a4e2ef8 100644
--- a/src/java/azkaban/utils/Emailer.java
+++ b/src/java/azkaban/utils/Emailer.java
@@ -163,7 +163,7 @@ public class Emailer extends AbstractMailer implements Alerter {
 		ArrayList<String> failedJobs = new ArrayList<String>();
 		for (ExecutableNode node : flow.getExecutableNodes()) {
 			if (node.getStatus() == Status.FAILED) {
-				failedJobs.add(node.getJobId());
+				failedJobs.add(node.getId());
 			}
 		}
 		return failedJobs;
diff --git a/src/java/azkaban/utils/StringUtils.java b/src/java/azkaban/utils/StringUtils.java
index 0ea393c..77c5941 100644
--- a/src/java/azkaban/utils/StringUtils.java
+++ b/src/java/azkaban/utils/StringUtils.java
@@ -17,6 +17,7 @@
 package azkaban.utils;
 
 import java.util.Collection;
+import java.util.List;
 
 public class StringUtils {
 	public static final char SINGLE_QUOTE = '\'';
diff --git a/src/java/azkaban/webapp/servlet/ExecutorServlet.java b/src/java/azkaban/webapp/servlet/ExecutorServlet.java
index fdae097..00ed276 100644
--- a/src/java/azkaban/webapp/servlet/ExecutorServlet.java
+++ b/src/java/azkaban/webapp/servlet/ExecutorServlet.java
@@ -21,7 +21,6 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
 import javax.servlet.ServletConfig;
 import javax.servlet.ServletException;
@@ -860,7 +859,7 @@ public class ExecutorServlet extends LoginAbstractAzkabanServlet {
 			return;
 		}
 		
-		ExecutableFlow exflow = new ExecutableFlow(flow);
+		ExecutableFlow exflow = new ExecutableFlow(project, flow);
 		exflow.setSubmitUser(user.getUserId());
 		exflow.addAllProxyUsers(project.getProxyUsers());
 
diff --git a/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java b/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java
index 8a271f5..9f907cc 100644
--- a/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java
+++ b/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java
@@ -31,7 +31,6 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.HashSet;
 import java.util.Set;
 
 import javax.servlet.ServletConfig;
@@ -596,41 +595,6 @@ public class ProjectManagerServlet extends LoginAbstractAzkabanServlet {
 		ret.put("nodes", nodeList);
 	}
 	
-	private void fillFlowInfo(Project project, String flowId, HashMap<String, Object> ret) {
-		Flow flow = project.getFlow(flowId);
-		
-		//Collections.sort(flowNodes, NODE_LEVEL_COMPARATOR);
-		ArrayList<Map<String, Object>> nodeList = new ArrayList<Map<String, Object>>();
-		for (Node node: flow.getNodes()) {
-			HashMap<String, Object> nodeObj = new HashMap<String,Object>();
-			nodeObj.put("id", node.getId());
-			nodeObj.put("level", node.getLevel());
-			nodeObj.put("type", node.getType());
-			if (node.getEmbeddedFlowId() != null) {
-				nodeObj.put("flowId", node.getEmbeddedFlowId());
-			}
-			
-			nodeList.add(nodeObj);
-		}
-		
-		ArrayList<Map<String, Object>> edgeList = new ArrayList<Map<String, Object>>();
-		for (Edge edge: flow.getEdges()) {
-			HashMap<String, Object> edgeObj = new HashMap<String,Object>();
-			edgeObj.put("from", edge.getSourceId());
-			edgeObj.put("target", edge.getTargetId());
-			
-			if (edge.hasError()) {
-				edgeObj.put("error", edge.getError());
-			}
-			
-			edgeList.add(edgeObj);
-		}
-		
-		ret.put("flowId", flowId);
-		ret.put("nodes", nodeList);
-		ret.put("edges", edgeList);
-	}
-	
 	private void ajaxFetchFlowNodeData(Project project, HashMap<String, Object> ret, HttpServletRequest req) throws ServletException {
 		String flowId = getParam(req, "flow");
 		Flow flow = project.getFlow(flowId);
diff --git a/src/java/azkaban/webapp/servlet/velocity/flowexecutionpanel.vm b/src/java/azkaban/webapp/servlet/velocity/flowexecutionpanel.vm
index 281bdf7..01f328c 100644
--- a/src/java/azkaban/webapp/servlet/velocity/flowexecutionpanel.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/flowexecutionpanel.vm
@@ -14,13 +14,6 @@
  * the License.
 *#
 
-			<script type="text/javascript" src="${context}/js/azkaban.layout.js"></script>
-			<script type="text/javascript" src="${context}/js/svgNavigate.js"></script>
-			<script type="text/javascript" src="${context}/js/azkaban.context.menu.js"></script>
-			<script type="text/javascript" src="${context}/js/azkaban.common.utils.js"></script>
-			<script type="text/javascript" src="${context}/js/azkaban.svg.graph.view.js"></script>
-			<script type="text/javascript" src="${context}/js/azkaban.flow.execute.view.js"></script>
-
 			<div class="modal modal-wide" id="execute-flow-panel">
 				<div class="modal-dialog">
 					<div class="modal-content">
diff --git a/src/java/azkaban/webapp/servlet/velocity/flowgraphview.vm b/src/java/azkaban/webapp/servlet/velocity/flowgraphview.vm
index 60cdb96..44d6654 100644
--- a/src/java/azkaban/webapp/servlet/velocity/flowgraphview.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/flowgraphview.vm
@@ -24,8 +24,13 @@
             <div class="panel-heading">
               <input id="filter" type="text" placeholder="Job Filter" class="form-control">
             </div>
-            <div id="list" class="list-group"></div>
+            <div id="joblist" class="list-group"></div>
             <div class="panel-footer">
+            	<div id="autoPanZoom" class="checkbox">
+            		<label>
+						<input type="checkbox" id="autoPanZoomCheckbox" class="autoPanZoom" value="autoPanZoom" />Auto Pan Zoom
+              		</label>
+              	</div>
               <button type="button" class="btn btn-sm btn-default" id="resetPanZoomBtn">Reset Pan Zoom</button>
             </div>
           </div><!-- /.panel -->
diff --git a/src/java/azkaban/webapp/servlet/velocity/flowpage.vm b/src/java/azkaban/webapp/servlet/velocity/flowpage.vm
index a3672b3..eb355ef 100644
--- a/src/java/azkaban/webapp/servlet/velocity/flowpage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/flowpage.vm
@@ -24,16 +24,23 @@
 		<script type="text/javascript" src="${context}/js/moment.min.js"></script>
 		<script type="text/javascript" src="${context}/js/bootstrap-datetimepicker.min.js"></script>
 		
-    <script type="text/javascript" src="${context}/js/dust-core-2.2.2.min.js"></script>
-		<script type="text/javascript" src="${context}/js/flowsummary.js"></script>
-
+		<script type="text/javascript" src="${context}/js/dust-core-2.2.2.min.js"></script>
+		<script type="text/javascript" src="${context}/js/flowsummary.js"></script> 
+ 
+ 		<script type="text/javascript" src="${context}/js/jquery.svg.min.js"></script> 
+ 		<script type="text/javascript" src="${context}/js/jquery.svganim.min.js"></script> 
+		<script type="text/javascript" src="${context}/js/jquery.svgfilter.min.js"></script>
+
+		<script type="text/javascript" src="${context}/js/svgutils.js"></script> 
 		<script type="text/javascript" src="${context}/js/azkaban.date.utils.js"></script>
 		<script type="text/javascript" src="${context}/js/azkaban.ajax.utils.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.common.utils.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban.context.menu.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban.job.status.utils.js"></script>
 		<script type="text/javascript" src="${context}/js/azkaban.layout.js"></script>
 		<script type="text/javascript" src="${context}/js/azkaban.flow.view.js"></script>
 		<script type="text/javascript" src="${context}/js/azkaban.flow.job.view.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.flow.graph.view.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban.svg.flow.loader.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban.svg.graph.view.js"></script>
 		<script type="text/javascript" src="${context}/js/svgNavigate.js"></script>
 		<script type="text/javascript">
 			var contextURL = "${context}";
@@ -47,6 +54,7 @@
 			var flowId = "${flowid}";
 			var execId = null;
 		</script>
+		<link rel="stylesheet" type="text/css" href="${context}/css/flow.css" />
 		<link rel="stylesheet" type="text/css" href="${context}/css/bootstrap-datetimepicker.css" />
 		<link rel="stylesheet" type="text/css" href="${context}/css/azkaban-graph.css" /> 
 	</head>
diff --git a/src/less/azkaban-svg.less b/src/less/azkaban-svg.less
index 31811f9..211d1b5 100644
--- a/src/less/azkaban-svg.less
+++ b/src/less/azkaban-svg.less
@@ -1,3 +1,4 @@
+
 svg {
   .edge {
     stroke: #777;

src/less/flow.less 125(+80 -45)

diff --git a/src/less/flow.less b/src/less/flow.less
index 931f47c..042f1cd 100644
--- a/src/less/flow.less
+++ b/src/less/flow.less
@@ -120,51 +120,86 @@ td {
 }
 
 // TODO: Rename this as #job-list
-#list {
+#joblist {
   height: 100%;
-
-  a {
-    &.nodedisabled,
-    &.DISABLED {
-      opacity: 0.3;
-    }
-
-    &.DISABLED .icon {
-      background-position: 16px 0px;
-    }
-
-    &.READY .icon {
-      background-position: 16px 0px;
-    }
-
-    &.QUEUED .icon {
-      opacity: 0.5;
-      background-position: 32px 0px;
-    }
-
-    &.RUNNING .icon {
-      background-position: 32px 0px;
-    }
-
-    &.SUCCEEDED .icon {
-      background-position: 48px 0px;
-    }
-
-    &.FAILED .icon {
-      background-position: 0px 0px;
-    }
-
-    &.KILLED .icon {
-      background-position: 0px 0px;
-    }
-
-    .icon {
-      float: left;
-      width: 16px;
-      height: 16px;
-      margin: 2px 4px 0px -5px;
-      background-image: url("./images/dot-icon.png");
-      background-position: 16px 0px;
-    }
+  ul {
+	list-style-type: none;
+	padding-left: 0px;
+	
+	li {
+		&.active > a {
+			background-color: #D9EDFF;
+		}
+	
+		ul {
+			padding-left: 20px;
+		}
+	
+		&.subFilter > a > .expandarrow {
+			color : #3398cc;
+		}
+	
+		a {
+			clear:both;
+			border-bottom-width: 0;
+		
+			&.nodedisabled,
+			&.DISABLED {
+			  opacity: 0.3;
+			}
+			
+			&.DISABLED .icon {
+			  background-position: 16px 0px;
+			}
+			
+			&.READY .icon {
+			  background-position: 16px 0px;
+			}
+			
+			&.QUEUED .icon {
+			  opacity: 0.5;
+			  background-position: 32px 0px;
+			}
+			
+			&.RUNNING .icon {
+			  background-position: 32px 0px;
+			}
+			
+			&.SUCCEEDED .icon {
+			  background-position: 48px 0px;
+			}
+			
+			&.FAILED .icon {
+			  background-position: 0px 0px;
+			}
+			
+			&.KILLED .icon {
+			  background-position: 0px 0px;
+			}
+			
+			&.FAILED_FINISHING .icon {
+			  background-position: 0px 0px;
+			}
+			
+			.icon {
+			  float: left;
+			  width: 16px;
+			  height: 16px;
+			  margin: 2px 4px 0px -5px;
+			  background-image: url("./images/dot-icon.png");
+			  background-position: 16px 0px;
+			}
+			
+			.expandarrow {
+				float: right;
+				width: 16px;
+				height: 16px;
+			}
+			
+			.filterHighlight {
+				background-color: #FFFF00;
+			}
+		}
+	}
   }
 }
diff --git a/src/web/css/azkaban-graph.css b/src/web/css/azkaban-graph.css
index c78a508..8b1184a 100644
--- a/src/web/css/azkaban-graph.css
+++ b/src/web/css/azkaban-graph.css
@@ -11,6 +11,13 @@
 	cursor: pointer;
 }
 
+.node.selected > .nodebox .border {
+	stroke-width: 3;
+}
+
+.node.selected > .nodebox .flowborder {
+	stroke-width: 3;
+}
 .nodebox > .border:hover {
 	fill-opacity: 0.7;
 }
diff --git a/src/web/js/azkaban.flow.job.view.js b/src/web/js/azkaban.flow.job.view.js
index cb28674..4cf6c13 100644
--- a/src/web/js/azkaban.flow.job.view.js
+++ b/src/web/js/azkaban.flow.job.view.js
@@ -18,11 +18,11 @@ azkaban.JobListView = Backbone.View.extend({
 	events: {
 		"keyup input": "filterJobs",
 		"click .job": "handleJobClick",
-		"click .resetPanZoomBtn": "handleResetPanZoom",
-		"contextmenu li": "handleContextMenuClick"
+		"click #resetPanZoomBtn": "handleResetPanZoom",
+		"contextmenu li.listElement": "handleContextMenuClick",
 		"change .autoPanZoom" : "handleAutoPanZoom",
+		"click .expandarrow" : "handleToggleMenuExpand"
 	},
-	
 	initialize: function(settings) {
 		this.model.bind('change:selected', this.handleSelectionChange, this);
 		this.model.bind('change:disabled', this.handleDisabledChange, this);
@@ -30,61 +30,69 @@ azkaban.JobListView = Backbone.View.extend({
 		this.model.bind('change:update', this.handleStatusUpdate, this);
 		
 		this.filterInput = $(this.el).find("#filter");
-		this.list = $(this.el).find("#list");
+		this.list = $(this.el).find("#joblist");
 		this.contextMenu = settings.contextMenuCallback;
 		this.listNodes = {};
 	},
-	
 	filterJobs: function(self) {
 		var filter = this.filterInput.val();
-		if (filter && filter.trim() != "") {
-			filter = filter.trim();
-			if (filter == "") {
-				if (this.filter) {
-					this.jobs.children().each(function(){
-						var a = $(this).find("a");
-						$(a).html(this.jobid);
-						$(this).show();
-					});
-				}
-				this.filter = null;
-				return;
-			}
-		}
-		else {
-			if (this.filter) {
-				this.jobs.children().each(function(){
-					var a = $(this).find("a");
-					$(a).html(this.jobid);
-					$(this).show();
-				});
-			}
-				
-			this.filter = null;
+		// Clear all filters first
+		if (!filter || filter.trim() == "") {
+			this.unfilterAll(self);
 			return;
 		}
 		
-		this.jobs.children().each(function() {
-			var jobid = this.jobid;
-			var index = jobid.indexOf(filter);
+		this.hideAll(self);
+		var showList = {};
+		
+		// find the jobs that need to be exposed.
+		for (var key in this.listNodes) {
+			var li = this.listNodes[key];
+			var node = li.node;
+			var nodeName = node.id;
+			node.listElement = li;
+
+			var index = nodeName.indexOf(filter);
 			if (index != -1) {
-				var a = $(this).find("a");
+				var spanlabel = $(li).find("> a > span");
+				
 				var endIndex = index + filter.length;
-				var newHTML = jobid.substring(0, index) + "<span>" + 
-						jobid.substring(index, endIndex) + "</span>" + 
-						jobid.substring(endIndex, jobid.length);
+				var newHTML = nodeName.substring(0, index) + "<span class=\"filterHighlight\">" + 
+					nodeName.substring(index, endIndex) + "</span>" + 
+					nodeName.substring(endIndex, nodeName.length);
+				$(spanlabel).html(newHTML);
 				
-				$(a).html(newHTML);
-				$(this).show();
-			}
-			else {
-				$(this).hide();
+				// Apply classes to all the included embedded flows.
+				var pIndex = key.length;
+				while((pIndex = key.lastIndexOf(":", pIndex - 1)) > 0) {
+					var parentId = key.substr(0, pIndex);
+					var parentLi = this.listNodes[parentId];
+					$(parentLi).show();
+					$(parentLi).addClass("subFilter");
+				}
+				
+				$(li).show();
 			}
-		});
-			
-		this.filter = filter;
+		}
+	},
+	hideAll: function(self) {
+		for (var key in this.listNodes) {
+			var li = this.listNodes[key];
+			var label = $(li).find("> a > span");
+			$(label).text(li.node.id);
+			$(li).removeClass("subFilter");
+			$(li).hide();
+		}
+	},
+	unfilterAll: function(self) {
+		for (var key in this.listNodes) {
+			var li = this.listNodes[key];
+			var label = $(li).find("> a > span");
+			$(label).text(li.node.id);
+			$(li).removeClass("subFilter");
+			$(li).show();
+		}
 	},
-	
 	handleStatusUpdate: function(evt) {
 		var updateData = this.model.get("update");
 		if (updateData.nodes) {
@@ -97,28 +105,38 @@ azkaban.JobListView = Backbone.View.extend({
 			}
 		}
 	},
-	
-	assignInitialStatus: function(evt) {
-		var data = this.model.get("data");
+	changeStatuses: function(data) {
 		for (var i = 0; i < data.nodes.length; ++i) {
-			var updateNode = data.nodes[i];
-			var job = this.listNodes[updateNode.id];
-      if (!$(job).hasClass("list-group-item")) {
-        $(job).addClass("list-group-item");
-      }
-			$(job).addClass(updateNode.status);
+			var node = data.nodes[i];
+			if (node.status) {
+				var liElement = node.listElement;
+				$(liElement).removeClass(statusList.join(' '));
+				$(liElement).addClass(node.status);
+			}
+			
+			if (node.flowData) {
+				this.changeStatuses(node.flowData);
+			}
 		}
 	},
-	
 	render: function(self) {
 		var data = this.model.get("data");
 		var nodes = data.nodes;
 		
-		this.listNodes = {}; 
+		this.renderTree(this.list, data);
+//		
+//		this.assignInitialStatus(self);
+//		this.handleDisabledChange(self);
+	},
+	renderTree : function(el, data, prefix) {
+		var nodes = data.nodes;
 		if (nodes.length == 0) {
 			console.log("No results");
 			return;
 		};
+		if (!prefix) {
+			prefix = "";
+		}
 	
 		var nodeArray = nodes.slice(0);
 		nodeArray.sort(function(a, b) {
@@ -131,53 +149,104 @@ azkaban.JobListView = Backbone.View.extend({
 			}
 		});
 		
-		var list = this.list;
-		this.jobs = $(list);
-		for (var i = 0; i < nodeArray.length; ++i) {
+		var ul = document.createElement('ul');
+		for(var i=0; i < nodeArray.length; ++i) {
+			var li = document.createElement("li");
+			$(li).addClass("listElement");
+			
+			// This is used for the filter step.
+			var listNodeName = prefix + nodeArray[i].id;
+			this.listNodes[listNodeName]=li;
+			li.node = nodeArray[i];
+			li.node.listElement = li;
+
 			var a = document.createElement("a");
+			var iconDiv = document.createElement('div');
+			$(iconDiv).addClass('icon');
+			
+			$(a).append(iconDiv);
 			$(a).addClass('list-group-item').addClass('job');
-      $(a).attr('href', '#');
-      
-      var iconDiv = document.createElement('div');
-      $(iconDiv).addClass('icon');
-      $(a).append(iconDiv);
-			$(a).append(nodeArray[i].id);
-			$(list).append(a);
-			a.jobid = nodeArray[i].id;
-			this.listNodes[nodeArray[i].id] = a;
+			
+			var span = document.createElement("span");
+			$(span).text(nodeArray[i].id);
+			$(span).addClass("jobname");
+			$(a).append(span);
+			$(li).append(a);
+			$(ul).append(li);
+			
+			if (nodeArray[i].flowData) {
+				// Add the up down
+				var expandDiv = document.createElement("div");
+				$(expandDiv).addClass("expandarrow glyphicon glyphicon-chevron-down");
+				$(a).append(expandDiv);
+				
+				// Create subtree
+				var subul = this.renderTree(li, nodeArray[i].flowData, listNodeName + ":");
+				$(subul).hide();
+			}
 		}
 		
-		this.assignInitialStatus(self);
-		this.handleDisabledChange(self);
+		$(el).append(ul);
+		return ul;
+	},
+	handleMenuExpand: function(li) {
+		var expandArrow = $(li).find("> a > .expandarrow");
+		var submenu = $(li).find("> ul");
+		
+		$(expandArrow).removeClass("glyphicon-chevron-down");
+		$(expandArrow).addClass("glyphicon-chevron-up");
+		$(submenu).slideDown();
+	},
+	handleMenuCollapse: function(li) {
+		var expandArrow = $(li).find("> a > .expandarrow");
+		var submenu = $(li).find("> ul");
+		
+		$(expandArrow).removeClass("glyphicon-chevron-up");
+		$(expandArrow).addClass("glyphicon-chevron-down");
+		$(submenu).slideUp();
+	},
+	handleToggleMenuExpand: function(evt) {
+		var expandarrow = evt.currentTarget;
+		var li = $(evt.currentTarget).closest("li.listElement");
+		var submenu = $(li).find("> ul");
+
+		if ($(submenu).is(":visible")) {
+			this.handleMenuCollapse(li);
+		}
+		else {
+			this.handleMenuExpand(li);
+		}
+		
+		evt.stopImmediatePropagation();
 	},
-	
 	handleContextMenuClick: function(evt) {
 		if (this.contextMenu) {
-			this.contextMenu(evt);
+			this.contextMenu(evt, this.model, evt.currentTarget.node);
 			return false;
 		}
 	},
-	
 	handleJobClick: function(evt) {
-		var jobid = evt.currentTarget.jobid;
-		if (!evt.currentTarget.jobid) {
+		console.log("Job clicked");
+		var li = $(evt.currentTarget).closest("li.listElement");
+		var node = li[0].node;
+		if (!node) {
 			return;
 		}
 		
 		if (this.model.has("selected")) {
 			var selected = this.model.get("selected");
-			if (selected == jobid) {
+			if (selected == node) {
 				this.model.unset("selected");
 			}
 			else {
-				this.model.set({"selected": jobid});
+				this.model.set({"selected": node});
 			}
 		}
 		else {
-			this.model.set({"selected": jobid});
+			this.model.set({"selected": node});
 		}
+
 	},
-	
 	handleDisabledChange: function(evt) {
 		var disabledMap = this.model.get("disabled");
 		var nodes = this.model.get("nodes");
@@ -191,7 +260,6 @@ azkaban.JobListView = Backbone.View.extend({
 			}
 		}
 	},
-	
 	handleSelectionChange: function(evt) {
 		if (!this.model.hasChanged("selected")) {
 			return;
@@ -199,16 +267,23 @@ azkaban.JobListView = Backbone.View.extend({
 		
 		var previous = this.model.previous("selected");
 		var current = this.model.get("selected");
-		
+
 		if (previous) {
-			$(this.listNodes[previous]).removeClass("active");
+			$(previous.listElement).removeClass("active");
 		}
 		
 		if (current) {
-			$(this.listNodes[current]).addClass("active");
+			$(current.listElement).addClass("active");
+			this.propagateExpansion(current.listElement);
+		}
+	},
+	propagateExpansion: function(li) {
+		var li = $(li).parent().closest("li.listElement")[0];
+		if (li) {
+			this.propagateExpansion(li);
+			this.handleMenuExpand(li);
 		}
 	},
-	
 	handleResetPanZoom: function(evt) {
 		this.model.trigger("resetPanZoom");
 	},
diff --git a/src/web/js/azkaban.flow.view.js b/src/web/js/azkaban.flow.view.js
index 2e33ddb..a6fd36e 100644
--- a/src/web/js/azkaban.flow.view.js
+++ b/src/web/js/azkaban.flow.view.js
@@ -362,52 +362,6 @@ azkaban.SummaryView = Backbone.View.extend({
 	},
 });
 
-var exNodeClickCallback = function(event) {
-	console.log("Node clicked callback");
-	var jobId = event.currentTarget.jobid;
-	var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + 
-			flowId + "&job=" + jobId;
-
-	var menu = [	
-		{title: "Open Job...", callback: function() {window.location.href=requestURL;}},
-		{title: "Open Job in New Window...", callback: function() {window.open(requestURL);}}
-	];
-
-	contextMenuView.show(event, menu);
-}
-
-var exJobClickCallback = function(event) {
-	console.log("Node clicked callback");
-	var jobId = event.currentTarget.jobid;
-	var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + 
-			flowId + "&job=" + jobId;
-
-	var menu = [	
-		{title: "Open Job...", callback: function() {window.location.href=requestURL;}},
-		{title: "Open Job in New Window...", callback: function() {window.open(requestURL);}}
-	];
-
-	contextMenuView.show(event, menu);
-}
-
-var exEdgeClickCallback = function(event) {
-	console.log("Edge clicked callback");
-}
-
-var exGraphClickCallback = function(event) {
-	console.log("Graph clicked callback");
-	var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowId;
-
-	var menu = [	
-		{title: "Open Flow...", callback: function() {window.location.href=requestURL;}},
-		{title: "Open Flow in New Window...", callback: function() {window.open(requestURL);}},
-		{break: 1},
-		{title: "Center Graph", callback: function() {graphModel.trigger("resetPanZoom");}}
-	];
-	
-	contextMenuView.show(event, menu);
-}
-
 var graphModel;
 azkaban.GraphModel = Backbone.Model.extend({});
 
@@ -444,16 +398,16 @@ $(function() {
 		el: $('#svgDiv'), 
 		model: graphModel, 
 		rightClick: { 
-			"node": exNodeClickCallback, 
-			"edge": exEdgeClickCallback, 
-			"graph": exGraphClickCallback 
+			"node": nodeClickCallback, 
+			"edge": edgeClickCallback, 
+			"graph": graphClickCallback 
 		}
 	});
 	
   jobsListView = new azkaban.JobListView({
 		el: $('#jobList'), 
 		model: graphModel, 
-		contextMenuCallback: exJobClickCallback
+		contextMenuCallback: jobClickCallback
 	});
 	
 	var requestURL = contextURL + "/manager";
@@ -477,32 +431,9 @@ $(function() {
 		"flow": flowId
 	};
 	var successHandler = function(data) {
-		// Create the nodes
-		var nodes = {};
-		for (var i = 0; i < data.nodes.length; ++i) {
-			var node = data.nodes[i];
-			nodes[node.id] = node;
-		}
-		for (var i = 0; i < data.edges.length; ++i) {
-			var edge = data.edges[i];
-			var fromNode = nodes[edge.from];
-			var toNode = nodes[edge.target];
-			
-			if (!fromNode.outNodes) {
-				fromNode.outNodes = {};
-			}
-			fromNode.outNodes[toNode.id] = toNode;
-			
-			if (!toNode.inNodes) {
-				toNode.inNodes = {};
-			}
-			toNode.inNodes[fromNode.id] = fromNode;
-		}
-	
 		console.log("data fetched");
-		graphModel.set({data: data});
-		graphModel.set({nodes: nodes});
-		graphModel.set({disabled: {}});
+		processFlowData(data);
+		graphModel.set({data:data});
 		graphModel.trigger("change:graph");
 		
 		// Handle the hash changes here so the graph finishes rendering first.
diff --git a/src/web/js/azkaban.job.status.utils.js b/src/web/js/azkaban.job.status.utils.js
index ee03ae6..6813d10 100644
--- a/src/web/js/azkaban.job.status.utils.js
+++ b/src/web/js/azkaban.job.status.utils.js
@@ -14,10 +14,10 @@
  * the License.
  */
 
-var statusList = ["FAILED", "FAILED_FINISHING", "SUCCEEDED", "RUNNING", "WAITING", "KILLED", "DISABLED", "READY", "UNKNOWN", "PAUSED", "SKIPPED"];
+var statusList = ["QUEUED", "FAILED", "FAILED_FINISHING", "SUCCEEDED", "RUNNING", "WAITING", "KILLED", "DISABLED", "READY", "UNKNOWN", "PAUSED", "SKIPPED"];
 var statusStringMap = {
+	"QUEUED":"Queued",
 	"SKIPPED": "Skipped",
-	"PREPARING": "Preparing",
 	"FAILED": "Failed",
 	"SUCCEEDED": "Success",
 	"FAILED_FINISHING": "Running w/Failure",
diff --git a/src/web/js/azkaban.svg.flow.loader.js b/src/web/js/azkaban.svg.flow.loader.js
index 88e46cf..a3cbc71 100644
--- a/src/web/js/azkaban.svg.flow.loader.js
+++ b/src/web/js/azkaban.svg.flow.loader.js
@@ -104,14 +104,14 @@ var nodeClickCallback = function(event, model, node) {
 
 	var target = event.currentTarget;
 	var type = node.type;
-	var flowId = node.flowId;
+	var flowId = node.parent.flow;
 	var jobId = node.id;
 	
 	var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowId + "&job=" + jobId;
 	var menu = [];
 
 	if (type == "flow") {
-		var flowRequestURL = contextURL + "/manager?project=" + projectName + "&flow=" + event.currentTarget.flowId;
+		var flowRequestURL = contextURL + "/manager?project=" + projectName + "&flow=" + node.flowId;
 		if (node.expanded) {
 			menu = [{title: "Collapse Flow...", callback: function() {model.trigger("collapseFlow", node);}}];
 		}
@@ -144,17 +144,20 @@ var nodeClickCallback = function(event, model, node) {
 	contextMenuView.show(event, menu);
 }
 
-var jobClickCallback = function(event, model) {
+var jobClickCallback = function(event, model, node) {
 	console.log("Node clicked callback");
-	var jobId = event.currentTarget.jobid;
-	var node = target.data;
-	var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowId + "&job=" + jobId;
+	var target = event.currentTarget;
+	var type = node.type;
+	var flowId = node.parent.flow;
+	var jobId = node.id;
+
+	var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowId + "&job=" + node.id;
 
 	var menu;
-	if (event.currentTarget.jobtype == "flow") {
-		var flowRequestURL = contextURL + "/manager?project=" + projectName + "&flow=" + event.currentTarget.flowId;
+	if (type == "flow") {
+		var flowRequestURL = contextURL + "/manager?project=" + projectName + "&flow=" + node.flowId;
 		menu = [
-				{title: "View Flow...", callback: function() {openJobDisplayCallback(jobId, flowId, event)}},
+				{title: "View Properties...", callback: function() {openJobDisplayCallback(jobId, flowId, event)}},
 				{break: 1},
 				{title: "Open Flow...", callback: function() {window.location.href=flowRequestURL;}},
 				{title: "Open Flow in New Window...", callback: function() {window.open(flowRequestURL);}},
diff --git a/src/web/js/azkaban.svg.graph.view.js b/src/web/js/azkaban.svg.graph.view.js
index 7f90247..a3cc2e0 100644
--- a/src/web/js/azkaban.svg.graph.view.js
+++ b/src/web/js/azkaban.svg.graph.view.js
@@ -17,12 +17,8 @@ $.namespace('azkaban');
 
 azkaban.SvgGraphView = Backbone.View.extend({
 	events: {
-
-	},
-	test: function() {
-		console.log("test");
+		
 	},
-	
   initialize: function(settings) {
 		this.model.bind('change:selected', this.changeSelected, this);
 		this.model.bind('centerNode', this.centerNode, this);
@@ -97,8 +93,21 @@ azkaban.SvgGraphView = Backbone.View.extend({
 			nodes[i].label = nodes[i].id;
 		}
 		
+		var self = this;
 		for (var i = 0; i < nodes.length; ++i) {
 			this.drawNode(this, nodes[i], g);
+			$(nodes[i].gNode).click(function(evt) {
+				var selected = self.model.get("selected");
+				if (selected == evt.currentTarget.data) {
+					self.model.unset("selected");
+				}
+				else {
+					self.model.set({"selected":evt.currentTarget.data});
+				}
+				
+				evt.stopPropagation();
+				evt.cancelBubble = true;
+			});
 		}
 
 		// layout
@@ -126,25 +135,6 @@ azkaban.SvgGraphView = Backbone.View.extend({
 			this.handleDisabledChange(self);
 		}
 
-/*
-		if (this.rightClick) {
-			var callbacks = this.rightClick;
-			var currentTarget = self.currentTarget;
-			if (callbacks.node && currentTarget.jobid) {
-				callbacks.node(self, this.model, currentTarget.nodeobj);
-			}
-			else if (callbacks.edge && (currentTarget.nodeName == "polyline" || currentTarget.nodeName == "line")) {
-				callbacks.edge(self, this.model);
-			}
-			else if (callbacks.graph) {
-				callbacks.graph(self, this.model);
-			}
-			return false;
-		}
-	
-*/
-
-		var self = this;
 		if (self.rightClick) {
 			if (self.rightClick.node) {
 				// Proper children selectors don't work properly on svg
@@ -197,36 +187,35 @@ azkaban.SvgGraphView = Backbone.View.extend({
 			$(g).attr("title", initialStatus);
 		}
 	},
-	
-  changeSelected: function(self) {
+	changeSelected: function(self) {
 		console.log("change selected");
 		var selected = this.model.get("selected");
 		var previous = this.model.previous("selected");
 		
 		if (previous) {
 			// Unset previous
-			var g = this.gNodes[previous];
-			removeClass(g, "selected");
+			removeClass(previous.gNode, "selected");
 		}
 		
 		if (selected) {
-			var g = this.gNodes[selected];
-			var node = this.nodes[selected];
-			
+			this.propagateExpansion(selected);
+			var g = selected.gNode;
 			addClass(g, "selected");
 			
 			console.log(this.model.get("autoPanZoom"));
 			if (this.model.get("autoPanZoom")) {
-				var offset = 150;
-				var widthHeight = offset*2;
-				var x = node.x - offset;
-				var y = node.y - offset;
-				
-				$(this.svgGraph).svgNavigate("transformToBox", {x: x, y: y, width: widthHeight, height: widthHeight});
+				this.centerNode(selected);
+			}
+		}
+	},
+  propagateExpansion: function(node) {
+		if (node.parent) {
+			if (node.parent.node) {
+				this.propagateExpansion(node.parent.node);
+				this.expandFlow(node.parent.node);
 			}
 		}
 	},
-	
   handleStatusUpdate: function(evt) {
 		var updateData = this.model.get("update");
 		if (updateData.nodes) {
@@ -251,8 +240,8 @@ azkaban.SvgGraphView = Backbone.View.extend({
 	
   clickGraph: function(self) {
 		console.log("click");
-		if (self.currentTarget.jobid) {
-			this.model.set({"selected": self.currentTarget.jobid});
+		if (self.currentTarget.data) {
+			this.model.set({"selected": self.currentTarget.data});
 		}
 	},
 	
@@ -310,12 +299,6 @@ azkaban.SvgGraphView = Backbone.View.extend({
 		else {
 			this.drawBoxNode(self, node, g);
 		}
-// 		
-// 		var boundingBox = node.gNode.getBBox();
-// 		node.width = boundingBox.width;
-// 		node.height = boundingBox.height;
-// 		node.centerX = node.width/2;
-// 		node.centerY = node.height/2;
 	},
 	moveNodes: function(nodes) {
 		var svg = this.svg;
@@ -326,7 +309,6 @@ azkaban.SvgGraphView = Backbone.View.extend({
 			svg.change(gNode, {"transform": translateStr(node.x, node.y)});
 		}
 	},
-
 	expandFlow: function(node) {
 		var svg = this.svg;
 		var gnode = node.gNode;
@@ -592,8 +574,7 @@ azkaban.SvgGraphView = Backbone.View.extend({
 		$(borderRect).animate({svgWidth: node.width, svgHeight: node.height}, time);
 		$(borderRect).animate({svgFill: 'white'}, time);
 	},
-	
-  resetPanZoom: function(duration) {
+	resetPanZoom: function(duration) {
 		var bounds = this.graphBounds;
 		var param = {x: bounds.minX, y: bounds.minY, width: (bounds.maxX - bounds.minX), height: (bounds.maxY - bounds.minY), duration: duration };
 

src/web/js/jquery.svg.js 1394(+1394 -0)

diff --git a/src/web/js/jquery.svg.js b/src/web/js/jquery.svg.js
new file mode 100644
index 0000000..dcbf95f
--- /dev/null
+++ b/src/web/js/jquery.svg.js
@@ -0,0 +1,1394 @@
+/* http://keith-wood.name/svg.html
+   SVG for jQuery v1.4.5.
+   Written by Keith Wood (kbwood{at}iinet.com.au) August 2007.
+   Dual licensed under the GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) and 
+   MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt) licenses. 
+   Please attribute the author if you use it. */
+
+(function($) { // Hide scope, no $ conflict
+
+/* SVG manager.
+   Use the singleton instance of this class, $.svg, 
+   to interact with the SVG functionality. */
+function SVGManager() {
+	this._settings = []; // Settings to be remembered per SVG object
+	this._extensions = []; // List of SVG extensions added to SVGWrapper
+		// for each entry [0] is extension name, [1] is extension class (function)
+		// the function takes one parameter - the SVGWrapper instance
+	this.regional = []; // Localisations, indexed by language, '' for default (English)
+	this.regional[''] = {errorLoadingText: 'Error loading',
+		notSupportedText: 'This browser does not support SVG'};
+	this.local = this.regional['']; // Current localisation
+	this._uuid = new Date().getTime();
+	this._renesis = detectActiveX('RenesisX.RenesisCtrl');
+}
+
+/* Determine whether a given ActiveX control is available.
+   @param  classId  (string) the ID for the ActiveX control
+   @return  (boolean) true if found, false if not */
+function detectActiveX(classId) {
+	try {
+		return !!(window.ActiveXObject && new ActiveXObject(classId));
+	}
+	catch (e) {
+		return false;
+	}
+}
+
+var PROP_NAME = 'svgwrapper';
+
+$.extend(SVGManager.prototype, {
+	/* Class name added to elements to indicate already configured with SVG. */
+	markerClassName: 'hasSVG',
+
+	/* SVG namespace. */
+	svgNS: 'http://www.w3.org/2000/svg',
+	/* XLink namespace. */
+	xlinkNS: 'http://www.w3.org/1999/xlink',
+
+	/* SVG wrapper class. */
+	_wrapperClass: SVGWrapper,
+
+	/* Camel-case versions of attribute names containing dashes or are reserved words. */
+	_attrNames: {class_: 'class', in_: 'in',
+		alignmentBaseline: 'alignment-baseline', baselineShift: 'baseline-shift',
+		clipPath: 'clip-path', clipRule: 'clip-rule',
+		colorInterpolation: 'color-interpolation',
+		colorInterpolationFilters: 'color-interpolation-filters',
+		colorRendering: 'color-rendering', dominantBaseline: 'dominant-baseline',
+		enableBackground: 'enable-background', fillOpacity: 'fill-opacity',
+		fillRule: 'fill-rule', floodColor: 'flood-color',
+		floodOpacity: 'flood-opacity', fontFamily: 'font-family',
+		fontSize: 'font-size', fontSizeAdjust: 'font-size-adjust',
+		fontStretch: 'font-stretch', fontStyle: 'font-style',
+		fontVariant: 'font-variant', fontWeight: 'font-weight',
+		glyphOrientationHorizontal: 'glyph-orientation-horizontal',
+		glyphOrientationVertical: 'glyph-orientation-vertical',
+		horizAdvX: 'horiz-adv-x', horizOriginX: 'horiz-origin-x',
+		imageRendering: 'image-rendering', letterSpacing: 'letter-spacing',
+		lightingColor: 'lighting-color', markerEnd: 'marker-end',
+		markerMid: 'marker-mid', markerStart: 'marker-start',
+		stopColor: 'stop-color', stopOpacity: 'stop-opacity',
+		strikethroughPosition: 'strikethrough-position',
+		strikethroughThickness: 'strikethrough-thickness',
+		strokeDashArray: 'stroke-dasharray', strokeDashOffset: 'stroke-dashoffset',
+		strokeLineCap: 'stroke-linecap', strokeLineJoin: 'stroke-linejoin',
+		strokeMiterLimit: 'stroke-miterlimit', strokeOpacity: 'stroke-opacity',
+		strokeWidth: 'stroke-width', textAnchor: 'text-anchor',
+		textDecoration: 'text-decoration', textRendering: 'text-rendering',
+		underlinePosition: 'underline-position', underlineThickness: 'underline-thickness',
+		vertAdvY: 'vert-adv-y', vertOriginY: 'vert-origin-y',
+		wordSpacing: 'word-spacing', writingMode: 'writing-mode'},
+
+	/* Add the SVG object to its container. */
+	_attachSVG: function(container, settings) {
+		var svg = (container.namespaceURI == this.svgNS ? container : null);
+		var container = (svg ? null : container);
+		if ($(container || svg).hasClass(this.markerClassName)) {
+			return;
+		}
+		if (typeof settings == 'string') {
+			settings = {loadURL: settings};
+		}
+		else if (typeof settings == 'function') {
+			settings = {onLoad: settings};
+		}
+		$(container || svg).addClass(this.markerClassName);
+		try {
+			if (!svg) {
+				svg = document.createElementNS(this.svgNS, 'svg');
+				svg.setAttribute('version', '1.1');
+				if (container.clientWidth > 0) {
+					svg.setAttribute('width', container.clientWidth);
+				}
+				if (container.clientHeight > 0) {
+					svg.setAttribute('height', container.clientHeight);
+				}
+				container.appendChild(svg);
+			}
+			this._afterLoad(container, svg, settings || {});
+		}
+		catch (e) {
+			if ($.browser.msie) {
+				if (!container.id) {
+					container.id = 'svg' + (this._uuid++);
+				}
+				this._settings[container.id] = settings;
+				container.innerHTML = '<embed type="image/svg+xml" width="100%" ' +
+					'height="100%" src="' + (settings.initPath || '') + 'blank.svg" ' +
+					'pluginspage="http://www.adobe.com/svg/viewer/install/main.html"/>';
+			}
+			else {
+				container.innerHTML = '<p class="svg_error">' +
+					this.local.notSupportedText + '</p>';
+			}
+		}
+	},
+
+	/* SVG callback after loading - register SVG root. */
+	_registerSVG: function() {
+		for (var i = 0; i < document.embeds.length; i++) { // Check all
+			var container = document.embeds[i].parentNode;
+			if (!$(container).hasClass($.svg.markerClassName) || // Not SVG
+					$.data(container, PROP_NAME)) { // Already done
+				continue;
+			}
+			var svg = null;
+			try {
+				svg = document.embeds[i].getSVGDocument();
+			}
+			catch(e) {
+				setTimeout($.svg._registerSVG, 250); // Renesis takes longer to load
+				return;
+			}
+			svg = (svg ? svg.documentElement : null);
+			if (svg) {
+				$.svg._afterLoad(container, svg);
+			}
+		}
+	},
+
+	/* Post-processing once loaded. */
+	_afterLoad: function(container, svg, settings) {
+		var settings = settings || this._settings[container.id];
+		this._settings[container ? container.id : ''] = null;
+		var wrapper = new this._wrapperClass(svg, container);
+		$.data(container || svg, PROP_NAME, wrapper);
+		try {
+			if (settings.loadURL) { // Load URL
+				wrapper.load(settings.loadURL, settings);
+			}
+			if (settings.settings) { // Additional settings
+				wrapper.configure(settings.settings);
+			}
+			if (settings.onLoad && !settings.loadURL) { // Onload callback
+				settings.onLoad.apply(container || svg, [wrapper]);
+			}
+		}
+		catch (e) {
+			alert(e);
+		}
+	},
+
+	/* Return the SVG wrapper created for a given container.
+	   @param  container  (string) selector for the container or
+	                      (element) the container for the SVG object or
+	                      jQuery collection - first entry is the container
+	   @return  (SVGWrapper) the corresponding SVG wrapper element, or null if not attached */
+	_getSVG: function(container) {
+		container = (typeof container == 'string' ? $(container)[0] :
+			(container.jquery ? container[0] : container));
+		return $.data(container, PROP_NAME);
+	},
+
+	/* Remove the SVG functionality from a div.
+	   @param  container  (element) the container for the SVG object */
+	_destroySVG: function(container) {
+		var $container = $(container);
+		if (!$container.hasClass(this.markerClassName)) {
+			return;
+		}
+		$container.removeClass(this.markerClassName);
+		if (container.namespaceURI != this.svgNS) {
+			$container.empty();
+		}
+		$.removeData(container, PROP_NAME);
+	},
+
+	/* Extend the SVGWrapper object with an embedded class.
+	   The constructor function must take a single parameter that is
+	   a reference to the owning SVG root object. This allows the 
+	   extension to access the basic SVG functionality.
+	   @param  name      (string) the name of the SVGWrapper attribute to access the new class
+	   @param  extClass  (function) the extension class constructor */
+	addExtension: function(name, extClass) {
+		this._extensions.push([name, extClass]);
+	},
+
+	/* Does this node belong to SVG?
+	   @param  node  (element) the node to be tested
+	   @return  (boolean) true if an SVG node, false if not */
+	isSVGElem: function(node) {
+		return (node.nodeType == 1 && node.namespaceURI == $.svg.svgNS);
+	}
+});
+
+/* The main SVG interface, which encapsulates the SVG element.
+   Obtain a reference from $().svg('get') */
+function SVGWrapper(svg, container) {
+	this._svg = svg; // The SVG root node
+	this._container = container; // The containing div
+	for (var i = 0; i < $.svg._extensions.length; i++) {
+		var extension = $.svg._extensions[i];
+		this[extension[0]] = new extension[1](this);
+	}
+}
+
+$.extend(SVGWrapper.prototype, {
+
+	/* Retrieve the width of the SVG object. */
+	_width: function() {
+		return (this._container ? this._container.clientWidth : this._svg.width);
+	},
+
+	/* Retrieve the height of the SVG object. */
+	_height: function() {
+		return (this._container ? this._container.clientHeight : this._svg.height);
+	},
+
+	/* Retrieve the root SVG element.
+	   @return  the top-level SVG element */
+	root: function() {
+		return this._svg;
+	},
+
+	/* Configure a SVG node.
+	   @param  node      (element, optional) the node to configure
+	   @param  settings  (object) additional settings for the root
+	   @param  clear     (boolean) true to remove existing attributes first,
+	                     false to add to what is already there (optional)
+	   @return  (SVGWrapper) this root */
+	configure: function(node, settings, clear) {
+		if (!node.nodeName) {
+			clear = settings;
+			settings = node;
+			node = this._svg;
+		}
+		if (clear) {
+			for (var i = node.attributes.length - 1; i >= 0; i--) {
+				var attr = node.attributes.item(i);
+				if (!(attr.nodeName == 'onload' || attr.nodeName == 'version' || 
+						attr.nodeName.substring(0, 5) == 'xmlns')) {
+					node.attributes.removeNamedItem(attr.nodeName);
+				}
+			}
+		}
+		for (var attrName in settings) {
+			node.setAttribute($.svg._attrNames[attrName] || attrName, settings[attrName]);
+		}
+		return this;
+	},
+
+	/* Locate a specific element in the SVG document.
+	   @param  id  (string) the element's identifier
+	   @return  (element) the element reference, or null if not found */
+	getElementById: function(id) {
+		return this._svg.ownerDocument.getElementById(id);
+	},
+
+	/* Change the attributes for a SVG node.
+	   @param  element   (SVG element) the node to change
+	   @param  settings  (object) the new settings
+	   @return  (SVGWrapper) this root */
+	change: function(element, settings) {
+		if (element) {
+			for (var name in settings) {
+				if (settings[name] == null) {
+					element.removeAttribute($.svg._attrNames[name] || name);
+				}
+				else {
+					element.setAttribute($.svg._attrNames[name] || name, settings[name]);
+				}
+			}
+		}
+		return this;
+	},
+
+	/* Check for parent being absent and adjust arguments accordingly. */
+	_args: function(values, names, optSettings) {
+		names.splice(0, 0, 'parent');
+		names.splice(names.length, 0, 'settings');
+		var args = {};
+		var offset = 0;
+		if (values[0] != null && values[0].jquery) {
+			values[0] = values[0][0];
+		}
+		if (values[0] != null && !(typeof values[0] == 'object' && values[0].nodeName)) {
+			args['parent'] = null;
+			offset = 1;
+		}
+		for (var i = 0; i < values.length; i++) {
+			args[names[i + offset]] = values[i];
+		}
+		if (optSettings) {
+			$.each(optSettings, function(i, value) {
+				if (typeof args[value] == 'object') {
+					args.settings = args[value];
+					args[value] = null;
+				}
+			});
+		}
+		return args;
+	},
+
+	/* Add a title.
+	   @param  parent    (element or jQuery) the parent node for the new title (optional)
+	   @param  text      (string) the text of the title
+	   @param  settings  (object) additional settings for the title (optional)
+	   @return  (element) the new title node */
+	title: function(parent, text, settings) {
+		var args = this._args(arguments, ['text']);
+		var node = this._makeNode(args.parent, 'title', args.settings || {});
+		node.appendChild(this._svg.ownerDocument.createTextNode(args.text));
+		return node;
+	},
+
+	/* Add a description.
+	   @param  parent    (element or jQuery) the parent node for the new description (optional)
+	   @param  text      (string) the text of the description
+	   @param  settings  (object) additional settings for the description (optional)
+	   @return  (element) the new description node */
+	describe: function(parent, text, settings) {
+		var args = this._args(arguments, ['text']);
+		var node = this._makeNode(args.parent, 'desc', args.settings || {});
+		node.appendChild(this._svg.ownerDocument.createTextNode(args.text));
+		return node;
+	},
+
+	/* Add a definitions node.
+	   @param  parent    (element or jQuery) the parent node for the new definitions (optional)
+	   @param  id        (string) the ID of this definitions (optional)
+	   @param  settings  (object) additional settings for the definitions (optional)
+	   @return  (element) the new definitions node */
+	defs: function(parent, id, settings) {
+		var args = this._args(arguments, ['id'], ['id']);
+		return this._makeNode(args.parent, 'defs', $.extend(
+			(args.id ? {id: args.id} : {}), args.settings || {}));
+	},
+
+	/* Add a symbol definition.
+	   @param  parent    (element or jQuery) the parent node for the new symbol (optional)
+	   @param  id        (string) the ID of this symbol
+	   @param  x1        (number) the left coordinate for this symbol
+	   @param  y1        (number) the top coordinate for this symbol
+	   @param  width     (number) the width of this symbol
+	   @param  height    (number) the height of this symbol
+	   @param  settings  (object) additional settings for the symbol (optional)
+	   @return  (element) the new symbol node */
+	symbol: function(parent, id, x1, y1, width, height, settings) {
+		var args = this._args(arguments, ['id', 'x1', 'y1', 'width', 'height']);
+		return this._makeNode(args.parent, 'symbol', $.extend({id: args.id,
+			viewBox: args.x1 + ' ' + args.y1 + ' ' + args.width + ' ' + args.height},
+			args.settings || {}));
+	},
+
+	/* Add a marker definition.
+	   @param  parent    (element or jQuery) the parent node for the new marker (optional)
+	   @param  id        (string) the ID of this marker
+	   @param  refX      (number) the x-coordinate for the reference point
+	   @param  refY      (number) the y-coordinate for the reference point
+	   @param  mWidth    (number) the marker viewport width
+	   @param  mHeight   (number) the marker viewport height
+	   @param  orient    (string or int) 'auto' or angle (degrees) (optional)
+	   @param  settings  (object) additional settings for the marker (optional)
+	   @return  (element) the new marker node */
+	marker: function(parent, id, refX, refY, mWidth, mHeight, orient, settings) {
+		var args = this._args(arguments, ['id', 'refX', 'refY',
+			'mWidth', 'mHeight', 'orient'], ['orient']);
+		return this._makeNode(args.parent, 'marker', $.extend(
+			{id: args.id, refX: args.refX, refY: args.refY, markerWidth: args.mWidth, 
+			markerHeight: args.mHeight, orient: args.orient || 'auto'}, args.settings || {}));
+	},
+
+	/* Add a style node.
+	   @param  parent    (element or jQuery) the parent node for the new node (optional)
+	   @param  styles    (string) the CSS styles
+	   @param  settings  (object) additional settings for the node (optional)
+	   @return  (element) the new style node */
+	style: function(parent, styles, settings) {
+		var args = this._args(arguments, ['styles']);
+		var node = this._makeNode(args.parent, 'style', $.extend(
+			{type: 'text/css'}, args.settings || {}));
+		node.appendChild(this._svg.ownerDocument.createTextNode(args.styles));
+		if ($.browser.opera) {
+			$('head').append('<style type="text/css">' + args.styles + '</style>');
+		}
+		return node;
+	},
+
+	/* Add a script node.
+	   @param  parent    (element or jQuery) the parent node for the new node (optional)
+	   @param  script    (string) the JavaScript code
+	   @param  type      (string) the MIME type for the code (optional, default 'text/javascript')
+	   @param  settings  (object) additional settings for the node (optional)
+	   @return  (element) the new script node */
+	script: function(parent, script, type, settings) {
+		var args = this._args(arguments, ['script', 'type'], ['type']);
+		var node = this._makeNode(args.parent, 'script', $.extend(
+			{type: args.type || 'text/javascript'}, args.settings || {}));
+		node.appendChild(this._svg.ownerDocument.createTextNode(args.script));
+		if (!$.browser.mozilla) {
+			$.globalEval(args.script);
+		}
+		return node;
+	},
+
+	/* Add a linear gradient definition.
+	   Specify all of x1, y1, x2, y2 or none of them.
+	   @param  parent    (element or jQuery) the parent node for the new gradient (optional)
+	   @param  id        (string) the ID for this gradient
+	   @param  stops     (string[][]) the gradient stops, each entry is
+	                     [0] is offset (0.0-1.0 or 0%-100%), [1] is colour, 
+						 [2] is opacity (optional)
+	   @param  x1        (number) the x-coordinate of the gradient start (optional)
+	   @param  y1        (number) the y-coordinate of the gradient start (optional)
+	   @param  x2        (number) the x-coordinate of the gradient end (optional)
+	   @param  y2        (number) the y-coordinate of the gradient end (optional)
+	   @param  settings  (object) additional settings for the gradient (optional)
+	   @return  (element) the new gradient node */
+	linearGradient: function(parent, id, stops, x1, y1, x2, y2, settings) {
+		var args = this._args(arguments,
+			['id', 'stops', 'x1', 'y1', 'x2', 'y2'], ['x1']);
+		var sets = $.extend({id: args.id}, 
+			(args.x1 != null ? {x1: args.x1, y1: args.y1, x2: args.x2, y2: args.y2} : {}));
+		return this._gradient(args.parent, 'linearGradient', 
+			$.extend(sets, args.settings || {}), args.stops);
+	},
+
+	/* Add a radial gradient definition.
+	   Specify all of cx, cy, r, fx, fy or none of them.
+	   @param  parent    (element or jQuery) the parent node for the new gradient (optional)
+	   @param  id        (string) the ID for this gradient
+	   @param  stops     (string[][]) the gradient stops, each entry
+	                     [0] is offset, [1] is colour, [2] is opacity (optional)
+	   @param  cx        (number) the x-coordinate of the largest circle centre (optional)
+	   @param  cy        (number) the y-coordinate of the largest circle centre (optional)
+	   @param  r         (number) the radius of the largest circle (optional)
+	   @param  fx        (number) the x-coordinate of the gradient focus (optional)
+	   @param  fy        (number) the y-coordinate of the gradient focus (optional)
+	   @param  settings  (object) additional settings for the gradient (optional)
+	   @return  (element) the new gradient node */
+	radialGradient: function(parent, id, stops, cx, cy, r, fx, fy, settings) {
+		var args = this._args(arguments,
+			['id', 'stops', 'cx', 'cy', 'r', 'fx', 'fy'], ['cx']);
+		var sets = $.extend({id: args.id}, (args.cx != null ?
+			{cx: args.cx, cy: args.cy, r: args.r, fx: args.fx, fy: args.fy} : {}));
+		return this._gradient(args.parent, 'radialGradient', 
+			$.extend(sets, args.settings || {}), args.stops);
+	},
+
+	/* Add a gradient node. */
+	_gradient: function(parent, name, settings, stops) {
+		var node = this._makeNode(parent, name, settings);
+		for (var i = 0; i < stops.length; i++) {
+			var stop = stops[i];
+			this._makeNode(node, 'stop', $.extend(
+				{offset: stop[0], stopColor: stop[1]}, 
+				(stop[2] != null ? {stopOpacity: stop[2]} : {})));
+		}
+		return node;
+	},
+
+	/* Add a pattern definition.
+	   Specify all of vx, vy, xwidth, vheight or none of them.
+	   @param  parent    (element or jQuery) the parent node for the new pattern (optional)
+	   @param  id        (string) the ID for this pattern
+	   @param  x         (number) the x-coordinate for the left edge of the pattern
+	   @param  y         (number) the y-coordinate for the top edge of the pattern
+	   @param  width     (number) the width of the pattern
+	   @param  height    (number) the height of the pattern
+	   @param  vx        (number) the minimum x-coordinate for view box (optional)
+	   @param  vy        (number) the minimum y-coordinate for the view box (optional)
+	   @param  vwidth    (number) the width of the view box (optional)
+	   @param  vheight   (number) the height of the view box (optional)
+	   @param  settings  (object) additional settings for the pattern (optional)
+	   @return  (element) the new pattern node */
+	pattern: function(parent, id, x, y, width, height, vx, vy, vwidth, vheight, settings) {
+		var args = this._args(arguments, ['id', 'x', 'y', 'width', 'height',
+			'vx', 'vy', 'vwidth', 'vheight'], ['vx']);
+		var sets = $.extend({id: args.id, x: args.x, y: args.y,
+			width: args.width, height: args.height}, (args.vx != null ?
+			{viewBox: args.vx + ' ' + args.vy + ' ' + args.vwidth + ' ' + args.vheight} : {}));
+		return this._makeNode(args.parent, 'pattern', $.extend(sets, args.settings || {}));
+	},
+
+	/* Add a clip path definition.
+	   @param  parent  (element) the parent node for the new element (optional)
+	   @param  id      (string) the ID for this path
+	   @param  units   (string) either 'userSpaceOnUse' (default) or 'objectBoundingBox' (optional)
+	   @return  (element) the new clipPath node */
+	clipPath: function(parent, id, units, settings) {
+		var args = this._args(arguments, ['id', 'units']);
+		args.units = args.units || 'userSpaceOnUse';
+		return this._makeNode(args.parent, 'clipPath', $.extend(
+			{id: args.id, clipPathUnits: args.units}, args.settings || {}));
+	},
+
+	/* Add a mask definition.
+	   @param  parent    (element or jQuery) the parent node for the new mask (optional)
+	   @param  id        (string) the ID for this mask
+	   @param  x         (number) the x-coordinate for the left edge of the mask
+	   @param  y         (number) the y-coordinate for the top edge of the mask
+	   @param  width     (number) the width of the mask
+	   @param  height    (number) the height of the mask
+	   @param  settings  (object) additional settings for the mask (optional)
+	   @return  (element) the new mask node */
+	mask: function(parent, id, x, y, width, height, settings) {
+		var args = this._args(arguments, ['id', 'x', 'y', 'width', 'height']);
+		return this._makeNode(args.parent, 'mask', $.extend(
+			{id: args.id, x: args.x, y: args.y, width: args.width, height: args.height},
+			args.settings || {}));
+	},
+
+	/* Create a new path object.
+	   @return  (SVGPath) a new path object */
+	createPath: function() {
+		return new SVGPath();
+	},
+
+	/* Create a new text object.
+	   @return  (SVGText) a new text object */
+	createText: function() {
+		return new SVGText();
+	},
+
+	/* Add an embedded SVG element.
+	   Specify all of vx, vy, vwidth, vheight or none of them.
+	   @param  parent    (element or jQuery) the parent node for the new node (optional)
+	   @param  x         (number) the x-coordinate for the left edge of the node
+	   @param  y         (number) the y-coordinate for the top edge of the node
+	   @param  width     (number) the width of the node
+	   @param  height    (number) the height of the node
+	   @param  vx        (number) the minimum x-coordinate for view box (optional)
+	   @param  vy        (number) the minimum y-coordinate for the view box (optional)
+	   @param  vwidth    (number) the width of the view box (optional)
+	   @param  vheight   (number) the height of the view box (optional)
+	   @param  settings  (object) additional settings for the node (optional)
+	   @return  (element) the new node */
+	svg: function(parent, x, y, width, height, vx, vy, vwidth, vheight, settings) {
+		var args = this._args(arguments, ['x', 'y', 'width', 'height',
+			'vx', 'vy', 'vwidth', 'vheight'], ['vx']);
+		var sets = $.extend({x: args.x, y: args.y, width: args.width, height: args.height}, 
+			(args.vx != null ? {viewBox: args.vx + ' ' + args.vy + ' ' +
+			args.vwidth + ' ' + args.vheight} : {}));
+		return this._makeNode(args.parent, 'svg', $.extend(sets, args.settings || {}));
+	},
+
+	/* Create a group.
+	   @param  parent    (element or jQuery) the parent node for the new group (optional)
+	   @param  id        (string) the ID of this group (optional)
+	   @param  settings  (object) additional settings for the group (optional)
+	   @return  (element) the new group node */
+	group: function(parent, id, settings) {
+		var args = this._args(arguments, ['id'], ['id']);
+		return this._makeNode(args.parent, 'g', $.extend({id: args.id}, args.settings || {}));
+	},
+
+	/* Add a usage reference.
+	   Specify all of x, y, width, height or none of them.
+	   @param  parent    (element or jQuery) the parent node for the new node (optional)
+	   @param  x         (number) the x-coordinate for the left edge of the node (optional)
+	   @param  y         (number) the y-coordinate for the top edge of the node (optional)
+	   @param  width     (number) the width of the node (optional)
+	   @param  height    (number) the height of the node (optional)
+	   @param  ref       (string) the ID of the definition node
+	   @param  settings  (object) additional settings for the node (optional)
+	   @return  (element) the new node */
+	use: function(parent, x, y, width, height, ref, settings) {
+		var args = this._args(arguments, ['x', 'y', 'width', 'height', 'ref']);
+		if (typeof args.x == 'string') {
+			args.ref = args.x;
+			args.settings = args.y;
+			args.x = args.y = args.width = args.height = null;
+		}
+		var node = this._makeNode(args.parent, 'use', $.extend(
+			{x: args.x, y: args.y, width: args.width, height: args.height},
+			args.settings || {}));
+		node.setAttributeNS($.svg.xlinkNS, 'href', args.ref);
+		return node;
+	},
+
+	/* Add a link, which applies to all child elements.
+	   @param  parent    (element or jQuery) the parent node for the new link (optional)
+	   @param  ref       (string) the target URL
+	   @param  settings  (object) additional settings for the link (optional)
+	   @return  (element) the new link node */
+	link: function(parent, ref, settings) {
+		var args = this._args(arguments, ['ref']);
+		var node = this._makeNode(args.parent, 'a', args.settings);
+		node.setAttributeNS($.svg.xlinkNS, 'href', args.ref);
+		return node;
+	},
+
+	/* Add an image.
+	   @param  parent    (element or jQuery) the parent node for the new image (optional)
+	   @param  x         (number) the x-coordinate for the left edge of the image
+	   @param  y         (number) the y-coordinate for the top edge of the image
+	   @param  width     (number) the width of the image
+	   @param  height    (number) the height of the image
+	   @param  ref       (string) the path to the image
+	   @param  settings  (object) additional settings for the image (optional)
+	   @return  (element) the new image node */
+	image: function(parent, x, y, width, height, ref, settings) {
+		var args = this._args(arguments, ['x', 'y', 'width', 'height', 'ref']);
+		var node = this._makeNode(args.parent, 'image', $.extend(
+			{x: args.x, y: args.y, width: args.width, height: args.height},
+			args.settings || {}));
+		node.setAttributeNS($.svg.xlinkNS, 'href', args.ref);
+		return node;
+	},
+
+	/* Draw a path.
+	   @param  parent    (element or jQuery) the parent node for the new shape (optional)
+	   @param  path      (string or SVGPath) the path to draw
+	   @param  settings  (object) additional settings for the shape (optional)
+	   @return  (element) the new shape node */
+	path: function(parent, path, settings) {
+		var args = this._args(arguments, ['path']);
+		return this._makeNode(args.parent, 'path', $.extend(
+			{d: (args.path.path ? args.path.path() : args.path)}, args.settings || {}));
+	},
+
+	/* Draw a rectangle.
+	   Specify both of rx and ry or neither.
+	   @param  parent    (element or jQuery) the parent node for the new shape (optional)
+	   @param  x         (number) the x-coordinate for the left edge of the rectangle
+	   @param  y         (number) the y-coordinate for the top edge of the rectangle
+	   @param  width     (number) the width of the rectangle
+	   @param  height    (number) the height of the rectangle
+	   @param  rx        (number) the x-radius of the ellipse for the rounded corners (optional)
+	   @param  ry        (number) the y-radius of the ellipse for the rounded corners (optional)
+	   @param  settings  (object) additional settings for the shape (optional)
+	   @return  (element) the new shape node */
+	rect: function(parent, x, y, width, height, rx, ry, settings) {
+		var args = this._args(arguments, ['x', 'y', 'width', 'height', 'rx', 'ry'], ['rx']);
+		return this._makeNode(args.parent, 'rect', $.extend(
+			{x: args.x, y: args.y, width: args.width, height: args.height},
+			(args.rx ? {rx: args.rx, ry: args.ry} : {}), args.settings || {}));
+	},
+
+	/* Draw a circle.
+	   @param  parent    (element or jQuery) the parent node for the new shape (optional)
+	   @param  cx        (number) the x-coordinate for the centre of the circle
+	   @param  cy        (number) the y-coordinate for the centre of the circle
+	   @param  r         (number) the radius of the circle
+	   @param  settings  (object) additional settings for the shape (optional)
+	   @return  (element) the new shape node */
+	circle: function(parent, cx, cy, r, settings) {
+		var args = this._args(arguments, ['cx', 'cy', 'r']);
+		return this._makeNode(args.parent, 'circle', $.extend(
+			{cx: args.cx, cy: args.cy, r: args.r}, args.settings || {}));
+	},
+
+	/* Draw an ellipse.
+	   @param  parent    (element or jQuery) the parent node for the new shape (optional)
+	   @param  cx        (number) the x-coordinate for the centre of the ellipse
+	   @param  cy        (number) the y-coordinate for the centre of the ellipse
+	   @param  rx        (number) the x-radius of the ellipse
+	   @param  ry        (number) the y-radius of the ellipse
+	   @param  settings  (object) additional settings for the shape (optional)
+	   @return  (element) the new shape node */
+	ellipse: function(parent, cx, cy, rx, ry, settings) {
+		var args = this._args(arguments, ['cx', 'cy', 'rx', 'ry']);
+		return this._makeNode(args.parent, 'ellipse', $.extend(
+			{cx: args.cx, cy: args.cy, rx: args.rx, ry: args.ry}, args.settings || {}));
+	},
+
+	/* Draw a line.
+	   @param  parent    (element or jQuery) the parent node for the new shape (optional)
+	   @param  x1        (number) the x-coordinate for the start of the line
+	   @param  y1        (number) the y-coordinate for the start of the line
+	   @param  x2        (number) the x-coordinate for the end of the line
+	   @param  y2        (number) the y-coordinate for the end of the line
+	   @param  settings  (object) additional settings for the shape (optional)
+	   @return  (element) the new shape node */
+	line: function(parent, x1, y1, x2, y2, settings) {
+		var args = this._args(arguments, ['x1', 'y1', 'x2', 'y2']);
+		return this._makeNode(args.parent, 'line', $.extend(
+			{x1: args.x1, y1: args.y1, x2: args.x2, y2: args.y2}, args.settings || {}));
+	},
+
+	/* Draw a polygonal line.
+	   @param  parent    (element or jQuery) the parent node for the new shape (optional)
+	   @param  points    (number[][]) the x-/y-coordinates for the points on the line
+	   @param  settings  (object) additional settings for the shape (optional)
+	   @return  (element) the new shape node */
+	polyline: function(parent, points, settings) {
+		var args = this._args(arguments, ['points']);
+		return this._poly(args.parent, 'polyline', args.points, args.settings);
+	},
+
+	/* Draw a polygonal shape.
+	   @param  parent    (element or jQuery) the parent node for the new shape (optional)
+	   @param  points    (number[][]) the x-/y-coordinates for the points on the shape
+	   @param  settings  (object) additional settings for the shape (optional)
+	   @return  (element) the new shape node */
+	polygon: function(parent, points, settings) {
+		var args = this._args(arguments, ['points']);
+		return this._poly(args.parent, 'polygon', args.points, args.settings);
+	},
+
+	/* Draw a polygonal line or shape. */
+	_poly: function(parent, name, points, settings) {
+		var ps = '';
+		for (var i = 0; i < points.length; i++) {
+			ps += points[i].join() + ' ';
+		}
+		return this._makeNode(parent, name, $.extend(
+			{points: $.trim(ps)}, settings || {}));
+	},
+
+	/* Draw text.
+	   Specify both of x and y or neither of them.
+	   @param  parent    (element or jQuery) the parent node for the text (optional)
+	   @param  x         (number or number[]) the x-coordinate(s) for the text (optional)
+	   @param  y         (number or number[]) the y-coordinate(s) for the text (optional)
+	   @param  value     (string) the text content or
+	                     (SVGText) text with spans and references
+	   @param  settings  (object) additional settings for the text (optional)
+	   @return  (element) the new text node */
+	text: function(parent, x, y, value, settings) {
+		var args = this._args(arguments, ['x', 'y', 'value']);
+		if (typeof args.x == 'string' && arguments.length < 4) {
+			args.value = args.x;
+			args.settings = args.y;
+			args.x = args.y = null;
+		}
+		return this._text(args.parent, 'text', args.value, $.extend(
+			{x: (args.x && isArray(args.x) ? args.x.join(' ') : args.x),
+			y: (args.y && isArray(args.y) ? args.y.join(' ') : args.y)}, 
+			args.settings || {}));
+	},
+
+	/* Draw text along a path.
+	   @param  parent    (element or jQuery) the parent node for the text (optional)
+	   @param  path      (string) the ID of the path
+	   @param  value     (string) the text content or
+	                     (SVGText) text with spans and references
+	   @param  settings  (object) additional settings for the text (optional)
+	   @return  (element) the new text node */
+	textpath: function(parent, path, value, settings) {
+		var args = this._args(arguments, ['path', 'value']);
+		var node = this._text(args.parent, 'textPath', args.value, args.settings || {});
+		node.setAttributeNS($.svg.xlinkNS, 'href', args.path);
+		return node;
+	},
+
+	/* Draw text. */
+	_text: function(parent, name, value, settings) {
+		var node = this._makeNode(parent, name, settings);
+		if (typeof value == 'string') {
+			node.appendChild(node.ownerDocument.createTextNode(value));
+		}
+		else {
+			for (var i = 0; i < value._parts.length; i++) {
+				var part = value._parts[i];
+				if (part[0] == 'tspan') {
+					var child = this._makeNode(node, part[0], part[2]);
+					child.appendChild(node.ownerDocument.createTextNode(part[1]));
+					node.appendChild(child);
+				}
+				else if (part[0] == 'tref') {
+					var child = this._makeNode(node, part[0], part[2]);
+					child.setAttributeNS($.svg.xlinkNS, 'href', part[1]);
+					node.appendChild(child);
+				}
+				else if (part[0] == 'textpath') {
+					var set = $.extend({}, part[2]);
+					set.href = null;
+					var child = this._makeNode(node, part[0], set);
+					child.setAttributeNS($.svg.xlinkNS, 'href', part[2].href);
+					child.appendChild(node.ownerDocument.createTextNode(part[1]));
+					node.appendChild(child);
+				}
+				else { // straight text
+					node.appendChild(node.ownerDocument.createTextNode(part[1]));
+				}
+			}
+		}
+		return node;
+	},
+
+	/* Add a custom SVG element.
+	   @param  parent    (element or jQuery) the parent node for the new element (optional)
+	   @param  name      (string) the name of the element
+	   @param  settings  (object) additional settings for the element (optional)
+	   @return  (element) the new custom node */
+	other: function(parent, name, settings) {
+		var args = this._args(arguments, ['name']);
+		return this._makeNode(args.parent, args.name, args.settings || {});
+	},
+
+	/* Create a shape node with the given settings. */
+	_makeNode: function(parent, name, settings) {
+		parent = parent || this._svg;
+		var node = this._svg.ownerDocument.createElementNS($.svg.svgNS, name);
+		for (var name in settings) {
+			var value = settings[name];
+			if (value != null && value != null && 
+					(typeof value != 'string' || value != '')) {
+				node.setAttribute($.svg._attrNames[name] || name, value);
+			}
+		}
+		parent.appendChild(node);
+		return node;
+	},
+
+	/* Add an existing SVG node to the diagram.
+	   @param  parent  (element or jQuery) the parent node for the new node (optional)
+	   @param  node    (element) the new node to add or
+	                   (string) the jQuery selector for the node or
+	                   (jQuery collection) set of nodes to add
+	   @return  (SVGWrapper) this wrapper */
+	add: function(parent, node) {
+		var args = this._args((arguments.length == 1 ? [null, parent] : arguments), ['node']);
+		var svg = this;
+		args.parent = args.parent || this._svg;
+		args.node = (args.node.jquery ? args.node : $(args.node));
+		try {
+			if ($.svg._renesis) {
+				throw 'Force traversal';
+			}
+			args.parent.appendChild(args.node.cloneNode(true));
+		}
+		catch (e) {
+			args.node.each(function() {
+				var child = svg._cloneAsSVG(this);
+				if (child) {
+					args.parent.appendChild(child);
+				}
+			});
+		}
+		return this;
+	},
+
+	/* Clone an existing SVG node and add it to the diagram.
+	   @param  parent  (element or jQuery) the parent node for the new node (optional)
+	   @param  node    (element) the new node to add or
+	                   (string) the jQuery selector for the node or
+	                   (jQuery collection) set of nodes to add
+	   @return  (element[]) collection of new nodes */
+	clone: function(parent, node) {
+		var svg = this;
+		var args = this._args((arguments.length == 1 ? [null, parent] : arguments), ['node']);
+		args.parent = args.parent || this._svg;
+		args.node = (args.node.jquery ? args.node : $(args.node));
+		var newNodes = [];
+		args.node.each(function() {
+			var child = svg._cloneAsSVG(this);
+			if (child) {
+				child.id = '';
+				args.parent.appendChild(child);
+				newNodes.push(child);
+			}
+		});
+		return newNodes;
+	},
+
+	/* SVG nodes must belong to the SVG namespace, so clone and ensure this is so.
+	   @param  node  (element) the SVG node to clone
+	   @return  (element) the cloned node */
+	_cloneAsSVG: function(node) {
+		var newNode = null;
+		if (node.nodeType == 1) { // element
+			newNode = this._svg.ownerDocument.createElementNS(
+				$.svg.svgNS, this._checkName(node.nodeName));
+			for (var i = 0; i < node.attributes.length; i++) {
+				var attr = node.attributes.item(i);
+				if (attr.nodeName != 'xmlns' && attr.nodeValue) {
+					if (attr.prefix == 'xlink') {
+						newNode.setAttributeNS($.svg.xlinkNS,
+							attr.localName || attr.baseName, attr.nodeValue);
+					}
+					else {
+						newNode.setAttribute(this._checkName(attr.nodeName), attr.nodeValue);
+					}
+				}
+			}
+			for (var i = 0; i < node.childNodes.length; i++) {
+				var child = this._cloneAsSVG(node.childNodes[i]);
+				if (child) {
+					newNode.appendChild(child);
+				}
+			}
+		}
+		else if (node.nodeType == 3) { // text
+			if ($.trim(node.nodeValue)) {
+				newNode = this._svg.ownerDocument.createTextNode(node.nodeValue);
+			}
+		}
+		else if (node.nodeType == 4) { // CDATA
+			if ($.trim(node.nodeValue)) {
+				try {
+					newNode = this._svg.ownerDocument.createCDATASection(node.nodeValue);
+				}
+				catch (e) {
+					newNode = this._svg.ownerDocument.createTextNode(
+						node.nodeValue.replace(/&/g, '&amp;').
+						replace(/</g, '&lt;').replace(/>/g, '&gt;'));
+				}
+			}
+		}
+		return newNode;
+	},
+
+	/* Node names must be lower case and without SVG namespace prefix. */
+	_checkName: function(name) {
+		name = (name.substring(0, 1) >= 'A' && name.substring(0, 1) <= 'Z' ?
+			name.toLowerCase() : name);
+		return (name.substring(0, 4) == 'svg:' ? name.substring(4) : name);
+	},
+
+	/* Load an external SVG document.
+	   @param  url       (string) the location of the SVG document or
+	                     the actual SVG content
+	   @param  settings  (boolean) see addTo below or
+	                     (function) see onLoad below or
+	                     (object) additional settings for the load with attributes below:
+	                       addTo       (boolean) true to add to what's already there,
+	                                   or false to clear the canvas first
+						   changeSize  (boolean) true to allow the canvas size to change,
+	                                   or false to retain the original
+	                       onLoad      (function) callback after the document has loaded,
+	                                   'this' is the container, receives SVG object and
+	                                   optional error message as a parameter
+	                       parent      (string or element or jQuery) the parent to load
+	                                   into, defaults to top-level svg element
+	   @return  (SVGWrapper) this root */
+	load: function(url, settings) {
+		settings = (typeof settings == 'boolean' ? {addTo: settings} :
+			(typeof settings == 'function' ? {onLoad: settings} :
+			(typeof settings == 'string' ? {parent: settings} : 
+			(typeof settings == 'object' && settings.nodeName ? {parent: settings} :
+			(typeof settings == 'object' && settings.jquery ? {parent: settings} :
+			settings || {})))));
+		if (!settings.parent && !settings.addTo) {
+			this.clear(false);
+		}
+		var size = [this._svg.getAttribute('width'), this._svg.getAttribute('height')];
+		var wrapper = this;
+		// Report a problem with the load
+		var reportError = function(message) {
+			message = $.svg.local.errorLoadingText + ': ' + message;
+			if (settings.onLoad) {
+				settings.onLoad.apply(wrapper._container || wrapper._svg, [wrapper, message]);
+			}
+			else {
+				wrapper.text(null, 10, 20, message);
+			}
+		};
+		// Create a DOM from SVG content
+		var loadXML4IE = function(data) {
+			var xml = new ActiveXObject('Microsoft.XMLDOM');
+			xml.validateOnParse = false;
+			xml.resolveExternals = false;
+			xml.async = false;
+			xml.loadXML(data);
+			if (xml.parseError.errorCode != 0) {
+				reportError(xml.parseError.reason);
+				return null;
+			}
+			return xml;
+		};
+		// Load the SVG DOM
+		var loadSVG = function(data) {
+			if (!data) {
+				return;
+			}
+			if (data.documentElement.nodeName != 'svg') {
+				var errors = data.getElementsByTagName('parsererror');
+				var messages = (errors.length ? errors[0].getElementsByTagName('div') : []); // Safari
+				reportError(!errors.length ? '???' :
+					(messages.length ? messages[0] : errors[0]).firstChild.nodeValue);
+				return;
+			}
+			var parent = (settings.parent ? $(settings.parent)[0] : wrapper._svg);
+			var attrs = {};
+			for (var i = 0; i < data.documentElement.attributes.length; i++) {
+				var attr = data.documentElement.attributes.item(i);
+				if (!(attr.nodeName == 'version' || attr.nodeName.substring(0, 5) == 'xmlns')) {
+					attrs[attr.nodeName] = attr.nodeValue;
+				}
+			}
+			wrapper.configure(parent, attrs, !settings.parent);
+			var nodes = data.documentElement.childNodes;
+			for (var i = 0; i < nodes.length; i++) {
+				try {
+					if ($.svg._renesis) {
+						throw 'Force traversal';
+					}
+					parent.appendChild(wrapper._svg.ownerDocument.importNode(nodes[i], true));
+					if (nodes[i].nodeName == 'script') {
+						$.globalEval(nodes[i].textContent);
+					}
+				}
+				catch (e) {
+					wrapper.add(parent, nodes[i]);
+				}
+			}
+			if (!settings.changeSize) {
+				wrapper.configure(parent, {width: size[0], height: size[1]});
+			}
+			if (settings.onLoad) {
+				settings.onLoad.apply(wrapper._container || wrapper._svg, [wrapper]);
+			}
+		};
+		if (url.match('<svg')) { // Inline SVG
+			loadSVG($.browser.msie ? loadXML4IE(url) :
+				new DOMParser().parseFromString(url, 'text/xml'));
+		}
+		else { // Remote SVG
+			$.ajax({url: url, dataType: ($.browser.msie ? 'text' : 'xml'),
+				success: function(xml) {
+					loadSVG($.browser.msie ? loadXML4IE(xml) : xml);
+				}, error: function(http, message, exc) {
+					reportError(message + (exc ? ' ' + exc.message : ''));
+				}});
+		}
+		return this;
+	},
+
+	/* Delete a specified node.
+	   @param  node  (element or jQuery) the drawing node to remove
+	   @return  (SVGWrapper) this root */
+	remove: function(node) {
+		node = (node.jquery ? node[0] : node);
+		node.parentNode.removeChild(node);
+		return this;
+	},
+
+	/* Delete everything in the current document.
+	   @param  attrsToo  (boolean) true to clear any root attributes as well,
+	                     false to leave them (optional)
+	   @return  (SVGWrapper) this root */
+	clear: function(attrsToo) {
+		if (attrsToo) {
+			this.configure({}, true);
+		}
+		while (this._svg.firstChild) {
+			this._svg.removeChild(this._svg.firstChild);
+		}
+		return this;
+	},
+
+	/* Serialise the current diagram into an SVG text document.
+	   @param  node  (SVG element) the starting node (optional)
+	   @return  (string) the SVG as text */
+	toSVG: function(node) {
+		node = node || this._svg;
+		return (typeof XMLSerializer == 'undefined' ? this._toSVG(node) :
+			new XMLSerializer().serializeToString(node));
+	},
+
+	/* Serialise one node in the SVG hierarchy. */
+	_toSVG: function(node) {
+		var svgDoc = '';
+		if (!node) {
+			return svgDoc;
+		}
+		if (node.nodeType == 3) { // Text
+			svgDoc = node.nodeValue;
+		}
+		else if (node.nodeType == 4) { // CDATA
+			svgDoc = '<![CDATA[' + node.nodeValue + ']]>';
+		}
+		else { // Element
+			svgDoc = '<' + node.nodeName;
+			if (node.attributes) {
+				for (var i = 0; i < node.attributes.length; i++) {
+					var attr = node.attributes.item(i);
+					if (!($.trim(attr.nodeValue) == '' || attr.nodeValue.match(/^\[object/) ||
+							attr.nodeValue.match(/^function/))) {
+						svgDoc += ' ' + (attr.namespaceURI == $.svg.xlinkNS ? 'xlink:' : '') + 
+							attr.nodeName + '="' + attr.nodeValue + '"';
+					}
+				}
+			}	
+			if (node.firstChild) {
+				svgDoc += '>';
+				var child = node.firstChild;
+				while (child) {
+					svgDoc += this._toSVG(child);
+					child = child.nextSibling;
+				}
+				svgDoc += '</' + node.nodeName + '>';
+			}
+				else {
+				svgDoc += '/>';
+			}
+		}
+		return svgDoc;
+	}
+});
+
+/* Helper to generate an SVG path.
+   Obtain an instance from the SVGWrapper object.
+   String calls together to generate the path and use its value:
+   var path = root.createPath();
+   root.path(null, path.move(100, 100).line(300, 100).line(200, 300).close(), {fill: 'red'});
+   or
+   root.path(null, path.move(100, 100).line([[300, 100], [200, 300]]).close(), {fill: 'red'}); */
+function SVGPath() {
+	this._path = '';
+}
+
+$.extend(SVGPath.prototype, {
+	/* Prepare to create a new path.
+	   @return  (SVGPath) this path */
+	reset: function() {
+		this._path = '';
+		return this;
+	},
+
+	/* Move the pointer to a position.
+	   @param  x         (number) x-coordinate to move to or
+	                     (number[][]) x-/y-coordinates to move to
+	   @param  y         (number) y-coordinate to move to (omitted if x is array)
+	   @param  relative  (boolean) true for coordinates relative to the current point,
+	                     false for coordinates being absolute
+	   @return  (SVGPath) this path */
+	move: function(x, y, relative) {
+		relative = (isArray(x) ? y : relative);
+		return this._coords((relative ? 'm' : 'M'), x, y);
+	},
+
+	/* Draw a line to a position.
+	   @param  x         (number) x-coordinate to move to or
+	                     (number[][]) x-/y-coordinates to move to
+	   @param  y         (number) y-coordinate to move to (omitted if x is array)
+	   @param  relative  (boolean) true for coordinates relative to the current point,
+	                     false for coordinates being absolute
+	   @return  (SVGPath) this path */
+	line: function(x, y, relative) {
+		relative = (isArray(x) ? y : relative);
+		return this._coords((relative ? 'l' : 'L'), x, y);
+	},
+
+	/* Draw a horizontal line to a position.
+	   @param  x         (number) x-coordinate to draw to or
+	                     (number[]) x-coordinates to draw to
+	   @param  relative  (boolean) true for coordinates relative to the current point,
+	                     false for coordinates being absolute
+	   @return  (SVGPath) this path */
+	horiz: function(x, relative) {
+		this._path += (relative ? 'h' : 'H') + (isArray(x) ? x.join(' ') : x);
+		return this;
+	},
+
+	/* Draw a vertical line to a position.
+	   @param  y         (number) y-coordinate to draw to or
+	                     (number[]) y-coordinates to draw to
+	   @param  relative  (boolean) true for coordinates relative to the current point,
+	                     false for coordinates being absolute
+	   @return  (SVGPath) this path */
+	vert: function(y, relative) {
+		this._path += (relative ? 'v' : 'V') + (isArray(y) ? y.join(' ') : y);
+		return this;
+	},
+
+	/* Draw a cubic Bézier curve.
+	   @param  x1        (number) x-coordinate of beginning control point or
+	                     (number[][]) x-/y-coordinates of control and end points to draw to
+	   @param  y1        (number) y-coordinate of beginning control point (omitted if x1 is array)
+	   @param  x2        (number) x-coordinate of ending control point (omitted if x1 is array)
+	   @param  y2        (number) y-coordinate of ending control point (omitted if x1 is array)
+	   @param  x         (number) x-coordinate of curve end (omitted if x1 is array)
+	   @param  y         (number) y-coordinate of curve end (omitted if x1 is array)
+	   @param  relative  (boolean) true for coordinates relative to the current point,
+	                     false for coordinates being absolute
+	   @return  (SVGPath) this path */
+	curveC: function(x1, y1, x2, y2, x, y, relative) {
+		relative = (isArray(x1) ? y1 : relative);
+		return this._coords((relative ? 'c' : 'C'), x1, y1, x2, y2, x, y);
+	},
+
+	/* Continue a cubic Bézier curve.
+	   Starting control point is the reflection of the previous end control point.
+	   @param  x2        (number) x-coordinate of ending control point or
+	                     (number[][]) x-/y-coordinates of control and end points to draw to
+	   @param  y2        (number) y-coordinate of ending control point (omitted if x2 is array)
+	   @param  x         (number) x-coordinate of curve end (omitted if x2 is array)
+	   @param  y         (number) y-coordinate of curve end (omitted if x2 is array)
+	   @param  relative  (boolean) true for coordinates relative to the current point,
+	                     false for coordinates being absolute
+	   @return  (SVGPath) this path */
+	smoothC: function(x2, y2, x, y, relative) {
+		relative = (isArray(x2) ? y2 : relative);
+		return this._coords((relative ? 's' : 'S'), x2, y2, x, y);
+	},
+
+	/* Draw a quadratic Bézier curve.
+	   @param  x1        (number) x-coordinate of control point or
+	                     (number[][]) x-/y-coordinates of control and end points to draw to
+	   @param  y1        (number) y-coordinate of control point (omitted if x1 is array)
+	   @param  x         (number) x-coordinate of curve end (omitted if x1 is array)
+	   @param  y         (number) y-coordinate of curve end (omitted if x1 is array)
+	   @param  relative  (boolean) true for coordinates relative to the current point,
+	                     false for coordinates being absolute
+	   @return  (SVGPath) this path */
+	curveQ: function(x1, y1, x, y, relative) {
+		relative = (isArray(x1) ? y1 : relative);
+		return this._coords((relative ? 'q' : 'Q'), x1, y1, x, y);
+	},
+
+	/* Continue a quadratic Bézier curve.
+	   Control point is the reflection of the previous control point.
+	   @param  x         (number) x-coordinate of curve end or
+	                     (number[][]) x-/y-coordinates of points to draw to
+	   @param  y         (number) y-coordinate of curve end (omitted if x is array)
+	   @param  relative  (boolean) true for coordinates relative to the current point,
+	                     false for coordinates being absolute
+	   @return  (SVGPath) this path */
+	smoothQ: function(x, y, relative) {
+		relative = (isArray(x) ? y : relative);
+		return this._coords((relative ? 't' : 'T'), x, y);
+	},
+
+	/* Generate a path command with (a list of) coordinates. */
+	_coords: function(cmd, x1, y1, x2, y2, x3, y3) {
+		if (isArray(x1)) {
+			for (var i = 0; i < x1.length; i++) {
+				var cs = x1[i];
+				this._path += (i == 0 ? cmd : ' ') + cs[0] + ',' + cs[1] +
+					(cs.length < 4 ? '' : ' ' + cs[2] + ',' + cs[3] +
+					(cs.length < 6 ? '': ' ' + cs[4] + ',' + cs[5]));
+			}
+		}
+		else {
+			this._path += cmd + x1 + ',' + y1 + 
+				(x2 == null ? '' : ' ' + x2 + ',' + y2 +
+				(x3 == null ? '' : ' ' + x3 + ',' + y3));
+		}
+		return this;
+	},
+
+	/* Draw an arc to a position.
+	   @param  rx         (number) x-radius of arc or
+	                      (number/boolean[][]) x-/y-coordinates and flags for points to draw to
+	   @param  ry         (number) y-radius of arc (omitted if rx is array)
+	   @param  xRotate    (number) x-axis rotation (degrees, clockwise) (omitted if rx is array)
+	   @param  large      (boolean) true to draw the large part of the arc,
+	                      false to draw the small part (omitted if rx is array)
+	   @param  clockwise  (boolean) true to draw the clockwise arc,
+	                      false to draw the anti-clockwise arc (omitted if rx is array)
+	   @param  x          (number) x-coordinate of arc end (omitted if rx is array)
+	   @param  y          (number) y-coordinate of arc end (omitted if rx is array)
+	   @param  relative   (boolean) true for coordinates relative to the current point,
+	                      false for coordinates being absolute
+	   @return  (SVGPath) this path */
+	arc: function(rx, ry, xRotate, large, clockwise, x, y, relative) {
+		relative = (isArray(rx) ? ry : relative);
+		this._path += (relative ? 'a' : 'A');
+		if (isArray(rx)) {
+			for (var i = 0; i < rx.length; i++) {
+				var cs = rx[i];
+				this._path += (i == 0 ? '' : ' ') + cs[0] + ',' + cs[1] + ' ' +
+					cs[2] + ' ' + (cs[3] ? '1' : '0') + ',' +
+					(cs[4] ? '1' : '0') + ' ' + cs[5] + ',' + cs[6];
+			}
+		}
+		else {
+			this._path += rx + ',' + ry + ' ' + xRotate + ' ' +
+				(large ? '1' : '0') + ',' + (clockwise ? '1' : '0') + ' ' + x + ',' + y;
+		}
+		return this;
+	},
+
+	/* Close the current path.
+	   @return  (SVGPath) this path */
+	close: function() {
+		this._path += 'z';
+		return this;
+	},
+
+	/* Return the string rendering of the specified path.
+	   @return  (string) stringified path */
+	path: function() {
+		return this._path;
+	}
+});
+
+SVGPath.prototype.moveTo = SVGPath.prototype.move;
+SVGPath.prototype.lineTo = SVGPath.prototype.line;
+SVGPath.prototype.horizTo = SVGPath.prototype.horiz;
+SVGPath.prototype.vertTo = SVGPath.prototype.vert;
+SVGPath.prototype.curveCTo = SVGPath.prototype.curveC;
+SVGPath.prototype.smoothCTo = SVGPath.prototype.smoothC;
+SVGPath.prototype.curveQTo = SVGPath.prototype.curveQ;
+SVGPath.prototype.smoothQTo = SVGPath.prototype.smoothQ;
+SVGPath.prototype.arcTo = SVGPath.prototype.arc;
+
+/* Helper to generate an SVG text object.
+   Obtain an instance from the SVGWrapper object.
+   String calls together to generate the text and use its value:
+   var text = root.createText();
+   root.text(null, x, y, text.string('This is ').
+     span('red', {fill: 'red'}).string('!'), {fill: 'blue'}); */
+function SVGText() {
+	this._parts = []; // The components of the text object
+}
+
+$.extend(SVGText.prototype, {
+	/* Prepare to create a new text object.
+	   @return  (SVGText) this text */
+	reset: function() {
+		this._parts = [];
+		return this;
+	},
+
+	/* Add a straight string value.
+	   @param  value  (string) the actual text
+	   @return  (SVGText) this text object */
+	string: function(value) {
+		this._parts[this._parts.length] = ['text', value];
+		return this;
+	},
+
+	/* Add a separate text span that has its own settings.
+	   @param  value     (string) the actual text
+	   @param  settings  (object) the settings for this text
+	   @return  (SVGText) this text object */
+	span: function(value, settings) {
+		this._parts[this._parts.length] = ['tspan', value, settings];
+		return this;
+	},
+
+	/* Add a reference to a previously defined text string.
+	   @param  id        (string) the ID of the actual text
+	   @param  settings  (object) the settings for this text
+	   @return  (SVGText) this text object */
+	ref: function(id, settings) {
+		this._parts[this._parts.length] = ['tref', id, settings];
+		return this;
+	},
+
+	/* Add text drawn along a path.
+	   @param  id        (string) the ID of the path
+	   @param  value     (string) the actual text
+	   @param  settings  (object) the settings for this text
+	   @return  (SVGText) this text object */
+	path: function(id, value, settings) {
+		this._parts[this._parts.length] = ['textpath', value, 
+			$.extend({href: id}, settings || {})];
+		return this;
+	}
+});
+
+/* Attach the SVG functionality to a jQuery selection.
+   @param  command  (string) the command to run (optional, default 'attach')
+   @param  options  (object) the new settings to use for these SVG instances
+   @return jQuery (object) for chaining further calls */
+$.fn.svg = function(options) {
+	var otherArgs = Array.prototype.slice.call(arguments, 1);
+	if (typeof options == 'string' && options == 'get') {
+		return $.svg['_' + options + 'SVG'].apply($.svg, [this[0]].concat(otherArgs));
+	}
+	return this.each(function() {
+		if (typeof options == 'string') {
+			$.svg['_' + options + 'SVG'].apply($.svg, [this].concat(otherArgs));
+		}
+		else {
+			$.svg._attachSVG(this, options || {});
+		} 
+	});
+};
+
+/* Determine whether an object is an array. */
+function isArray(a) {
+	return (a && a.constructor == Array);
+}
+
+// Singleton primary SVG interface
+$.svg = new SVGManager();
+
+})(jQuery);
diff --git a/src/web/js/jquery.svg.min.js b/src/web/js/jquery.svg.min.js
new file mode 100644
index 0000000..5b922fb
--- /dev/null
+++ b/src/web/js/jquery.svg.min.js
@@ -0,0 +1,7 @@
+/* http://keith-wood.name/svg.html
+   SVG for jQuery v1.4.5.
+   Written by Keith Wood (kbwood{at}iinet.com.au) August 2007.
+   Dual licensed under the GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) and 
+   MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt) licenses. 
+   Please attribute the author if you use it. */
+(function($){function SVGManager(){this._settings=[];this._extensions=[];this.regional=[];this.regional['']={errorLoadingText:'Error loading',notSupportedText:'This browser does not support SVG'};this.local=this.regional[''];this._uuid=new Date().getTime();this._renesis=detectActiveX('RenesisX.RenesisCtrl')}function detectActiveX(a){try{return!!(window.ActiveXObject&&new ActiveXObject(a))}catch(e){return false}}var q='svgwrapper';$.extend(SVGManager.prototype,{markerClassName:'hasSVG',svgNS:'http://www.w3.org/2000/svg',xlinkNS:'http://www.w3.org/1999/xlink',_wrapperClass:SVGWrapper,_attrNames:{class_:'class',in_:'in',alignmentBaseline:'alignment-baseline',baselineShift:'baseline-shift',clipPath:'clip-path',clipRule:'clip-rule',colorInterpolation:'color-interpolation',colorInterpolationFilters:'color-interpolation-filters',colorRendering:'color-rendering',dominantBaseline:'dominant-baseline',enableBackground:'enable-background',fillOpacity:'fill-opacity',fillRule:'fill-rule',floodColor:'flood-color',floodOpacity:'flood-opacity',fontFamily:'font-family',fontSize:'font-size',fontSizeAdjust:'font-size-adjust',fontStretch:'font-stretch',fontStyle:'font-style',fontVariant:'font-variant',fontWeight:'font-weight',glyphOrientationHorizontal:'glyph-orientation-horizontal',glyphOrientationVertical:'glyph-orientation-vertical',horizAdvX:'horiz-adv-x',horizOriginX:'horiz-origin-x',imageRendering:'image-rendering',letterSpacing:'letter-spacing',lightingColor:'lighting-color',markerEnd:'marker-end',markerMid:'marker-mid',markerStart:'marker-start',stopColor:'stop-color',stopOpacity:'stop-opacity',strikethroughPosition:'strikethrough-position',strikethroughThickness:'strikethrough-thickness',strokeDashArray:'stroke-dasharray',strokeDashOffset:'stroke-dashoffset',strokeLineCap:'stroke-linecap',strokeLineJoin:'stroke-linejoin',strokeMiterLimit:'stroke-miterlimit',strokeOpacity:'stroke-opacity',strokeWidth:'stroke-width',textAnchor:'text-anchor',textDecoration:'text-decoration',textRendering:'text-rendering',underlinePosition:'underline-position',underlineThickness:'underline-thickness',vertAdvY:'vert-adv-y',vertOriginY:'vert-origin-y',wordSpacing:'word-spacing',writingMode:'writing-mode'},_attachSVG:function(a,b){var c=(a.namespaceURI==this.svgNS?a:null);var a=(c?null:a);if($(a||c).hasClass(this.markerClassName)){return}if(typeof b=='string'){b={loadURL:b}}else if(typeof b=='function'){b={onLoad:b}}$(a||c).addClass(this.markerClassName);try{if(!c){c=document.createElementNS(this.svgNS,'svg');c.setAttribute('version','1.1');if(a.clientWidth>0){c.setAttribute('width',a.clientWidth)}if(a.clientHeight>0){c.setAttribute('height',a.clientHeight)}a.appendChild(c)}this._afterLoad(a,c,b||{})}catch(e){if($.browser.msie){if(!a.id){a.id='svg'+(this._uuid++)}this._settings[a.id]=b;a.innerHTML='<embed type="image/svg+xml" width="100%" '+'height="100%" src="'+(b.initPath||'')+'blank.svg" '+'pluginspage="http://www.adobe.com/svg/viewer/install/main.html"/>'}else{a.innerHTML='<p class="svg_error">'+this.local.notSupportedText+'</p>'}}},_registerSVG:function(){for(var i=0;i<document.embeds.length;i++){var a=document.embeds[i].parentNode;if(!$(a).hasClass($.svg.markerClassName)||$.data(a,q)){continue}var b=null;try{b=document.embeds[i].getSVGDocument()}catch(e){setTimeout($.svg._registerSVG,250);return}b=(b?b.documentElement:null);if(b){$.svg._afterLoad(a,b)}}},_afterLoad:function(a,b,c){var c=c||this._settings[a.id];this._settings[a?a.id:'']=null;var d=new this._wrapperClass(b,a);$.data(a||b,q,d);try{if(c.loadURL){d.load(c.loadURL,c)}if(c.settings){d.configure(c.settings)}if(c.onLoad&&!c.loadURL){c.onLoad.apply(a||b,[d])}}catch(e){alert(e)}},_getSVG:function(a){a=(typeof a=='string'?$(a)[0]:(a.jquery?a[0]:a));return $.data(a,q)},_destroySVG:function(a){var b=$(a);if(!b.hasClass(this.markerClassName)){return}b.removeClass(this.markerClassName);if(a.namespaceURI!=this.svgNS){b.empty()}$.removeData(a,q)},addExtension:function(a,b){this._extensions.push([a,b])},isSVGElem:function(a){return(a.nodeType==1&&a.namespaceURI==$.svg.svgNS)}});function SVGWrapper(a,b){this._svg=a;this._container=b;for(var i=0;i<$.svg._extensions.length;i++){var c=$.svg._extensions[i];this[c[0]]=new c[1](this)}}$.extend(SVGWrapper.prototype,{_width:function(){return(this._container?this._container.clientWidth:this._svg.width)},_height:function(){return(this._container?this._container.clientHeight:this._svg.height)},root:function(){return this._svg},configure:function(a,b,c){if(!a.nodeName){c=b;b=a;a=this._svg}if(c){for(var i=a.attributes.length-1;i>=0;i--){var d=a.attributes.item(i);if(!(d.nodeName=='onload'||d.nodeName=='version'||d.nodeName.substring(0,5)=='xmlns')){a.attributes.removeNamedItem(d.nodeName)}}}for(var e in b){a.setAttribute($.svg._attrNames[e]||e,b[e])}return this},getElementById:function(a){return this._svg.ownerDocument.getElementById(a)},change:function(a,b){if(a){for(var c in b){if(b[c]==null){a.removeAttribute($.svg._attrNames[c]||c)}else{a.setAttribute($.svg._attrNames[c]||c,b[c])}}}return this},_args:function(b,c,d){c.splice(0,0,'parent');c.splice(c.length,0,'settings');var e={};var f=0;if(b[0]!=null&&b[0].jquery){b[0]=b[0][0]}if(b[0]!=null&&!(typeof b[0]=='object'&&b[0].nodeName)){e['parent']=null;f=1}for(var i=0;i<b.length;i++){e[c[i+f]]=b[i]}if(d){$.each(d,function(i,a){if(typeof e[a]=='object'){e.settings=e[a];e[a]=null}})}return e},title:function(a,b,c){var d=this._args(arguments,['text']);var e=this._makeNode(d.parent,'title',d.settings||{});e.appendChild(this._svg.ownerDocument.createTextNode(d.text));return e},describe:function(a,b,c){var d=this._args(arguments,['text']);var e=this._makeNode(d.parent,'desc',d.settings||{});e.appendChild(this._svg.ownerDocument.createTextNode(d.text));return e},defs:function(a,b,c){var d=this._args(arguments,['id'],['id']);return this._makeNode(d.parent,'defs',$.extend((d.id?{id:d.id}:{}),d.settings||{}))},symbol:function(a,b,c,d,e,f,g){var h=this._args(arguments,['id','x1','y1','width','height']);return this._makeNode(h.parent,'symbol',$.extend({id:h.id,viewBox:h.x1+' '+h.y1+' '+h.width+' '+h.height},h.settings||{}))},marker:function(a,b,c,d,e,f,g,h){var i=this._args(arguments,['id','refX','refY','mWidth','mHeight','orient'],['orient']);return this._makeNode(i.parent,'marker',$.extend({id:i.id,refX:i.refX,refY:i.refY,markerWidth:i.mWidth,markerHeight:i.mHeight,orient:i.orient||'auto'},i.settings||{}))},style:function(a,b,c){var d=this._args(arguments,['styles']);var e=this._makeNode(d.parent,'style',$.extend({type:'text/css'},d.settings||{}));e.appendChild(this._svg.ownerDocument.createTextNode(d.styles));if($.browser.opera){$('head').append('<style type="text/css">'+d.styles+'</style>')}return e},script:function(a,b,c,d){var e=this._args(arguments,['script','type'],['type']);var f=this._makeNode(e.parent,'script',$.extend({type:e.type||'text/javascript'},e.settings||{}));f.appendChild(this._svg.ownerDocument.createTextNode(e.script));if(!$.browser.mozilla){$.globalEval(e.script)}return f},linearGradient:function(a,b,c,d,e,f,g,h){var i=this._args(arguments,['id','stops','x1','y1','x2','y2'],['x1']);var j=$.extend({id:i.id},(i.x1!=null?{x1:i.x1,y1:i.y1,x2:i.x2,y2:i.y2}:{}));return this._gradient(i.parent,'linearGradient',$.extend(j,i.settings||{}),i.stops)},radialGradient:function(a,b,c,d,e,r,f,g,h){var i=this._args(arguments,['id','stops','cx','cy','r','fx','fy'],['cx']);var j=$.extend({id:i.id},(i.cx!=null?{cx:i.cx,cy:i.cy,r:i.r,fx:i.fx,fy:i.fy}:{}));return this._gradient(i.parent,'radialGradient',$.extend(j,i.settings||{}),i.stops)},_gradient:function(a,b,c,d){var e=this._makeNode(a,b,c);for(var i=0;i<d.length;i++){var f=d[i];this._makeNode(e,'stop',$.extend({offset:f[0],stopColor:f[1]},(f[2]!=null?{stopOpacity:f[2]}:{})))}return e},pattern:function(a,b,x,y,c,d,e,f,g,h,i){var j=this._args(arguments,['id','x','y','width','height','vx','vy','vwidth','vheight'],['vx']);var k=$.extend({id:j.id,x:j.x,y:j.y,width:j.width,height:j.height},(j.vx!=null?{viewBox:j.vx+' '+j.vy+' '+j.vwidth+' '+j.vheight}:{}));return this._makeNode(j.parent,'pattern',$.extend(k,j.settings||{}))},clipPath:function(a,b,c,d){var e=this._args(arguments,['id','units']);e.units=e.units||'userSpaceOnUse';return this._makeNode(e.parent,'clipPath',$.extend({id:e.id,clipPathUnits:e.units},e.settings||{}))},mask:function(a,b,x,y,c,d,e){var f=this._args(arguments,['id','x','y','width','height']);return this._makeNode(f.parent,'mask',$.extend({id:f.id,x:f.x,y:f.y,width:f.width,height:f.height},f.settings||{}))},createPath:function(){return new SVGPath()},createText:function(){return new SVGText()},svg:function(a,x,y,b,c,d,e,f,g,h){var i=this._args(arguments,['x','y','width','height','vx','vy','vwidth','vheight'],['vx']);var j=$.extend({x:i.x,y:i.y,width:i.width,height:i.height},(i.vx!=null?{viewBox:i.vx+' '+i.vy+' '+i.vwidth+' '+i.vheight}:{}));return this._makeNode(i.parent,'svg',$.extend(j,i.settings||{}))},group:function(a,b,c){var d=this._args(arguments,['id'],['id']);return this._makeNode(d.parent,'g',$.extend({id:d.id},d.settings||{}))},use:function(a,x,y,b,c,d,e){var f=this._args(arguments,['x','y','width','height','ref']);if(typeof f.x=='string'){f.ref=f.x;f.settings=f.y;f.x=f.y=f.width=f.height=null}var g=this._makeNode(f.parent,'use',$.extend({x:f.x,y:f.y,width:f.width,height:f.height},f.settings||{}));g.setAttributeNS($.svg.xlinkNS,'href',f.ref);return g},link:function(a,b,c){var d=this._args(arguments,['ref']);var e=this._makeNode(d.parent,'a',d.settings);e.setAttributeNS($.svg.xlinkNS,'href',d.ref);return e},image:function(a,x,y,b,c,d,e){var f=this._args(arguments,['x','y','width','height','ref']);var g=this._makeNode(f.parent,'image',$.extend({x:f.x,y:f.y,width:f.width,height:f.height},f.settings||{}));g.setAttributeNS($.svg.xlinkNS,'href',f.ref);return g},path:function(a,b,c){var d=this._args(arguments,['path']);return this._makeNode(d.parent,'path',$.extend({d:(d.path.path?d.path.path():d.path)},d.settings||{}))},rect:function(a,x,y,b,c,d,e,f){var g=this._args(arguments,['x','y','width','height','rx','ry'],['rx']);return this._makeNode(g.parent,'rect',$.extend({x:g.x,y:g.y,width:g.width,height:g.height},(g.rx?{rx:g.rx,ry:g.ry}:{}),g.settings||{}))},circle:function(a,b,c,r,d){var e=this._args(arguments,['cx','cy','r']);return this._makeNode(e.parent,'circle',$.extend({cx:e.cx,cy:e.cy,r:e.r},e.settings||{}))},ellipse:function(a,b,c,d,e,f){var g=this._args(arguments,['cx','cy','rx','ry']);return this._makeNode(g.parent,'ellipse',$.extend({cx:g.cx,cy:g.cy,rx:g.rx,ry:g.ry},g.settings||{}))},line:function(a,b,c,d,e,f){var g=this._args(arguments,['x1','y1','x2','y2']);return this._makeNode(g.parent,'line',$.extend({x1:g.x1,y1:g.y1,x2:g.x2,y2:g.y2},g.settings||{}))},polyline:function(a,b,c){var d=this._args(arguments,['points']);return this._poly(d.parent,'polyline',d.points,d.settings)},polygon:function(a,b,c){var d=this._args(arguments,['points']);return this._poly(d.parent,'polygon',d.points,d.settings)},_poly:function(a,b,c,d){var e='';for(var i=0;i<c.length;i++){e+=c[i].join()+' '}return this._makeNode(a,b,$.extend({points:$.trim(e)},d||{}))},text:function(a,x,y,b,c){var d=this._args(arguments,['x','y','value']);if(typeof d.x=='string'&&arguments.length<4){d.value=d.x;d.settings=d.y;d.x=d.y=null}return this._text(d.parent,'text',d.value,$.extend({x:(d.x&&isArray(d.x)?d.x.join(' '):d.x),y:(d.y&&isArray(d.y)?d.y.join(' '):d.y)},d.settings||{}))},textpath:function(a,b,c,d){var e=this._args(arguments,['path','value']);var f=this._text(e.parent,'textPath',e.value,e.settings||{});f.setAttributeNS($.svg.xlinkNS,'href',e.path);return f},_text:function(a,b,c,d){var e=this._makeNode(a,b,d);if(typeof c=='string'){e.appendChild(e.ownerDocument.createTextNode(c))}else{for(var i=0;i<c._parts.length;i++){var f=c._parts[i];if(f[0]=='tspan'){var g=this._makeNode(e,f[0],f[2]);g.appendChild(e.ownerDocument.createTextNode(f[1]));e.appendChild(g)}else if(f[0]=='tref'){var g=this._makeNode(e,f[0],f[2]);g.setAttributeNS($.svg.xlinkNS,'href',f[1]);e.appendChild(g)}else if(f[0]=='textpath'){var h=$.extend({},f[2]);h.href=null;var g=this._makeNode(e,f[0],h);g.setAttributeNS($.svg.xlinkNS,'href',f[2].href);g.appendChild(e.ownerDocument.createTextNode(f[1]));e.appendChild(g)}else{e.appendChild(e.ownerDocument.createTextNode(f[1]))}}}return e},other:function(a,b,c){var d=this._args(arguments,['name']);return this._makeNode(d.parent,d.name,d.settings||{})},_makeNode:function(a,b,c){a=a||this._svg;var d=this._svg.ownerDocument.createElementNS($.svg.svgNS,b);for(var b in c){var e=c[b];if(e!=null&&e!=null&&(typeof e!='string'||e!='')){d.setAttribute($.svg._attrNames[b]||b,e)}}a.appendChild(d);return d},add:function(b,c){var d=this._args((arguments.length==1?[null,b]:arguments),['node']);var f=this;d.parent=d.parent||this._svg;d.node=(d.node.jquery?d.node:$(d.node));try{if($.svg._renesis){throw'Force traversal';}d.parent.appendChild(d.node.cloneNode(true))}catch(e){d.node.each(function(){var a=f._cloneAsSVG(this);if(a){d.parent.appendChild(a)}})}return this},clone:function(b,c){var d=this;var e=this._args((arguments.length==1?[null,b]:arguments),['node']);e.parent=e.parent||this._svg;e.node=(e.node.jquery?e.node:$(e.node));var f=[];e.node.each(function(){var a=d._cloneAsSVG(this);if(a){a.id='';e.parent.appendChild(a);f.push(a)}});return f},_cloneAsSVG:function(a){var b=null;if(a.nodeType==1){b=this._svg.ownerDocument.createElementNS($.svg.svgNS,this._checkName(a.nodeName));for(var i=0;i<a.attributes.length;i++){var c=a.attributes.item(i);if(c.nodeName!='xmlns'&&c.nodeValue){if(c.prefix=='xlink'){b.setAttributeNS($.svg.xlinkNS,c.localName||c.baseName,c.nodeValue)}else{b.setAttribute(this._checkName(c.nodeName),c.nodeValue)}}}for(var i=0;i<a.childNodes.length;i++){var d=this._cloneAsSVG(a.childNodes[i]);if(d){b.appendChild(d)}}}else if(a.nodeType==3){if($.trim(a.nodeValue)){b=this._svg.ownerDocument.createTextNode(a.nodeValue)}}else if(a.nodeType==4){if($.trim(a.nodeValue)){try{b=this._svg.ownerDocument.createCDATASection(a.nodeValue)}catch(e){b=this._svg.ownerDocument.createTextNode(a.nodeValue.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'))}}}return b},_checkName:function(a){a=(a.substring(0,1)>='A'&&a.substring(0,1)<='Z'?a.toLowerCase():a);return(a.substring(0,4)=='svg:'?a.substring(4):a)},load:function(j,k){k=(typeof k=='boolean'?{addTo:k}:(typeof k=='function'?{onLoad:k}:(typeof k=='string'?{parent:k}:(typeof k=='object'&&k.nodeName?{parent:k}:(typeof k=='object'&&k.jquery?{parent:k}:k||{})))));if(!k.parent&&!k.addTo){this.clear(false)}var l=[this._svg.getAttribute('width'),this._svg.getAttribute('height')];var m=this;var n=function(a){a=$.svg.local.errorLoadingText+': '+a;if(k.onLoad){k.onLoad.apply(m._container||m._svg,[m,a])}else{m.text(null,10,20,a)}};var o=function(a){var b=new ActiveXObject('Microsoft.XMLDOM');b.validateOnParse=false;b.resolveExternals=false;b.async=false;b.loadXML(a);if(b.parseError.errorCode!=0){n(b.parseError.reason);return null}return b};var p=function(a){if(!a){return}if(a.documentElement.nodeName!='svg'){var b=a.getElementsByTagName('parsererror');var c=(b.length?b[0].getElementsByTagName('div'):[]);n(!b.length?'???':(c.length?c[0]:b[0]).firstChild.nodeValue);return}var d=(k.parent?$(k.parent)[0]:m._svg);var f={};for(var i=0;i<a.documentElement.attributes.length;i++){var g=a.documentElement.attributes.item(i);if(!(g.nodeName=='version'||g.nodeName.substring(0,5)=='xmlns')){f[g.nodeName]=g.nodeValue}}m.configure(d,f,!k.parent);var h=a.documentElement.childNodes;for(var i=0;i<h.length;i++){try{if($.svg._renesis){throw'Force traversal';}d.appendChild(m._svg.ownerDocument.importNode(h[i],true));if(h[i].nodeName=='script'){$.globalEval(h[i].textContent)}}catch(e){m.add(d,h[i])}}if(!k.changeSize){m.configure(d,{width:l[0],height:l[1]})}if(k.onLoad){k.onLoad.apply(m._container||m._svg,[m])}};if(j.match('<svg')){p($.browser.msie?o(j):new DOMParser().parseFromString(j,'text/xml'))}else{$.ajax({url:j,dataType:($.browser.msie?'text':'xml'),success:function(a){p($.browser.msie?o(a):a)},error:function(a,b,c){n(b+(c?' '+c.message:''))}})}return this},remove:function(a){a=(a.jquery?a[0]:a);a.parentNode.removeChild(a);return this},clear:function(a){if(a){this.configure({},true)}while(this._svg.firstChild){this._svg.removeChild(this._svg.firstChild)}return this},toSVG:function(a){a=a||this._svg;return(typeof XMLSerializer=='undefined'?this._toSVG(a):new XMLSerializer().serializeToString(a))},_toSVG:function(a){var b='';if(!a){return b}if(a.nodeType==3){b=a.nodeValue}else if(a.nodeType==4){b='<![CDATA['+a.nodeValue+']]>'}else{b='<'+a.nodeName;if(a.attributes){for(var i=0;i<a.attributes.length;i++){var c=a.attributes.item(i);if(!($.trim(c.nodeValue)==''||c.nodeValue.match(/^\[object/)||c.nodeValue.match(/^function/))){b+=' '+(c.namespaceURI==$.svg.xlinkNS?'xlink:':'')+c.nodeName+'="'+c.nodeValue+'"'}}}if(a.firstChild){b+='>';var d=a.firstChild;while(d){b+=this._toSVG(d);d=d.nextSibling}b+='</'+a.nodeName+'>'}else{b+='/>'}}return b}});function SVGPath(){this._path=''}$.extend(SVGPath.prototype,{reset:function(){this._path='';return this},move:function(x,y,a){a=(isArray(x)?y:a);return this._coords((a?'m':'M'),x,y)},line:function(x,y,a){a=(isArray(x)?y:a);return this._coords((a?'l':'L'),x,y)},horiz:function(x,a){this._path+=(a?'h':'H')+(isArray(x)?x.join(' '):x);return this},vert:function(y,a){this._path+=(a?'v':'V')+(isArray(y)?y.join(' '):y);return this},curveC:function(a,b,c,d,x,y,e){e=(isArray(a)?b:e);return this._coords((e?'c':'C'),a,b,c,d,x,y)},smoothC:function(a,b,x,y,c){c=(isArray(a)?b:c);return this._coords((c?'s':'S'),a,b,x,y)},curveQ:function(a,b,x,y,c){c=(isArray(a)?b:c);return this._coords((c?'q':'Q'),a,b,x,y)},smoothQ:function(x,y,a){a=(isArray(x)?y:a);return this._coords((a?'t':'T'),x,y)},_coords:function(a,b,c,d,e,f,g){if(isArray(b)){for(var i=0;i<b.length;i++){var h=b[i];this._path+=(i==0?a:' ')+h[0]+','+h[1]+(h.length<4?'':' '+h[2]+','+h[3]+(h.length<6?'':' '+h[4]+','+h[5]))}}else{this._path+=a+b+','+c+(d==null?'':' '+d+','+e+(f==null?'':' '+f+','+g))}return this},arc:function(a,b,c,d,e,x,y,f){f=(isArray(a)?b:f);this._path+=(f?'a':'A');if(isArray(a)){for(var i=0;i<a.length;i++){var g=a[i];this._path+=(i==0?'':' ')+g[0]+','+g[1]+' '+g[2]+' '+(g[3]?'1':'0')+','+(g[4]?'1':'0')+' '+g[5]+','+g[6]}}else{this._path+=a+','+b+' '+c+' '+(d?'1':'0')+','+(e?'1':'0')+' '+x+','+y}return this},close:function(){this._path+='z';return this},path:function(){return this._path}});SVGPath.prototype.moveTo=SVGPath.prototype.move;SVGPath.prototype.lineTo=SVGPath.prototype.line;SVGPath.prototype.horizTo=SVGPath.prototype.horiz;SVGPath.prototype.vertTo=SVGPath.prototype.vert;SVGPath.prototype.curveCTo=SVGPath.prototype.curveC;SVGPath.prototype.smoothCTo=SVGPath.prototype.smoothC;SVGPath.prototype.curveQTo=SVGPath.prototype.curveQ;SVGPath.prototype.smoothQTo=SVGPath.prototype.smoothQ;SVGPath.prototype.arcTo=SVGPath.prototype.arc;function SVGText(){this._parts=[]}$.extend(SVGText.prototype,{reset:function(){this._parts=[];return this},string:function(a){this._parts[this._parts.length]=['text',a];return this},span:function(a,b){this._parts[this._parts.length]=['tspan',a,b];return this},ref:function(a,b){this._parts[this._parts.length]=['tref',a,b];return this},path:function(a,b,c){this._parts[this._parts.length]=['textpath',b,$.extend({href:a},c||{})];return this}});$.fn.svg=function(a){var b=Array.prototype.slice.call(arguments,1);if(typeof a=='string'&&a=='get'){return $.svg['_'+a+'SVG'].apply($.svg,[this[0]].concat(b))}return this.each(function(){if(typeof a=='string'){$.svg['_'+a+'SVG'].apply($.svg,[this].concat(b))}else{$.svg._attachSVG(this,a||{})}})};function isArray(a){return(a&&a.constructor==Array)}$.svg=new SVGManager()})(jQuery);
\ No newline at end of file
diff --git a/src/web/js/jquery.svganim.min.js b/src/web/js/jquery.svganim.min.js
new file mode 100644
index 0000000..3cc4020
--- /dev/null
+++ b/src/web/js/jquery.svganim.min.js
@@ -0,0 +1,7 @@
+/* http://keith-wood.name/svg.html
+   SVG attribute animations for jQuery v1.4.5.
+   Written by Keith Wood (kbwood{at}iinet.com.au) June 2008.
+   Dual licensed under the GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) and 
+   MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt) licenses. 
+   Please attribute the author if you use it. */
+(function($){$.each(['x','y','width','height','rx','ry','cx','cy','r','x1','y1','x2','y2','stroke-width','strokeWidth','opacity','fill-opacity','fillOpacity','stroke-opacity','strokeOpacity','stroke-dashoffset','strokeDashOffset','font-size','fontSize','font-weight','fontWeight','letter-spacing','letterSpacing','word-spacing','wordSpacing'],function(i,f){var g=f.charAt(0).toUpperCase()+f.substr(1);if($.cssProps){$.cssProps['svg'+g]=$.cssProps['svg-'+f]=f}$.fx.step['svg'+g]=$.fx.step['svg-'+f]=function(a){var b=$.svg._attrNames[f]||f;var c=a.elem.attributes.getNamedItem(b);if(!a.set){a.start=(c?parseFloat(c.nodeValue):0);var d=($.fn.jquery>='1.6'?'':a.options.curAnim['svg'+g]||a.options.curAnim['svg-'+f]);if(/^[+-]=/.exec(d)){a.end=a.start+parseFloat(d.replace(/=/,''))}$(a.elem).css(b,'');a.set=true}var e=(a.pos*(a.end-a.start)+a.start)+(a.unit=='%'?'%':'');(c?c.nodeValue=e:a.elem.setAttribute(b,e))}});$.fx.step['svgStrokeDashArray']=$.fx.step['svg-strokeDashArray']=$.fx.step['svgStroke-dasharray']=$.fx.step['svg-stroke-dasharray']=function(a){var b=a.elem.attributes.getNamedItem('stroke-dasharray');if(!a.set){a.start=parseDashArray(b?b.nodeValue:'');var c=($.fn.jquery>='1.6'?a.end:a.options.curAnim['svgStrokeDashArray']||a.options.curAnim['svg-strokeDashArray']||a.options.curAnim['svgStroke-dasharray']||a.options.curAnim['svg-stroke-dasharray']);a.end=parseDashArray(c);if(/^[+-]=/.exec(c)){c=c.split(/[, ]+/);if(c.length%2==1){var d=c.length;for(var i=0;i<d;i++){c.push(c[i])}}for(var i=0;i<c.length;i++){if(/^[+-]=/.exec(c[i])){a.end[i]=a.start[i]+parseFloat(c[i].replace(/=/,''))}}}a.set=true}var e=$.map(a.start,function(n,i){return(a.pos*(a.end[i]-n)+n)}).join(',');(b?b.nodeValue=e:a.elem.setAttribute('stroke-dasharray',e))};function parseDashArray(a){var b=a.split(/[, ]+/);for(var i=0;i<b.length;i++){b[i]=parseFloat(b[i]);if(isNaN(b[i])){b[i]=0}}if(b.length%2==1){var c=b.length;for(var i=0;i<c;i++){b.push(b[i])}}return b}$.fx.step['svgViewBox']=$.fx.step['svg-viewBox']=function(a){var b=a.elem.attributes.getNamedItem('viewBox');if(!a.set){a.start=parseViewBox(b?b.nodeValue:'');var c=($.fn.jquery>='1.6'?a.end:a.options.curAnim['svgViewBox']||a.options.curAnim['svg-viewBox']);a.end=parseViewBox(c);if(/^[+-]=/.exec(c)){c=c.split(/[, ]+/);while(c.length<4){c.push('0')}for(var i=0;i<4;i++){if(/^[+-]=/.exec(c[i])){a.end[i]=a.start[i]+parseFloat(c[i].replace(/=/,''))}}}a.set=true}var d=$.map(a.start,function(n,i){return(a.pos*(a.end[i]-n)+n)}).join(' ');(b?b.nodeValue=d:a.elem.setAttribute('viewBox',d))};function parseViewBox(a){var b=a.split(/[, ]+/);for(var i=0;i<b.length;i++){b[i]=parseFloat(b[i]);if(isNaN(b[i])){b[i]=0}}while(b.length<4){b.push(0)}return b}$.fx.step['svgTransform']=$.fx.step['svg-transform']=function(a){var b=a.elem.attributes.getNamedItem('transform');if(!a.set){a.start=parseTransform(b?b.nodeValue:'');a.end=parseTransform(a.end,a.start);a.set=true}var c='';for(var i=0;i<a.end.order.length;i++){switch(a.end.order.charAt(i)){case't':c+=' translate('+(a.pos*(a.end.translateX-a.start.translateX)+a.start.translateX)+','+(a.pos*(a.end.translateY-a.start.translateY)+a.start.translateY)+')';break;case's':c+=' scale('+(a.pos*(a.end.scaleX-a.start.scaleX)+a.start.scaleX)+','+(a.pos*(a.end.scaleY-a.start.scaleY)+a.start.scaleY)+')';break;case'r':c+=' rotate('+(a.pos*(a.end.rotateA-a.start.rotateA)+a.start.rotateA)+','+(a.pos*(a.end.rotateX-a.start.rotateX)+a.start.rotateX)+','+(a.pos*(a.end.rotateY-a.start.rotateY)+a.start.rotateY)+')';break;case'x':c+=' skewX('+(a.pos*(a.end.skewX-a.start.skewX)+a.start.skewX)+')';case'y':c+=' skewY('+(a.pos*(a.end.skewY-a.start.skewY)+a.start.skewY)+')';break;case'm':var d='';for(var j=0;j<6;j++){d+=','+(a.pos*(a.end.matrix[j]-a.start.matrix[j])+a.start.matrix[j])}c+=' matrix('+d.substr(1)+')';break}}(b?b.nodeValue=c:a.elem.setAttribute('transform',c))};function parseTransform(a,b){a=a||'';if(typeof a=='object'){a=a.nodeValue}var c=$.extend({translateX:0,translateY:0,scaleX:0,scaleY:0,rotateA:0,rotateX:0,rotateY:0,skewX:0,skewY:0,matrix:[0,0,0,0,0,0]},b||{});c.order='';var d=/([a-zA-Z]+)\(\s*([+-]?[\d\.]+)\s*(?:[\s,]\s*([+-]?[\d\.]+)\s*(?:[\s,]\s*([+-]?[\d\.]+)\s*(?:[\s,]\s*([+-]?[\d\.]+)\s*[\s,]\s*([+-]?[\d\.]+)\s*[\s,]\s*([+-]?[\d\.]+)\s*)?)?)?\)/g;var e=d.exec(a);while(e){switch(e[1]){case'translate':c.order+='t';c.translateX=parseFloat(e[2]);c.translateY=(e[3]?parseFloat(e[3]):0);break;case'scale':c.order+='s';c.scaleX=parseFloat(e[2]);c.scaleY=(e[3]?parseFloat(e[3]):c.scaleX);break;case'rotate':c.order+='r';c.rotateA=parseFloat(e[2]);c.rotateX=(e[3]?parseFloat(e[3]):0);c.rotateY=(e[4]?parseFloat(e[4]):0);break;case'skewX':c.order+='x';c.skewX=parseFloat(e[2]);break;case'skewY':c.order+='y';c.skewY=parseFloat(e[2]);break;case'matrix':c.order+='m';c.matrix=[parseFloat(e[2]),parseFloat(e[3]),parseFloat(e[4]),parseFloat(e[5]),parseFloat(e[6]),parseFloat(e[7])];break}e=d.exec(a)}if(c.order=='m'&&Math.abs(c.matrix[0])==Math.abs(c.matrix[3])&&c.matrix[1]!=0&&Math.abs(c.matrix[1])==Math.abs(c.matrix[2])){var f=Math.acos(c.matrix[0])*180/Math.PI;f=(c.matrix[1]<0?360-f:f);c.order='rt';c.rotateA=f;c.rotateX=c.rotateY=0;c.translateX=c.matrix[4];c.translateY=c.matrix[5]}return c}$.each(['fill','stroke'],function(i,e){var f=e.charAt(0).toUpperCase()+e.substr(1);$.fx.step['svg'+f]=$.fx.step['svg-'+e]=function(a){if(!a.set){a.start=$.svg._getColour(a.elem,e);var b=(a.end=='none');a.end=(b?$.svg._getColour(a.elem.parentNode,e):$.svg._getRGB(a.end));a.end[3]=b;$(a.elem).css(e,'');a.set=true}var c=a.elem.attributes.getNamedItem(e);var d='rgb('+[Math.min(Math.max(parseInt((a.pos*(a.end[0]-a.start[0]))+a.start[0],10),0),255),Math.min(Math.max(parseInt((a.pos*(a.end[1]-a.start[1]))+a.start[1],10),0),255),Math.min(Math.max(parseInt((a.pos*(a.end[2]-a.start[2]))+a.start[2],10),0),255)].join(',')+')';d=(a.end[3]&&a.state==1?'none':d);(c?c.nodeValue=d:a.elem.setAttribute(e,d))}});$.svg._getColour=function(a,b){a=$(a);var c;do{c=a.attr(b)||a.css(b);if((c!=''&&c!='none')||a.hasClass($.svg.markerClassName)){break}}while(a=a.parent());return $.svg._getRGB(c)};$.svg._getRGB=function(a){var b;if(a&&a.constructor==Array){return(a.length==3||a.length==4?a:h['none'])}if(b=/^rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)$/.exec(a)){return[parseInt(b[1],10),parseInt(b[2],10),parseInt(b[3],10)]}if(b=/^rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)$/.exec(a)){return[parseFloat(b[1])*2.55,parseFloat(b[2])*2.55,parseFloat(b[3])*2.55]}if(b=/^#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})$/.exec(a)){return[parseInt(b[1],16),parseInt(b[2],16),parseInt(b[3],16)]}if(b=/^#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])$/.exec(a)){return[parseInt(b[1]+b[1],16),parseInt(b[2]+b[2],16),parseInt(b[3]+b[3],16)]}return h[$.trim(a).toLowerCase()]||h['none']};var h={'':[255,255,255,1],none:[255,255,255,1],aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],grey:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]}})(jQuery);
\ No newline at end of file
diff --git a/src/web/js/jquery.svgdom.js b/src/web/js/jquery.svgdom.js
new file mode 100644
index 0000000..b5ad8af
--- /dev/null
+++ b/src/web/js/jquery.svgdom.js
@@ -0,0 +1,406 @@
+/* http://keith-wood.name/svg.html
+   jQuery DOM compatibility for jQuery SVG v1.4.5.
+   Written by Keith Wood (kbwood{at}iinet.com.au) April 2009.
+   Dual licensed under the GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) and 
+   MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt) licenses. 
+   Please attribute the author if you use it. */
+
+(function($) { // Hide scope, no $ conflict
+
+/* Support adding class names to SVG nodes. */
+$.fn.addClass = function(origAddClass) {
+	return function(classNames) {
+		classNames = classNames || '';
+		return this.each(function() {
+			if ($.svg.isSVGElem(this)) {
+				var node = this;
+				$.each(classNames.split(/\s+/), function(i, className) {
+					var classes = (node.className ? node.className.baseVal : node.getAttribute('class'));
+					if ($.inArray(className, classes.split(/\s+/)) == -1) {
+						classes += (classes ? ' ' : '') + className;
+						(node.className ? node.className.baseVal = classes :
+							node.setAttribute('class',  classes));
+					}
+				});
+			}
+			else {
+				origAddClass.apply($(this), [classNames]);
+			}
+		});
+	};
+}($.fn.addClass);
+
+/* Support removing class names from SVG nodes. */
+$.fn.removeClass = function(origRemoveClass) {
+	return function(classNames) {
+		classNames = classNames || '';
+		return this.each(function() {
+			if ($.svg.isSVGElem(this)) {
+				var node = this;
+				$.each(classNames.split(/\s+/), function(i, className) {
+					var classes = (node.className ? node.className.baseVal : node.getAttribute('class'));
+					classes = $.grep(classes.split(/\s+/), function(n, i) { return n != className; }).
+						join(' ');
+					(node.className ? node.className.baseVal = classes :
+						node.setAttribute('class', classes));
+				});
+			}
+			else {
+				origRemoveClass.apply($(this), [classNames]);
+			}
+		});
+	};
+}($.fn.removeClass);
+
+/* Support toggling class names on SVG nodes. */
+$.fn.toggleClass = function(origToggleClass) {
+	return function(className, state) {
+		return this.each(function() {
+			if ($.svg.isSVGElem(this)) {
+				if (typeof state !== 'boolean') {
+					state = !$(this).hasClass(className);
+				}
+				$(this)[(state ? 'add' : 'remove') + 'Class'](className);
+			}
+			else {
+				origToggleClass.apply($(this), [className, state]);
+			}
+		});
+	};
+}($.fn.toggleClass);
+
+/* Support checking class names on SVG nodes. */
+$.fn.hasClass = function(origHasClass) {
+	return function(className) {
+		className = className || '';
+		var found = false;
+		this.each(function() {
+			if ($.svg.isSVGElem(this)) {
+				var classes = (this.className ? this.className.baseVal :
+					this.getAttribute('class')).split(/\s+/);
+				found = ($.inArray(className, classes) > -1);
+			}
+			else {
+				found = (origHasClass.apply($(this), [className]));
+			}
+			return !found;
+		});
+		return found;
+	};
+}($.fn.hasClass);
+
+/* Support attributes on SVG nodes. */
+$.fn.attr = function(origAttr) {
+	return function(name, value, type) {
+		if (typeof name === 'string' && value === undefined) {
+			var val = origAttr.apply(this, [name]);
+			if (val && val.baseVal && val.baseVal.numberOfItems != null) { // Multiple values
+				value = '';
+				val = val.baseVal;
+				if (name == 'transform') {
+					for (var i = 0; i < val.numberOfItems; i++) {
+						var item = val.getItem(i);
+						switch (item.type) {
+							case 1: value += ' matrix(' + item.matrix.a + ',' + item.matrix.b + ',' +
+										item.matrix.c + ',' + item.matrix.d + ',' +
+										item.matrix.e + ',' + item.matrix.f + ')';
+									break;
+							case 2: value += ' translate(' + item.matrix.e + ',' + item.matrix.f + ')'; break;
+							case 3: value += ' scale(' + item.matrix.a + ',' + item.matrix.d + ')'; break;
+							case 4: value += ' rotate(' + item.angle + ')'; break; // Doesn't handle new origin
+							case 5: value += ' skewX(' + item.angle + ')'; break;
+							case 6: value += ' skewY(' + item.angle + ')'; break;
+						}
+					}
+					val = value.substring(1);
+				}
+				else {
+					val = val.getItem(0).valueAsString;
+				}
+			}
+			return (val && val.baseVal ? val.baseVal.valueAsString : val);
+		}
+
+		var options = name;
+		if (typeof name === 'string') {
+			options = {};
+			options[name] = value;
+		}
+		return this.each(function() {
+			if ($.svg.isSVGElem(this)) {
+				for (var n in options) {
+					var val = ($.isFunction(options[n]) ? options[n]() : options[n]);
+					(type ? this.style[n] = val : this.setAttribute(n, val));
+				}
+			}
+			else {
+				origAttr.apply($(this), [name, value, type]);
+			}
+		});
+	};
+}($.fn.attr);
+
+/* Support removing attributes on SVG nodes. */
+$.fn.removeAttr = function(origRemoveAttr) {
+	return function(name) {
+		return this.each(function() {
+			if ($.svg.isSVGElem(this)) {
+				(this[name] && this[name].baseVal ? this[name].baseVal.value = '' :
+					this.setAttribute(name, ''));
+			}
+			else {
+				origRemoveAttr.apply($(this), [name]);
+			}
+		});
+	};
+}($.fn.removeAttr);
+
+/* Add numeric only properties. */
+$.extend($.cssNumber, {
+	'stopOpacity': true,
+	'strokeMitrelimit': true,
+	'strokeOpacity': true
+});
+
+/* Support retrieving CSS/attribute values on SVG nodes. */
+if ($.cssProps) {
+	$.css = function(origCSS) {
+		return function(elem, name, extra) {
+			var value = (name.match(/^svg.*/) ? $(elem).attr($.cssProps[name] || name) : '');
+			return value || origCSS(elem, name, extra);
+		};
+	}($.css);
+}
+  
+/* Determine if any nodes are SVG nodes. */
+function anySVG(checkSet) {
+	for (var i = 0; i < checkSet.length; i++) {
+		if (checkSet[i].nodeType == 1 && checkSet[i].namespaceURI == $.svg.svgNS) {
+			return true;
+		}
+	}
+	return false;
+}
+
+/* Update Sizzle selectors. */
+
+$.expr.relative['+'] = function(origRelativeNext) {
+	return function(checkSet, part, isXML) {
+		origRelativeNext(checkSet, part, isXML || anySVG(checkSet));
+	};
+}($.expr.relative['+']);
+
+$.expr.relative['>'] = function(origRelativeChild) {
+	return function(checkSet, part, isXML) {
+		origRelativeChild(checkSet, part, isXML || anySVG(checkSet));
+	};
+}($.expr.relative['>']);
+
+$.expr.relative[''] = function(origRelativeDescendant) {
+	return function(checkSet, part, isXML) {
+		origRelativeDescendant(checkSet, part, isXML || anySVG(checkSet));
+	};
+}($.expr.relative['']);
+
+$.expr.relative['~'] = function(origRelativeSiblings) {
+	return function(checkSet, part, isXML) {
+		origRelativeSiblings(checkSet, part, isXML || anySVG(checkSet));
+	};
+}($.expr.relative['~']);
+
+$.expr.find.ID = function(origFindId) {
+	return function(match, context, isXML) {
+		return ($.svg.isSVGElem(context) ?
+			[context.ownerDocument.getElementById(match[1])] :
+			origFindId(match, context, isXML));
+	};
+}($.expr.find.ID);
+
+var div = document.createElement('div');
+div.appendChild(document.createComment(''));
+if (div.getElementsByTagName('*').length > 0) { // Make sure no comments are found
+	$.expr.find.TAG = function(match, context) {
+		var results = context.getElementsByTagName(match[1]);
+		if (match[1] === '*') { // Filter out possible comments
+			var tmp = [];
+			for (var i = 0; results[i] || results.item(i); i++) {
+				if ((results[i] || results.item(i)).nodeType === 1) {
+					tmp.push(results[i] || results.item(i));
+				}
+			}
+			results = tmp;
+		}
+		return results;
+	};
+}
+
+$.expr.preFilter.CLASS = function(match, curLoop, inplace, result, not, isXML) {
+	match = ' ' + match[1].replace(/\\/g, '') + ' ';
+	if (isXML) {
+		return match;
+	}
+	for (var i = 0, elem = {}; elem != null; i++) {
+		elem = curLoop[i];
+		if (!elem) {
+			try {
+				elem = curLoop.item(i);
+			}
+			catch (e) {
+				// Ignore
+			}
+		}
+		if (elem) {
+			var className = (!$.svg.isSVGElem(elem) ? elem.className :
+				(elem.className ? elem.className.baseVal : '') || elem.getAttribute('class'));
+			if (not ^ (className && (' ' + className + ' ').indexOf(match) > -1)) {
+				if (!inplace)
+					result.push(elem);
+			}
+			else if (inplace) {
+				curLoop[i] = false;
+			}
+		}
+	}
+	return false;
+};
+
+$.expr.filter.CLASS = function(elem, match) {
+	var className = (!$.svg.isSVGElem(elem) ? elem.className :
+		(elem.className ? elem.className.baseVal : elem.getAttribute('class')));
+	return (' ' + className + ' ').indexOf(match) > -1;
+};
+
+$.expr.filter.ATTR = function(origFilterAttr) {
+	return function(elem, match) {
+		var handler = null;
+		if ($.svg.isSVGElem(elem)) {
+			handler = match[1];
+			$.expr.attrHandle[handler] = function(elem){
+				var attr = elem.getAttribute(handler);
+				return attr && attr.baseVal || attr;
+			};
+		}
+		var filter = origFilterAttr(elem, match);
+		if (handler) {
+			$.expr.attrHandle[handler] = null;
+		}
+		return filter;
+	};
+}($.expr.filter.ATTR);
+
+/*
+	In the removeData function (line 1881, v1.7.2):
+
+				if ( jQuery.support.deleteExpando ) {
+					delete elem[ internalKey ];
+				} else {
+					try { // SVG
+						elem.removeAttribute( internalKey );
+					} catch (e) {
+						elem[ internalKey ] = null;
+					}
+				}
+
+	In the event.add function (line 2985, v1.7.2):
+
+				if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
+					// Bind the global event handler to the element
+					try { // SVG
+						elem.addEventListener( type, eventHandle, false );
+					} catch(e) {
+						if ( elem.attachEvent ) {
+							elem.attachEvent( "on" + type, eventHandle );
+						}
+					}
+				}
+
+	In the event.remove function (line 3074, v1.7.2):
+
+			if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) {
+				try { // SVG
+					elem.removeEventListener(type, elemData.handle, false);
+				}
+				catch (e) {
+					if (elem.detachEvent) {
+						elem.detachEvent("on" + type, elemData.handle);
+					}
+				}
+			}
+
+	In the event.fix function (line 3394, v1.7.2):
+
+		if (event.target.namespaceURI == 'http://www.w3.org/2000/svg') { // SVG
+			event.button = [1, 4, 2][event.button];
+		}
+
+		// Add which for click: 1 === left; 2 === middle; 3 === right
+		// Note: button is not normalized, so don't use it
+		if ( !event.which && button !== undefined ) {
+			event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) );
+		}
+
+	In the Sizzle function (line 4083, v1.7.2):
+
+	if ( toString.call(checkSet) === "[object Array]" ) {
+		if ( !prune ) {
+			results.push.apply( results, checkSet );
+
+		} else if ( context && context.nodeType === 1 ) {
+			for ( i = 0; checkSet[i] != null; i++ ) {
+				if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) {
+				results.push( set[i] || set.item(i) ); // SVG
+				}
+			}
+
+		} else {
+			for ( i = 0; checkSet[i] != null; i++ ) {
+				if ( checkSet[i] && checkSet[i].nodeType === 1 ) {
+					results.push( set[i] || set.item(i) ); // SVG
+				}
+			}
+		}
+	} else {...
+
+	In the fallback for the Sizzle makeArray function (line 4877, v1.7.2):
+
+	if ( toString.call(array) === "[object Array]" ) {
+		Array.prototype.push.apply( ret, array );
+
+	} else {
+		if ( typeof array.length === "number" ) {
+			for ( var l = array.length; i &lt; l; i++ ) {
+				ret.push( array[i] || array.item(i) ); // SVG
+			}
+
+		} else {
+			for ( ; array[i]; i++ ) {
+				ret.push( array[i] );
+			}
+		}
+	}
+
+	In the jQuery.cleandata function (line 6538, v1.7.2):
+
+				if ( deleteExpando ) {
+					delete elem[ jQuery.expando ];
+
+				} else {
+					try { // SVG
+						elem.removeAttribute( jQuery.expando );
+					} catch (e) {
+						// Ignore
+					}
+				}
+
+	In the fallback getComputedStyle function (line 6727, v1.7.2):
+
+		defaultView = (elem.ownerDocument ? elem.ownerDocument.defaultView : elem.defaultView); // SVG
+		if ( defaultView &&
+		(computedStyle = defaultView.getComputedStyle( elem, null )) ) {
+
+			ret = computedStyle.getPropertyValue( name );
+			...
+
+*/
+
+})(jQuery);
diff --git a/src/web/js/jquery.svgdom.min.js b/src/web/js/jquery.svgdom.min.js
new file mode 100644
index 0000000..3c280a5
--- /dev/null
+++ b/src/web/js/jquery.svgdom.min.js
@@ -0,0 +1,7 @@
+/* http://keith-wood.name/svg.html
+   jQuery DOM compatibility for jQuery SVG v1.4.5.
+   Written by Keith Wood (kbwood{at}iinet.com.au) April 2009.
+   Dual licensed under the GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) and 
+   MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt) licenses. 
+   Please attribute the author if you use it. */
+(function($){$.fn.addClass=function(e){return function(d){d=d||'';return this.each(function(){if($.svg.isSVGElem(this)){var c=this;$.each(d.split(/\s+/),function(i,a){var b=(c.className?c.className.baseVal:c.getAttribute('class'));if($.inArray(a,b.split(/\s+/))==-1){b+=(b?' ':'')+a;(c.className?c.className.baseVal=b:c.setAttribute('class',b))}})}else{e.apply($(this),[d])}})}}($.fn.addClass);$.fn.removeClass=function(e){return function(d){d=d||'';return this.each(function(){if($.svg.isSVGElem(this)){var c=this;$.each(d.split(/\s+/),function(i,a){var b=(c.className?c.className.baseVal:c.getAttribute('class'));b=$.grep(b.split(/\s+/),function(n,i){return n!=a}).join(' ');(c.className?c.className.baseVal=b:c.setAttribute('class',b))})}else{e.apply($(this),[d])}})}}($.fn.removeClass);$.fn.toggleClass=function(c){return function(a,b){return this.each(function(){if($.svg.isSVGElem(this)){if(typeof b!=='boolean'){b=!$(this).hasClass(a)}$(this)[(b?'add':'remove')+'Class'](a)}else{c.apply($(this),[a,b])}})}}($.fn.toggleClass);$.fn.hasClass=function(d){return function(b){b=b||'';var c=false;this.each(function(){if($.svg.isSVGElem(this)){var a=(this.className?this.className.baseVal:this.getAttribute('class')).split(/\s+/);c=($.inArray(b,a)>-1)}else{c=(d.apply($(this),[b]))}return!c});return c}}($.fn.hasClass);$.fn.attr=function(h){return function(b,c,d){if(typeof b==='string'&&c===undefined){var e=h.apply(this,[b]);if(e&&e.baseVal&&e.baseVal.numberOfItems!=null){c='';e=e.baseVal;if(b=='transform'){for(var i=0;i<e.numberOfItems;i++){var f=e.getItem(i);switch(f.type){case 1:c+=' matrix('+f.matrix.a+','+f.matrix.b+','+f.matrix.c+','+f.matrix.d+','+f.matrix.e+','+f.matrix.f+')';break;case 2:c+=' translate('+f.matrix.e+','+f.matrix.f+')';break;case 3:c+=' scale('+f.matrix.a+','+f.matrix.d+')';break;case 4:c+=' rotate('+f.angle+')';break;case 5:c+=' skewX('+f.angle+')';break;case 6:c+=' skewY('+f.angle+')';break}}e=c.substring(1)}else{e=e.getItem(0).valueAsString}}return(e&&e.baseVal?e.baseVal.valueAsString:e)}var g=b;if(typeof b==='string'){g={};g[b]=c}return this.each(function(){if($.svg.isSVGElem(this)){for(var n in g){var a=($.isFunction(g[n])?g[n]():g[n]);(d?this.style[n]=a:this.setAttribute(n,a))}}else{h.apply($(this),[b,c,d])}})}}($.fn.attr);$.fn.removeAttr=function(b){return function(a){return this.each(function(){if($.svg.isSVGElem(this)){(this[a]&&this[a].baseVal?this[a].baseVal.value='':this.setAttribute(a,''))}else{b.apply($(this),[a])}})}}($.fn.removeAttr);$.extend($.cssNumber,{'stopOpacity':true,'strokeMitrelimit':true,'strokeOpacity':true});if($.cssProps){$.css=function(e){return function(a,b,c){var d=(b.match(/^svg.*/)?$(a).attr($.cssProps[b]||b):'');return d||e(a,b,c)}}($.css)}function anySVG(a){for(var i=0;i<a.length;i++){if(a[i].nodeType==1&&a[i].namespaceURI==$.svg.svgNS){return true}}return false}$.expr.relative['+']=function(d){return function(a,b,c){d(a,b,c||anySVG(a))}}($.expr.relative['+']);$.expr.relative['>']=function(d){return function(a,b,c){d(a,b,c||anySVG(a))}}($.expr.relative['>']);$.expr.relative['']=function(d){return function(a,b,c){d(a,b,c||anySVG(a))}}($.expr.relative['']);$.expr.relative['~']=function(d){return function(a,b,c){d(a,b,c||anySVG(a))}}($.expr.relative['~']);$.expr.find.ID=function(d){return function(a,b,c){return($.svg.isSVGElem(b)?[b.ownerDocument.getElementById(a[1])]:d(a,b,c))}}($.expr.find.ID);var j=document.createElement('div');j.appendChild(document.createComment(''));if(j.getElementsByTagName('*').length>0){$.expr.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==='*'){var d=[];for(var i=0;c[i]||c.item(i);i++){if((c[i]||c.item(i)).nodeType===1){d.push(c[i]||c.item(i))}}c=d}return c}}$.expr.preFilter.CLASS=function(a,b,c,d,f,g){a=' '+a[1].replace(/\\/g,'')+' ';if(g){return a}for(var i=0,elem={};elem!=null;i++){elem=b[i];if(!elem){try{elem=b.item(i)}catch(e){}}if(elem){var h=(!$.svg.isSVGElem(elem)?elem.className:(elem.className?elem.className.baseVal:'')||elem.getAttribute('class'));if(f^(h&&(' '+h+' ').indexOf(a)>-1)){if(!c)d.push(elem)}else if(c){b[i]=false}}}return false};$.expr.filter.CLASS=function(a,b){var c=(!$.svg.isSVGElem(a)?a.className:(a.className?a.className.baseVal:a.getAttribute('class')));return(' '+c+' ').indexOf(b)>-1};$.expr.filter.ATTR=function(g){return function(c,d){var e=null;if($.svg.isSVGElem(c)){e=d[1];$.expr.attrHandle[e]=function(a){var b=a.getAttribute(e);return b&&b.baseVal||b}}var f=g(c,d);if(e){$.expr.attrHandle[e]=null}return f}}($.expr.filter.ATTR)})(jQuery);
\ No newline at end of file
diff --git a/src/web/js/jquery.svgfilter.min.js b/src/web/js/jquery.svgfilter.min.js
new file mode 100644
index 0000000..551bdc9
--- /dev/null
+++ b/src/web/js/jquery.svgfilter.min.js
@@ -0,0 +1,7 @@
+/* http://keith-wood.name/svg.html
+   SVG filters for jQuery v1.4.5.
+   Written by Keith Wood (kbwood{at}iinet.com.au) August 2007.
+   Dual licensed under the GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) and
+   MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt) licenses.
+   Please attribute the author if you use it. */
+(function($){$.svg.addExtension('filters',SVGFilter);$.extend($.svg._wrapperClass.prototype,{filter:function(a,b,x,y,c,d,e){var f=this._args(arguments,['id','x','y','width','height']);return this._makeNode(f.parent,'filter',$.extend({id:f.id,x:f.x,y:f.y,width:f.width,height:f.height},f.settings||{}))}});function SVGFilter(a){this._wrapper=a}$.extend(SVGFilter.prototype,{distantLight:function(a,b,c,d,e){var f=this._wrapper._args(arguments,['result','azimuth','elevation']);return this._wrapper._makeNode(f.parent,'feDistantLight',$.extend({result:f.result,azimuth:f.azimuth,elevation:f.elevation},f.settings||{}))},pointLight:function(a,b,x,y,z,c){var d=this._wrapper._args(arguments,['result','x','y','z']);return this._wrapper._makeNode(d.parent,'fePointLight',$.extend({result:d.result,x:d.x,y:d.y,z:d.z},d.settings||{}))},spotLight:function(a,b,x,y,z,c,d,e,f){var g=this._wrapper._args(arguments,['result','x','y','z','toX','toY','toZ'],['toX']);var h=$.extend({result:g.result,x:g.x,y:g.y,z:g.z},(g.toX!=null?{pointsAtX:g.toX,pointsAtY:g.toY,pointsAtZ:g.toZ}:{}));return this._wrapper._makeNode(g.parent,'feSpotLight',$.extend(h,g.settings||{}))},blend:function(a,b,c,d,e,f){var g=this._wrapper._args(arguments,['result','mode','in1','in2']);return this._wrapper._makeNode(g.parent,'feBlend',$.extend({result:g.result,mode:g.mode,in_:g.in1,in2:g.in2},g.settings||{}))},colorMatrix:function(a,b,c,d,e,f){var g=this._wrapper._args(arguments,['result','in1','type','values']);if(isArray(g.values)){var h='';for(var i=0;i<g.values.length;i++){h+=(i==0?'':' ')+g.values[i].join(' ')}g.values=h}else if(typeof g.values=='object'){g.settings=g.values;g.values=null}var j=$.extend({result:g.result,in_:g.in1,type:g.type},(g.values!=null?{values:g.values}:{}));return this._wrapper._makeNode(g.parent,'feColorMatrix',$.extend(j,g.settings||{}))},componentTransfer:function(a,b,c,d){var e=this._wrapper._args(arguments,['result','functions']);var f=this._wrapper._makeNode(e.parent,'feComponentTransfer',$.extend({result:e.result},e.settings||{}));var g=['R','G','B','A'];for(var i=0;i<Math.min(4,e.functions.length);i++){var h=e.functions[i];var j=$.extend({type:h[0]},(h[0]=='table'||h[0]=='discrete'?{tableValues:h[1].join(' ')}:(h[0]=='linear'?{slope:h[1],intercept:h[2]}:(h[0]=='gamma'?{amplitude:h[1],exponent:h[2],offset:h[3]}:{}))));this._wrapper._makeNode(f,'feFunc'+g[i],j)}return f},composite:function(a,b,c,d,e,f,g,h,i,j){var k=this._wrapper._args(arguments,['result','operator','in1','in2','k1','k2','k3','k4'],['k1']);var l=$.extend({result:k.result,operator:k.operator,'in':k.in1,in2:k.in2},(k.k1!=null?{k1:k.k1,k2:k.k2,k3:k.k3,k4:k.k4}:{}));return this._wrapper._makeNode(k.parent,'feComposite',$.extend(l,k.settings||{}))},convolveMatrix:function(a,b,c,d,e){var f=this._wrapper._args(arguments,['result','order','matrix']);var g='';for(var i=0;i<f.matrix.length;i++){g+=(i==0?'':' ')+f.matrix[i].join(' ')}f.matrix=g;return this._wrapper._makeNode(f.parent,'feConvolveMatrix',$.extend({result:f.result,order:f.order,kernelMatrix:f.matrix},f.settings||{}))},diffuseLighting:function(a,b,c,d){var e=this._wrapper._args(arguments,['result','colour'],['colour']);return this._wrapper._makeNode(e.parent,'feDiffuseLighting',$.extend($.extend({result:e.result},(e.colour?{lightingColor:e.colour}:{})),e.settings||{}))},displacementMap:function(a,b,c,d,e){var f=this._wrapper._args(arguments,['result','in1','in2']);return this._wrapper._makeNode(f.parent,'feDisplacementMap',$.extend({result:f.result,in_:f.in1,in2:f.in2},f.settings||{}))},flood:function(a,b,x,y,c,d,e,f,g){var h=this._wrapper._args(arguments,['result','x','y','width','height','colour','opacity']);if(arguments.length<6){h.colour=h.x;h.opacity=h.y;h.settings=h.width;h.x=null}var i=$.extend({result:h.result,floodColor:h.colour,floodOpacity:h.opacity},(h.x!=null?{x:h.x,y:h.y,width:h.width,height:h.height}:{}));return this._wrapper._makeNode(h.parent,'feFlood',$.extend(i,h.settings||{}))},gaussianBlur:function(a,b,c,d,e,f){var g=this._wrapper._args(arguments,['result','in1','stdDevX','stdDevY'],['stdDevY']);return this._wrapper._makeNode(g.parent,'feGaussianBlur',$.extend({result:g.result,in_:g.in1,stdDeviation:g.stdDevX+(g.stdDevY?' '+g.stdDevY:'')},g.settings||{}))},image:function(a,b,c,d){var e=this._wrapper._args(arguments,['result','href']);var f=this._wrapper._makeNode(e.parent,'feImage',$.extend({result:e.result},e.settings||{}));f.setAttributeNS($.svg.xlinkNS,'href',e.href);return f},merge:function(a,b,c,d){var e=this._wrapper._args(arguments,['result','refs']);var f=this._wrapper._makeNode(e.parent,'feMerge',$.extend({result:e.result},e.settings||{}));for(var i=0;i<e.refs.length;i++){this._wrapper._makeNode(f,'feMergeNode',{in_:e.refs[i]})}return f},morphology:function(a,b,c,d,e,f,g){var h=this._wrapper._args(arguments,['result','in1','operator','radiusX','radiusY'],['radiusY']);return this._wrapper._makeNode(h.parent,'feMorphology',$.extend({result:h.result,in_:h.in1,operator:h.operator,radius:h.radiusX+(h.radiusY?' '+h.radiusY:'')},h.settings||{}))},offset:function(a,b,c,d,e,f){var g=this._wrapper._args(arguments,['result','in1','dx','dy']);return this._wrapper._makeNode(g.parent,'feOffset',$.extend({result:g.result,in_:g.in1,dx:g.dx,dy:g.dy},g.settings||{}))},specularLighting:function(a,b,c,d,e,f,g){var h=this._wrapper._args(arguments,['result','in1','surfaceScale','specularConstant','specularExponent'],['surfaceScale','specularConstant','specularExponent']);return this._wrapper._makeNode(h.parent,'feSpecularLighting',$.extend({result:h.result,in_:h.in1,surfaceScale:h.surfaceScale,specularConstant:h.specularConstant,specularExponent:h.specularExponent},h.settings||{}))},tile:function(a,b,c,x,y,d,e,f){var g=this._wrapper._args(arguments,['result','in1','x','y','width','height']);return this._wrapper._makeNode(g.parent,'feTile',$.extend({result:g.result,in_:g.in1,x:g.x,y:g.y,width:g.width,height:g.height},g.settings||{}))},turbulence:function(a,b,c,d,e,f){var g=this._wrapper._args(arguments,['result','type','baseFreq','octaves'],['octaves']);return this._wrapper._makeNode(g.parent,'feTurbulence',$.extend({result:g.result,type:g.type,baseFrequency:g.baseFreq,numOctaves:g.octaves},g.settings||{}))}});function isArray(a){return(a&&a.constructor==Array)}})(jQuery)
\ No newline at end of file