Browse Source

ZOOKEEPER-773. Log visualisation

git-svn-id: https://svn.apache.org/repos/asf/hadoop/zookeeper/trunk@953041 13f79535-47bb-0310-9956-ffa450edef68
Patrick D. Hunt 15 years ago
parent
commit
6bb8b641c6
59 changed files with 9729 additions and 0 deletions
  1. 2 0
      CHANGES.txt
  2. 69 0
      src/contrib/loggraph/README.txt
  3. 43 0
      src/contrib/loggraph/bin/loggraph-dev.sh
  4. 48 0
      src/contrib/loggraph/bin/loggraph.sh
  5. 70 0
      src/contrib/loggraph/build.xml
  6. 41 0
      src/contrib/loggraph/ivy.xml
  7. 22 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/FilterException.java
  8. 75 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/FilterOp.java
  9. 131 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/FilterParser.java
  10. 226 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/JsonGenerator.java
  11. 40 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/Log4JEntry.java
  12. 380 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/Log4JSource.java
  13. 46 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/LogEntry.java
  14. 26 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/LogIterator.java
  15. 66 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/LogServer.java
  16. 94 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/LogSkipList.java
  17. 33 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/LogSource.java
  18. 103 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/MeasureThroughput.java
  19. 218 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/MergedLogSource.java
  20. 327 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/RandomAccessFileReader.java
  21. 59 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/TransactionEntry.java
  22. 377 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/TxnLogSource.java
  23. 33 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/filterops/AndOp.java
  24. 36 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/filterops/Arg.java
  25. 44 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/filterops/EqualsOp.java
  26. 70 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/filterops/GreaterThanOp.java
  27. 69 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/filterops/LessThanOp.java
  28. 31 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/filterops/NotOp.java
  29. 28 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/filterops/NumberArg.java
  30. 33 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/filterops/OrOp.java
  31. 28 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/filterops/StringArg.java
  32. 27 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/filterops/SymbolArg.java
  33. 40 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/filterops/XorOp.java
  34. 60 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/servlets/FileLoader.java
  35. 69 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/servlets/Fs.java
  36. 84 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/servlets/GraphData.java
  37. 85 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/servlets/JsonServlet.java
  38. 86 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/servlets/NumEvents.java
  39. 50 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/servlets/StaticContent.java
  40. 124 0
      src/contrib/loggraph/src/java/org/apache/zookeeper/graph/servlets/Throughput.java
  41. 11 0
      src/contrib/loggraph/web/org/apache/zookeeper/graph/log4j.properties
  42. 126 0
      src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/date.format.js
  43. 385 0
      src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/g.bar.js
  44. 110 0
      src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/g.dot.js
  45. 230 0
      src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/g.line.js
  46. 205 0
      src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/g.pie.js
  47. 481 0
      src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/g.raphael.js
  48. BIN
      src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/load-big.gif
  49. BIN
      src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/load.gif
  50. 54 0
      src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/loggraph.css
  51. 262 0
      src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/loggraph.js
  52. 57 0
      src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/loggraph.log.js
  53. 329 0
      src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/loggraph.server.js
  54. 202 0
      src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/loggraph.session.js
  55. 44 0
      src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/loggraph.stats.js
  56. 377 0
      src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/loggraph.ui.js
  57. 60 0
      src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/main.html
  58. 3296 0
      src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/raphael.js
  59. 7 0
      src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/yui-min.js

+ 2 - 0
CHANGES.txt

@@ -80,6 +80,8 @@ NEW FEATURES:
   ZOOKEEPER-464. Need procedure to garbage collect ledgers
   (erwin via fpj)
 
+  ZOOKEEPER-773. Log visualisation (Ivan Kelly via phunt)
+
 Release 3.3.0 - 2010-03-24
 
 Non-backward compatible changes:

+ 69 - 0
src/contrib/loggraph/README.txt

@@ -0,0 +1,69 @@
+LogGraph README
+
+1 - About
+LogGraph is an application for viewing and filtering zookeeper logs. It can handle transaction logs and message logs. 
+
+2 - Compiling
+
+Run "ant jar" in src/contrib/loggraph/. This will download all dependencies and compile all the loggraph code.
+
+Once compilation has finished, you can run it the the loggraph.sh script in src/contrib/loggraph/bin. This will start and embedded web server on your machine. 
+Navigate to http://localhost:8182/graph/main.html
+
+3 - Usage
+LogGraph presents the user with 4 views, 
+ 
+  a) Simple log view
+     This view simply displays the log text. This isn't very useful without filters (see "Filtering the logs").
+
+  b) Server view
+     The server view shows the interactions between the different servers in an ensemble. The X axis represents time. 
+        * Exceptions show up as red dots. Hovering your mouse over them will give you more details of the exception
+	* The colour of the line represents the election state of the server. 
+	   - orange means LOOKING for leader
+	   - dark green means the server is the leader
+	   - light green means the server is following a leader
+	   - yellow means there isn't enough information to determine the state of the server. 
+	* The gray arrows denote election messages between servers. Pink dashed arrows are messages that were sent but never delivered.
+
+  c) Session view
+     The session view shows the lifetime of sessions on a server. Use the time filter to narrow down the view. Any more than about 2000 events will take a long time to view in your browser. 
+     The X axis represents time. Each line is a session. The black dots represent events on the session. You can click on the black dots for more details of the event.
+
+  d) Stats view
+     There is currently only one statistics view, Transactions/minute. Suggestions for other statistic views are very welcome.
+
+4 - Filtering the logs
+The logs can be filtered in 2 ways, by time and by content. 
+
+To filter by time simply move the slider to the desired start time. The time window specifies how many milliseconds after and including the start time will be displayed.
+
+Content filtering uses a adhoc filtering language, using prefix notation. The language looks somewhat similar to lisp. A statement in the language takes the form (op arg arg ....). A statement resolves to a boolean value. Statements can be nested. 
+
+4.1 - Filter arguments
+An argument can be a number, a string or a symbol. A number is any argument which starts with -, + or 0 to 9. If the number starts with 0x it is interpretted as hexidecimal. Otherwise it is interpretted as decimal. If the argument begins with a double-quote, (") it is interpretted as a string. Anything else is interpretted as a symbol.
+
+4.2 - Filter symbols
+The possible filter symbols are: 
+
+client-id : number, the session id of the client who initiated a transaction.
+cxid : number, the cxid of a transaction
+zxid : number, the zxid of a transaction
+operation : string, the operation being performed, for example "setData", "createSession", "closeSession", "error", "create"
+
+4.3 - Filter operations
+The possible filter operations are:
+
+or : logical or, takes 1 or more arguments which must be other statements.
+and : logical and, takes 1 or more arguments which must be other statements.
+not : logical not, takes 1 argument which must be another statement.
+xor : exclusive or, takes 1 or more arguments which must be other statements.
+= : equals, takes 1 or more arguments, which must all be equal to each other to return true.
+> : greater than, takes 1 or more arguments, to return true the 1st argument must be greater than the 2nd argument which must be greater than the 3rd argument and so on... 
+< : less than, takes 1 or more arguments, to return true the 1st argument must be less than the 2nd argument which must be less than the 3rd argument and so on... 
+
+4.3 - Filter examples
+Give me all the setData operations with session id 0xdeadbeef or 0xcafeb33r but not with zxid 0x12341234 ->
+
+(and (= operation "setData") (or (= client-id 0xdeadbeef) (= client-id 0xcafeb33r)) (not (= zxid 0x12341234)))
+

+ 43 - 0
src/contrib/loggraph/bin/loggraph-dev.sh

@@ -0,0 +1,43 @@
+#!/bin/sh
+
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+make_canonical () {
+    cd $1; pwd;
+}
+
+SCRIPTDIR=`dirname $0`
+BUILDDIR=`make_canonical $SCRIPTDIR/../../../../build/contrib/loggraph`
+LIBDIR=`make_canonical $BUILDDIR/lib`
+WEBDIR=`make_canonical $SCRIPTDIR/../web`
+ZKDIR=`make_canonical $SCRIPTDIR/../../../../build/`
+
+if [ ! -x $BUILDDIR ]; then
+    echo "\n\n*** You need to build loggraph before running it ***\n\n";
+    exit;
+fi
+
+for i in `ls $LIBDIR`; do 
+    CLASSPATH=$LIBDIR/$i:$CLASSPATH
+done
+
+for i in $ZKDIR/zookeeper-*.jar; do
+    CLASSPATH="$i:$CLASSPATH"
+done
+
+CLASSPATH=$BUILDDIR/classes:$WEBDIR:$CLASSPATH
+echo $CLASSPATH
+java -Dlog4j.configuration=org/apache/zookeeper/graph/log4j.properties -Xdebug -Xrunjdwp:transport=dt_socket,address=4444,server=y,suspend=n -cp $CLASSPATH org.apache.zookeeper.graph.LogServer $*

+ 48 - 0
src/contrib/loggraph/bin/loggraph.sh

@@ -0,0 +1,48 @@
+#!/bin/sh
+
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+make_canonical () {
+    cd $1; pwd;
+}
+
+SCRIPTDIR=`dirname $0`
+BUILDDIR=`make_canonical $SCRIPTDIR/../../../../build/contrib/loggraph`
+LIBDIR=`make_canonical $BUILDDIR/lib`
+ZKDIR=`make_canonical $SCRIPTDIR/../../../../build/`
+
+if [ ! -x $BUILDDIR ]; then
+    echo "\n\n*** You need to build loggraph before running it ***\n\n";
+    exit;
+fi
+
+for i in `ls $LIBDIR`; do 
+    CLASSPATH=$LIBDIR/$i:$CLASSPATH
+done
+
+for i in `ls $BUILDDIR/*.jar`; do 
+    CLASSPATH=$i:$CLASSPATH
+done
+
+for i in $ZKDIR/zookeeper-*.jar; do
+    CLASSPATH="$i:$CLASSPATH"
+done
+
+java -cp $CLASSPATH org.apache.zookeeper.graph.LogServer $*
+
+
+
+

+ 70 - 0
src/contrib/loggraph/build.xml

@@ -0,0 +1,70 @@
+<?xml version="1.0"?>
+
+<!--
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+-->
+
+<project name="loggraph" default="jar">
+
+  <import file="../build-contrib.xml"/>
+  
+  <target name="init" depends="check-contrib,zookeeperbuildcontrib.init" unless="skip.contrib">
+    <echo message="contrib: ${name}"/>
+    <mkdir dir="${build.dir}"/>
+    <antcall target="init-contrib"/>
+  </target>
+
+  <target name="compile" depends="init,ivy-retrieve,zookeeperbuildcontrib.compile" unless="skip.contrib">
+  </target>
+
+  <target name="setjarname">
+    <property name="jarname" value="${build.dir}/zookeeper-${version}-${name}.jar"/>
+  </target>
+
+  <target name="jar" depends="setjarname,compile"  >
+    <jar destfile="${jarname}">
+      <fileset file="${zk.root}/LICENSE.txt" />
+      <fileset dir="${build.classes}" />
+      <fileset dir="web"/>
+      <manifest>
+        <attribute name="Built-By" value="${user.name}"/>
+        <attribute name="Built-At" value="${build.time}"/>
+        <attribute name="Built-On" value="${host.name}" />
+        <attribute name="Implementation-Title" value="org.apache.zookeeper.graph"/>
+        <attribute name="Implementation-Version" value="${revision}"/>
+        <attribute name="Implementation-Vendor" value="The Apache Software Foundation"/>
+      </manifest>
+    </jar>
+  </target>
+  
+  <target name="test">
+    <echo message="No test target defined for this package" />
+  </target>
+  
+
+  <target name="package" depends="compile, zookeeperbuildcontrib.package" unless="skip.contrib">
+    <echo message="contrib: ${name}"/>
+    
+    <copy file="${basedir}/build.xml" todir="${dist.dir}/contrib/${name}"/>
+
+    <mkdir dir="${dist.dir}/contrib/${name}/src"/>
+    <copy todir="${dist.dir}/contrib/${name}/src">
+      <fileset dir="${basedir}/src"/>
+    </copy>
+
+  </target>
+
+</project>

+ 41 - 0
src/contrib/loggraph/ivy.xml

@@ -0,0 +1,41 @@
+<!--
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+-->
+
+<ivy-module version="2.0"
+            xmlns:e="http://ant.apache.org/ivy/extra">
+
+  <info organisation="org.apache.zookeeper"
+        module="${name}" revision="${version}">
+    <license name="Apache 2.0"/>
+    <ivyauthor name="Apache Hadoop" url="http://hadoop.apache.org"/>
+    <description>ZooKeeper Graphing</description>
+  </info>
+
+  <configurations defaultconfmapping="default">
+    <conf name="default"/>
+    <conf name="test"/>
+  </configurations>
+
+  <dependencies>
+    <!-- transitive false turns off dependency checking, log4j deps seem borked -->
+    <dependency org="log4j" name="log4j" rev="1.2.15" transitive="false"/>
+    <dependency org="org.eclipse.jetty" name="jetty-server" rev="7.0.1.v20091125" />
+    <dependency org="org.eclipse.jetty" name="jetty-servlet" rev="7.0.1.v20091125" />
+    <dependency org="com.googlecode.json-simple" name="json-simple" rev="1.1" />
+  </dependencies>
+
+</ivy-module>

+ 22 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/FilterException.java

@@ -0,0 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph;
+
+public class FilterException extends Exception {
+    public FilterException(String s) { super(s); }
+};

+ 75 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/FilterOp.java

@@ -0,0 +1,75 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph;
+
+import java.util.ArrayList;
+import org.apache.zookeeper.graph.filterops.*;
+
+public abstract class FilterOp {
+    protected ArrayList<FilterOp> subOps;
+    protected ArrayList<Arg> args;
+
+    public enum ArgType {
+	STRING, NUMBER, SYMBOL
+    }
+
+    public FilterOp() {
+	subOps = new ArrayList<FilterOp>();
+	args = new ArrayList<Arg>();
+    }
+
+    public static FilterOp newOp(String op) throws FilterException {
+	if (op.equals("or")) 
+	    return new OrOp();
+	if (op.equals("and"))
+	    return new AndOp();
+	if (op.equals("not"))
+	    return new NotOp();
+	if (op.equals("xor"))
+	    return new XorOp();
+	if (op.equals("="))
+	    return new EqualsOp();
+	if (op.equals("<"))
+	    return new LessThanOp();
+	if (op.equals(">")) 
+	    return new GreaterThanOp();
+
+	throw new FilterException("Invalid operation '"+op+"'");
+    }
+
+    public void addSubOp(FilterOp op) {
+	subOps.add(op);
+    }
+    
+    public void addArg(Arg arg) {
+	args.add(arg); 
+    }
+
+    public abstract boolean matches(LogEntry entry) throws FilterException;
+    
+    public String toString() {
+	String op = "(" + getClass().getName();
+	for (FilterOp f :  subOps) {
+	    op += " " + f;
+	}
+	for (Arg a : args) {
+	    op += " " + a;
+	}
+	return op + ")";
+    }
+}

+ 131 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/FilterParser.java

@@ -0,0 +1,131 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph;
+
+import java.io.PushbackReader;
+import java.io.StringReader;
+import java.io.IOException;
+import java.util.ArrayList;
+
+import org.apache.zookeeper.graph.filterops.*;
+
+public class FilterParser {
+    private PushbackReader reader;
+
+    public FilterParser(String s) {
+	reader = new PushbackReader(new StringReader(s));
+    }
+
+    private String readUntilSpace() throws IOException {
+	StringBuffer buffer = new StringBuffer();
+
+	int c = reader.read();
+	while (!Character.isWhitespace(c) && c != ')' && c != '(') {
+	    buffer.append((char)c);
+	    c = reader.read();
+	    if (c == -1) {
+		break;
+	    }
+	}	
+	reader.unread(c);
+
+	return buffer.toString().trim();
+    }
+
+    private StringArg readStringArg() throws IOException, FilterException {
+	int c = reader.read();
+	int last = 0;
+	if (c != '"') {
+	    throw new FilterException("Check the parser, trying to read a string that doesn't begin with quotes");
+	}
+	StringBuffer buffer = new StringBuffer();
+	while (reader.ready()) {
+	    last = c;
+	    c = reader.read();
+	    if (c == -1) {
+		break;
+	    }
+	    
+	    if (c == '"' && last != '\\') {
+		return new StringArg(buffer.toString());
+	    } else {
+		buffer.append((char)c);
+	    }
+	}
+	throw new FilterException("Unterminated string");
+    }
+
+    private NumberArg readNumberArg() throws IOException, FilterException {
+	String strval = readUntilSpace();
+	
+	try {
+	    if (strval.startsWith("0x")) {
+		return new NumberArg(Long.valueOf(strval.substring(2), 16));
+	    } else {
+		return new NumberArg(Long.valueOf(strval));
+	    }
+	} catch (NumberFormatException e) {
+	    throw new FilterException("Not a number [" + strval + "]\n" + e);
+	}
+    }
+
+    private SymbolArg readSymbolArg() throws IOException, FilterException {
+	return new SymbolArg(readUntilSpace());
+    }
+
+    public FilterOp parse() throws IOException, FilterException {
+	int c = reader.read();
+	if (c != '(') {
+	    throw new FilterException("Invalid format");
+	}
+
+	String opstr = readUntilSpace();
+	FilterOp op = FilterOp.newOp(opstr);
+
+	while (reader.ready()) {
+	    c = reader.read();
+	    if (c == -1) {
+		break;
+	    }
+	    if (c == '(') {
+		reader.unread(c);
+		op.addSubOp(parse());
+	    } else if (c == ')') {
+		return op;
+	    } else if (c == '"') {
+		reader.unread(c);
+		op.addArg(readStringArg());
+	    } else if (Character.isDigit(c) || c == '-' || c == '+') {
+		reader.unread(c);
+		op.addArg(readNumberArg());
+	    } else if (Character.isJavaIdentifierStart(c)) {
+		reader.unread(c);
+		op.addArg(readSymbolArg());
+	    }
+	}
+	throw new FilterException("Incomplete filter");
+    }
+
+    public static void main(String[] args) throws IOException, FilterException {
+	if (args.length == 1) {
+	    System.out.println(new FilterParser(args[0]).parse());
+	} else {
+	    System.out.println(new FilterParser("(or (and (= session foobar) (= session barfoo)) (= session sdfs))").parse());
+	}
+    }
+};

+ 226 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/JsonGenerator.java

@@ -0,0 +1,226 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph;
+
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.JSONValue;
+
+import java.io.Writer;
+import java.io.OutputStreamWriter;
+import java.io.IOException;
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.ListIterator;
+
+public class JsonGenerator {
+    private JSONObject root;
+    private HashSet<Integer> servers;
+
+    private class Message {
+	private int from;
+	private int to;
+	private long zxid;
+
+	public Message(int from, int to, long zxid) {
+	    this.from = from;
+	    this.to = to;
+	    this.zxid = zxid;
+	}
+	
+	public boolean equals(Message m) {
+	    return (m.from == this.from 
+		    && m.to == this.to
+		    && m.zxid == this.zxid);
+	}
+    };
+
+    public JSONObject txnEntry(TransactionEntry e) {
+	JSONObject event = new JSONObject();
+
+	event.put("time", Long.toString(e.getTimestamp()));
+	event.put("client", Long.toHexString(e.getClientId()));
+	event.put("cxid", Long.toHexString(e.getCxid()));
+	event.put("zxid", Long.toHexString(e.getZxid()));
+	event.put("op", e.getOp());
+	event.put("extra", e.getExtra());
+	event.put("type", "transaction");
+
+	return event;
+    }
+
+    /**
+       Assumes entries are sorted by timestamp.
+     */
+    public JsonGenerator(LogIterator iter) {
+	servers = new HashSet<Integer>();
+
+	Pattern stateChangeP = Pattern.compile("- (LOOKING|FOLLOWING|LEADING)");
+	Pattern newElectionP = Pattern.compile("New election. My id =  (\\d+), Proposed zxid = (\\d+)");
+	Pattern receivedProposalP = Pattern.compile("Notification: \\d+, (\\d+), (\\d+), \\d+, [^,]*, [^,]*, (\\d+)");//, LOOKING, LOOKING, 2
+	Pattern exceptionP = Pattern.compile("xception");
+	
+	root = new JSONObject();
+	Matcher m = null;
+	JSONArray events = new JSONArray();
+	root.put("events", events);
+	
+	long starttime = Long.MAX_VALUE;
+	long endtime = 0;
+
+	int leader = 0;
+	long curEpoch = 0;
+	boolean newEpoch = false;
+
+	while (iter.hasNext()) {
+	    LogEntry ent = iter.next();
+	    
+	    if (ent.getTimestamp() < starttime) {
+		starttime = ent.getTimestamp();
+	    }
+	    if (ent.getTimestamp() > endtime) {
+		endtime = ent.getTimestamp();
+	    }
+	    
+	    if (ent.getType() == LogEntry.Type.TXN) {
+		events.add(txnEntry((TransactionEntry)ent));
+	    } else {
+		Log4JEntry e = (Log4JEntry)ent;
+		servers.add(e.getNode());
+		
+		if ((m = stateChangeP.matcher(e.getEntry())).find()) {
+		    JSONObject stateChange = new JSONObject();
+		    stateChange.put("type", "stateChange");
+		    stateChange.put("time", e.getTimestamp());
+		    stateChange.put("server", e.getNode());
+		    stateChange.put("state", m.group(1));
+		    events.add(stateChange);
+		    
+		    if (m.group(1).equals("LEADING")) {
+			leader = e.getNode();
+		    }
+		} else if ((m = newElectionP.matcher(e.getEntry())).find()) {
+		    Iterator<Integer> iterator = servers.iterator();
+		    long zxid = Long.valueOf(m.group(2));
+		    int count = (int)zxid;// & 0xFFFFFFFFL;
+		    int epoch = (int)Long.rotateRight(zxid, 32);// >> 32;
+		    
+		    if (leader != 0 && epoch > curEpoch) {
+			JSONObject stateChange = new JSONObject();
+			stateChange.put("type", "stateChange");
+			stateChange.put("time", e.getTimestamp());
+			stateChange.put("server", leader);
+			stateChange.put("state", "INIT");
+			events.add(stateChange);
+			leader = 0;
+		    }
+		    
+		    while (iterator.hasNext()) {
+			int dst = iterator.next();
+			if (dst != e.getNode()) {
+			    JSONObject msg = new JSONObject();
+			    msg.put("type", "postmessage");
+			    msg.put("src", e.getNode());
+			    msg.put("dst", dst);
+			    msg.put("time", e.getTimestamp());
+			    msg.put("zxid", m.group(2));
+			    msg.put("count", count);
+			    msg.put("epoch", epoch);
+			    
+			    events.add(msg);
+			}
+		    }
+		} else if ((m = receivedProposalP.matcher(e.getEntry())).find()) {
+		    // Pattern.compile("Notification: \\d+, (\\d+), (\\d+), \\d+, [^,]*, [^,]*, (\\d+)");//, LOOKING, LOOKING, 2
+		    int src = Integer.valueOf(m.group(3));
+		    long zxid = Long.valueOf(m.group(1));
+		    int dst = e.getNode();
+		    long epoch2 = Long.valueOf(m.group(2));
+		    
+		    int count = (int)zxid;// & 0xFFFFFFFFL;
+		    int epoch = (int)Long.rotateRight(zxid, 32);// >> 32;
+		    
+		    if (leader != 0 && epoch > curEpoch) {
+			JSONObject stateChange = new JSONObject();
+			stateChange.put("type", "stateChange");
+			stateChange.put("time", e.getTimestamp());
+			stateChange.put("server", leader);
+			stateChange.put("state", "INIT");
+			events.add(stateChange);
+			leader = 0;
+		    }
+		    
+		    //	System.out.println("src: "+src+" dst: "+dst+ " zxid: "+zxid);
+		    
+		    //  System.out.println(cur);
+		    if (src != dst) {
+			JSONObject msg = new JSONObject();
+			msg.put("type", "delivermessage");
+			msg.put("src", src);
+			msg.put("dst", dst);
+			msg.put("time", e.getTimestamp());
+			msg.put("zxid", zxid);
+			msg.put("epoch", epoch);
+			msg.put("count", count);
+			msg.put("epoch2", epoch2);
+			
+			events.add(msg);
+		    }
+		} else if ((m = exceptionP.matcher(e.getEntry())).find()) {
+		    JSONObject ex = new JSONObject();
+		    ex.put("type", "exception");
+		    ex.put("server", e.getNode());
+		    ex.put("time", e.getTimestamp());
+		    ex.put("text", e.getEntry());
+		    events.add(ex);
+		} 
+	    }
+	    JSONObject ex = new JSONObject();
+	    ex.put("type", "text");
+	    ex.put("time", ent.getTimestamp());
+	    String txt = ent.toString();
+	    ex.put("text", txt);
+	    events.add(ex);
+	}
+	//	System.out.println("pending messages: "+pendingMessages.size());
+	root.put("starttime", starttime);
+	root.put("endtime", endtime);
+
+	JSONArray serversarray = new JSONArray();
+	root.put("servers", serversarray);
+	
+	Iterator<Integer> iterator = servers.iterator();
+	while (iterator.hasNext()) {
+	    serversarray.add(iterator.next());
+	}
+    }
+
+    public String toString() {
+	return JSONValue.toJSONString(root);
+    }
+
+    public static void main(String[] args) throws Exception {
+	MergedLogSource src = new MergedLogSource(args);
+	LogIterator iter = src.iterator();
+	System.out.println(new JsonGenerator(iter));
+    }
+}

+ 40 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/Log4JEntry.java

@@ -0,0 +1,40 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph;
+
+public class Log4JEntry extends LogEntry {
+    public Log4JEntry(long timestamp, int node, String entry) { 
+	super(timestamp);
+	setAttribute("log-text", entry);
+	setAttribute("node", new Integer(node));
+    }
+
+    public String getEntry() {
+	return (String) getAttribute("log-text");
+    }
+
+    public String toString() {
+	return "" + getTimestamp() + "::::" + getNode() + "::::"  + getEntry();
+    }
+
+    public int getNode() {
+	return (Integer) getAttribute("node");
+    }
+
+    public Type getType() { return LogEntry.Type.LOG4J; }
+}

+ 380 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/Log4JSource.java

@@ -0,0 +1,380 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph;
+
+import java.io.File;
+import java.io.InputStreamReader;
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+import java.util.ArrayList;
+import java.util.Date;
+import java.text.SimpleDateFormat;
+import java.text.ParseException;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+
+import java.io.EOFException;
+import java.io.Closeable;
+import java.io.FileNotFoundException;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+import org.apache.log4j.Logger;
+
+public class Log4JSource implements LogSource {
+    private static final Logger LOG = Logger.getLogger(Log4JSource.class);
+    
+    private static final int skipN = 10000;
+    private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss,SSS";
+
+    private LogSkipList skiplist = null;
+
+    private String file = null;
+    private long starttime = 0;
+    private long endtime = 0;
+    private int serverid = 0;
+    private long size = 0;
+
+    private Pattern timep;
+
+    public boolean overlapsRange(long starttime, long endtime) {
+	return (starttime <= this.endtime && endtime >= this.starttime);
+    }
+    
+    public long size() { return size; }
+    public long getStartTime() { return starttime; }
+    public long getEndTime() { return endtime; }
+    public LogSkipList getSkipList() { return skiplist; }
+    
+    private class Log4JSourceIterator implements LogIterator {
+	private RandomAccessFileReader in;
+	private LogEntry next = null;
+	private long starttime = 0;
+	private long endtime = 0;
+	private String buf = "";	
+	private Log4JSource src = null;
+	private long skippedAtStart = 0;
+	private SimpleDateFormat dateformat = null;
+	private FilterOp filter = null;
+
+	public Log4JSourceIterator(Log4JSource src, long starttime, long endtime) throws IllegalArgumentException, FilterException {
+	    this(src, starttime, endtime, null);
+	}
+
+	public Log4JSourceIterator(Log4JSource src, long starttime, long endtime, FilterOp filter) throws IllegalArgumentException, FilterException {
+
+	    this.dateformat = new SimpleDateFormat(DATE_FORMAT);
+	    this.src = src;
+	    this.starttime = starttime;
+	    this.endtime = endtime;
+
+	    File f = new File(src.file);
+	    try {
+		in = new RandomAccessFileReader(f);
+	    } catch (FileNotFoundException e) {
+		throw new IllegalArgumentException("Bad file passed in (" + src.file +") cannot open:" + e);
+	    }
+
+	    // skip to the offset of latest skip point before starttime
+	    LogSkipList.Mark start = src.getSkipList().findMarkBefore(starttime);
+	    try {
+		in.seek(start.getBytes());
+		skippedAtStart = start.getEntriesSkipped();
+	    } catch (IOException ioe) {
+		// if we can't skip, we should just read from the start
+	    }
+
+	    LogEntry e;
+	    while ((e = readNextEntry()) != null && e.getTimestamp() < endtime) {
+		if (e.getTimestamp() >= starttime && (filter == null || filter.matches(e))) {
+		    next = e;
+		    return;
+		}
+		skippedAtStart++;
+	    }
+	    this.filter = filter;
+	}
+	
+	synchronized public long size() throws IOException {
+	    if (LOG.isTraceEnabled()) {
+		LOG.trace("size() called");
+	    }
+
+	    if (this.endtime >= src.getEndTime()) {
+		return src.size() - skippedAtStart;
+	    }
+	    
+	    long pos = in.getPosition();
+	    
+	    if (LOG.isTraceEnabled()) {
+		LOG.trace("saved pos () = " + pos);
+	    }
+	    
+	    LogEntry e;
+	  
+	    LogSkipList.Mark lastseg = src.getSkipList().findMarkBefore(this.endtime);
+	    in.seek(lastseg.getBytes());
+	    buf = "";  // clear the buf so we don't get something we read before we sought
+	    // number of entries skipped to get to the end of the iterator, less the number skipped to get to the start
+	    long count = lastseg.getEntriesSkipped() - skippedAtStart; 
+
+	    while ((e = readNextEntry()) != null) {
+		if (LOG.isTraceEnabled()) {
+		    //LOG.trace(e);
+		}
+		if (e.getTimestamp() > this.endtime) {
+		    break;
+		}
+		count++;
+	    }
+	    in.seek(pos);
+	    buf = "";
+
+	    if (LOG.isTraceEnabled()) {
+		LOG.trace("size() = " + count);
+	    }
+	    
+	    return count;
+	}
+
+	synchronized private LogEntry readNextEntry() {
+	    try {
+		try {
+		    while (true) {
+			String line = in.readLine();
+			if (line == null) {
+			    break;
+			}
+
+			Matcher m = src.timep.matcher(line);
+			if (m.lookingAt()) {
+			    if (buf.length() > 0) {
+				LogEntry e = new Log4JEntry(src.timestampFromText(dateformat, buf), src.getServerId(), buf);
+				buf = line;
+				return e;
+			    }
+			    buf = line;
+			} else if (buf.length() > 0) {
+			    buf += line + "\n";
+			}
+		    }
+		} catch (EOFException eof) {
+		    // ignore, we've simply come to the end of the file
+		}
+		if (buf.length() > 0) {
+		    LogEntry e = new Log4JEntry(src.timestampFromText(dateformat, buf), src.getServerId(), buf);
+		    buf = "";
+		    return e;
+		}
+	    } catch (Exception e) {
+		LOG.error("Error reading next entry in file (" + src.file + "): " + e);
+		return null;
+	    }
+	    return null;
+	}
+
+	public boolean hasNext() {
+	    return next != null;
+	}
+	
+	public LogEntry next() throws NoSuchElementException {
+	    LogEntry ret = next;
+	    LogEntry e = readNextEntry();
+
+	    if (filter != null) {
+		try {
+		    while (e != null && !filter.matches(e)) {
+			e = readNextEntry();
+		    }
+		} catch (FilterException fe) {
+		    throw new NoSuchElementException(e.toString());
+		}
+	    }
+
+	    if (e != null && e.getTimestamp() < endtime) {
+		next = e;
+	    } else {
+		next = null;
+	    }
+	    return ret;
+	}
+
+	public void remove() throws UnsupportedOperationException {
+	    throw new UnsupportedOperationException("remove not supported for L4J logs");
+	}
+	
+	public void close() throws IOException {
+	    in.close();
+	}
+	
+	public String toString() {
+	    String size;
+	    try {
+		size = new Long(size()).toString();
+	    } catch (IOException ioe) {
+		size = "Unable to read";
+	    }
+	    return "Log4JSourceIterator(start=" + starttime + ", end=" + endtime + ", size=" + size + ")";
+	}
+    }
+
+    public LogIterator iterator(long starttime, long endtime) throws IllegalArgumentException {
+	try {
+	    return iterator(starttime, endtime, null);
+	} catch (FilterException fe) {
+	    assert(false); //"This should never happen, you can't have a filter exception without a filter");
+	    return null;
+	}
+    }
+
+    public LogIterator iterator(long starttime, long endtime, FilterOp filter) throws IllegalArgumentException, FilterException{
+	// sanitise start and end times
+	if (endtime < starttime) {
+	    throw new IllegalArgumentException("End time (" +  endtime + ") must be greater or equal to starttime (" + starttime + ")");
+	}
+
+	return new Log4JSourceIterator(this, starttime, endtime, filter);
+    }
+
+    public LogIterator iterator() throws IllegalArgumentException {
+	return iterator(starttime, endtime+1);
+    }
+    
+    public Log4JSource(String file) throws IOException {
+	this.file=file;
+	
+	timep = Pattern.compile("^(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2},\\d{3})");
+	skiplist = new LogSkipList();
+	init();
+    }
+    
+    private static long timestampFromText(SimpleDateFormat format, String s) {
+	Date d = null;
+	try {
+	    d = format.parse(s);
+	} catch (ParseException e) {
+	    return 0;
+	}
+	Calendar c = new GregorianCalendar();
+	c.setTime(d);
+	return c.getTimeInMillis();
+    }
+
+    private void init() throws IOException {
+	File f = new File(file);
+	RandomAccessFileReader in = new RandomAccessFileReader(f);
+	SimpleDateFormat dateformat = new SimpleDateFormat(DATE_FORMAT);
+	Pattern idp = Pattern.compile("Notification:\\s+\\d+,\\s+\\d+,\\s+\\d+,\\s+(\\d+)");// 1, My\\s+id\\s+=\\s+(\\d+)");
+
+	long lastFp = in.getPosition();
+	String line = in.readLine();
+	Matcher m = null;
+
+	// if we have read data from the file, and it matchs the timep pattern
+	if ((line != null) && (m = timep.matcher(line)).lookingAt()) {
+	    starttime = timestampFromText(dateformat, m.group(1));
+	} else {
+	    throw new IOException("Invalid log4j format. First line doesn't start with time");
+	}
+
+	/*
+	  Count number of log entries. Any line starting with a timestamp counts as an entry
+	*/
+	String lastentry = line;
+	try {
+	    while (line != null) {
+		m = timep.matcher(line);
+		if (m.lookingAt()) {
+		    if (size % skipN == 0) {
+			long time = timestampFromText(dateformat, m.group(1));
+			skiplist.addMark(time, lastFp, size);
+		    }
+		    size++;
+		    lastentry = line;
+		} 
+		if (serverid == 0 && (m = idp.matcher(line)).find()) {
+		    serverid = Integer.valueOf(m.group(1));
+		}
+
+		lastFp = in.getPosition();
+		line = in.readLine();
+	    }
+	} catch (EOFException eof) {
+	    // ignore, simply end of file, though really (line!=null) should have caught this
+	} finally {
+	    in.close();
+	}
+
+	m = timep.matcher(lastentry);
+	if (m.lookingAt()) {
+	    endtime = timestampFromText(dateformat, m.group(1));
+	} else {
+	    throw new IOException("Invalid log4j format. Last line doesn't start with time");
+	}
+    }
+    
+    public String toString() {
+	return "Log4JSource(file=" + file + ", size=" + size + ", start=" + starttime + ", end=" + endtime +", id=" + serverid +")";
+    }
+
+    public static void main(String[] args) throws IOException {
+	final Log4JSource s = new Log4JSource(args[0]);
+	System.out.println(s);
+
+	LogIterator iter;
+
+	if (args.length == 3) {
+	    final long starttime = Long.valueOf(args[1]);
+	    final long endtime = Long.valueOf(args[2]);
+	    iter = s.iterator(starttime, endtime);
+	    
+	    Thread t1 = new Thread() { public void run () { 
+		
+		LogIterator iter = s.iterator(starttime, endtime);
+		System.out.println(iter);
+	    }; };
+	    Thread t2 = new Thread() { public void run () { 
+		
+		LogIterator iter = s.iterator(starttime, endtime);
+		System.out.println(iter);
+	    }; };
+	    Thread t3 = new Thread() { public void run () { 
+		
+		LogIterator iter = s.iterator(starttime, endtime);
+		System.out.println(iter);
+	    }; };
+	    t1.start();
+	    t2.start();
+	    //	    t3.start();
+	} else {
+	    iter = s.iterator();
+	}
+
+	/*while (iter.hasNext()) {
+	    System.out.println(iter.next());
+	    }*/
+	iter.close();
+    }
+
+    public int getServerId() {
+	return serverid;
+    }
+}

+ 46 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/LogEntry.java

@@ -0,0 +1,46 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph;
+
+import java.io.Serializable;
+import java.util.HashMap;
+
+public abstract class LogEntry implements Serializable {
+    private HashMap attributes;
+
+    public enum Type { UNKNOWN, LOG4J, TXN };
+        
+    public LogEntry(long timestamp) {
+	attributes = new HashMap();
+	setAttribute("timestamp", new Long(timestamp));
+    }
+    
+    public long getTimestamp() {
+	return (Long)getAttribute("timestamp");
+    }    
+    
+    public abstract Type getType();
+    
+    public void setAttribute(String key, Object v) {
+	attributes.put(key, v);
+    }
+
+    public Object getAttribute(String key) {
+	return attributes.get(key);
+    }
+}

+ 26 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/LogIterator.java

@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph;
+
+import java.io.Closeable;
+import java.util.Iterator;
+import java.io.IOException;
+
+public interface LogIterator extends Iterator<LogEntry>, Closeable {
+    long size() throws IOException;;
+};

+ 66 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/LogServer.java

@@ -0,0 +1,66 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.ServletException;
+ 
+import java.io.IOException;
+ 
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+
+import org.apache.zookeeper.graph.servlets.*;
+
+public class LogServer extends ServletContextHandler {    
+    public LogServer(MergedLogSource src) throws Exception {
+	super(ServletContextHandler.SESSIONS);
+	setContextPath("/");
+
+	addServlet(new ServletHolder(new StaticContent()),"/graph/*");
+
+	addServlet(new ServletHolder(new Fs()),"/fs");
+	addServlet(new ServletHolder(new GraphData(src)), "/data");
+	addServlet(new ServletHolder(new FileLoader(src)), "/loadfile");
+	addServlet(new ServletHolder(new NumEvents(src)), "/info");
+	addServlet(new ServletHolder(new Throughput(src)), "/throughput");
+    }
+
+    public static void main(String[] args) {  
+	try {  
+	    MergedLogSource src = new MergedLogSource(args);
+	    System.out.println(src);
+
+	    Server server = new Server(8182);
+	    server.setHandler(new LogServer(src));
+	    
+	    server.start();
+	    server.join();
+
+	} catch (Exception e) {  
+	    // Something is wrong.  
+	    e.printStackTrace();  
+	}  
+    } 
+} 

+ 94 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/LogSkipList.java

@@ -0,0 +1,94 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph;
+
+import java.util.List;
+import java.util.LinkedList;
+import java.util.NoSuchElementException;
+
+import org.apache.log4j.Logger;
+
+/**
+Generic skip list for holding a rough index of a log file. When the log file is loaded, this 
+index is built by adding a mark every n entries. Then when a specific time position is requested
+from the file, a point at most n-1 entries before the time position can be jumped to.
+
+*/
+public class LogSkipList {
+    private static final Logger LOG = Logger.getLogger(LogSkipList.class);
+    
+    private LinkedList<Mark> marks;
+
+    public class Mark {
+	private long time;
+	private long bytes;
+	private long skipped;
+
+	public Mark(long time, long bytes, long skipped) {
+	    this.time = time;
+	    this.bytes = bytes;
+	    this.skipped = skipped;
+	}
+
+	public long getTime() { return this.time; }
+	public long getBytes() { return this.bytes; }
+	public long getEntriesSkipped() { return this.skipped; }
+
+	public String toString() {
+	    return "Mark(time=" + time + ", bytes=" + bytes + ", skipped=" + skipped + ")";
+	}
+    };
+
+    public LogSkipList() {
+	if (LOG.isTraceEnabled()) {
+	    LOG.trace("New skip list");
+	}
+	marks = new LinkedList<Mark>();
+    }
+
+    public void addMark(long time, long bytes, long skipped) {
+	if (LOG.isTraceEnabled()) {
+	    LOG.trace("addMark (time:" + time + ", bytes: " + bytes + ", skipped: " + skipped + ")");
+	}
+	marks.add(new Mark(time, bytes, skipped));
+    }
+
+    /** 
+	Find the last mark in the skip list before time.
+     */
+    public Mark findMarkBefore(long time) throws NoSuchElementException {
+	if (LOG.isTraceEnabled()) {
+	    LOG.trace("findMarkBefore(" + time + ")");
+	}
+		    
+	Mark last = marks.getFirst();
+	for (Mark m: marks) {
+	    if (m.getTime() > time) {
+		break;
+	    } 
+	    last = m;
+	}
+	
+	if (LOG.isTraceEnabled()) {
+	    LOG.trace("return " + last );
+	}
+	
+	return last;
+    }
+
+};

+ 33 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/LogSource.java

@@ -0,0 +1,33 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph;
+import java.util.Iterator;
+
+public interface LogSource extends Iterable<LogEntry> {
+    public LogIterator iterator(long starttime, long endtime, FilterOp filter) throws IllegalArgumentException, FilterException;
+
+    public LogIterator iterator(long starttime, long endtime) throws IllegalArgumentException;
+
+    public LogIterator iterator() throws IllegalArgumentException;
+
+    public boolean overlapsRange(long starttime, long endtime);
+
+    public long size();
+    public long getStartTime();
+    public long getEndTime();
+}

+ 103 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/MeasureThroughput.java

@@ -0,0 +1,103 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph;
+
+import java.io.IOException;
+import java.io.BufferedOutputStream;
+import java.io.FileOutputStream;
+import java.io.DataOutputStream;
+import java.io.PrintStream;
+
+import java.util.HashSet;
+
+public class MeasureThroughput {
+    private static final int MS_PER_SEC = 1000;
+    private static final int MS_PER_MIN = MS_PER_SEC*60;
+    private static final int MS_PER_HOUR = MS_PER_MIN*60;
+    
+    public static void main(String[] args) throws IOException {	
+	MergedLogSource source = new MergedLogSource(args);
+
+	PrintStream ps_ms = new PrintStream(new BufferedOutputStream(new FileOutputStream("throughput-ms.out")));
+	PrintStream ps_sec = new PrintStream(new BufferedOutputStream(new FileOutputStream("throughput-sec.out")));
+	PrintStream ps_min = new PrintStream(new BufferedOutputStream(new FileOutputStream("throughput-min.out")));
+	PrintStream ps_hour = new PrintStream(new BufferedOutputStream(new FileOutputStream("throughput-hour.out")));
+	LogIterator iter;
+	
+	System.out.println(source);
+	iter = source.iterator();
+	long currentms = 0;
+	long currentsec = 0;
+	long currentmin = 0;
+	long currenthour = 0;
+	HashSet<Long> zxids_ms = new HashSet<Long>();
+	long zxid_sec = 0;
+	long zxid_min = 0;
+	long zxid_hour = 0;
+
+	while (iter.hasNext()) {
+	    LogEntry e = iter.next();
+	    TransactionEntry cxn = (TransactionEntry)e;
+	    
+	    long ms = cxn.getTimestamp();
+	    long sec = ms/MS_PER_SEC;
+	    long min = ms/MS_PER_MIN;
+	    long hour = ms/MS_PER_HOUR;
+
+	    if (currentms != ms && currentms != 0) {
+		ps_ms.println("" + currentms + " " + zxids_ms.size());
+
+		zxid_sec += zxids_ms.size();
+		zxid_min += zxids_ms.size();
+		zxid_hour += zxids_ms.size();
+		zxids_ms.clear();
+	    }
+
+	    if (currentsec != sec && currentsec != 0) {
+		ps_sec.println("" + currentsec*MS_PER_SEC + " " + zxid_sec);
+
+		zxid_sec = 0;
+	    }
+
+	    if (currentmin != min && currentmin != 0) {
+		ps_min.println("" + currentmin*MS_PER_MIN + " " + zxid_min);
+		
+		zxid_min = 0;
+	    }
+
+	    if (currenthour != hour && currenthour != 0) {
+		ps_hour.println("" + currenthour*MS_PER_HOUR + " " + zxid_hour);
+		
+		zxid_hour = 0;
+	    }
+
+	    currentms = ms;
+	    currentsec = sec;
+	    currentmin = min;
+	    currenthour = hour;
+
+	    zxids_ms.add(cxn.getZxid());
+	}
+
+	iter.close();
+	ps_ms.close();
+	ps_sec.close();
+	ps_min.close();
+	ps_hour.close();
+    }
+};

+ 218 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/MergedLogSource.java

@@ -0,0 +1,218 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph;
+
+import java.io.ByteArrayInputStream;
+import java.io.EOFException;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.text.DateFormat;
+import java.util.Date;
+import java.util.zip.Adler32;
+import java.util.zip.Checksum;
+import java.util.HashMap;
+
+import org.apache.jute.BinaryInputArchive;
+import org.apache.jute.InputArchive;
+import org.apache.jute.Record;
+import org.apache.zookeeper.server.TraceFormatter;
+import org.apache.zookeeper.server.persistence.FileHeader;
+import org.apache.zookeeper.server.persistence.FileTxnLog;
+import org.apache.zookeeper.server.util.SerializeUtils;
+import org.apache.zookeeper.txn.TxnHeader;
+
+import org.apache.zookeeper.ZooDefs.OpCode;
+
+import org.apache.zookeeper.txn.CreateSessionTxn;
+import org.apache.zookeeper.txn.CreateTxn;
+import org.apache.zookeeper.txn.DeleteTxn;
+import org.apache.zookeeper.txn.ErrorTxn;
+import org.apache.zookeeper.txn.SetACLTxn;
+import org.apache.zookeeper.txn.SetDataTxn;
+import org.apache.zookeeper.txn.TxnHeader;
+
+import java.io.Closeable;
+import java.io.FileNotFoundException;
+import java.util.Vector;
+import java.util.Iterator;
+import java.util.Collections;
+import java.util.NoSuchElementException;
+
+import org.apache.log4j.Logger;
+
+public class MergedLogSource implements LogSource {
+    private static final Logger LOG = Logger.getLogger(MergedLogSource.class);
+    private Vector<LogSource> sources = null;
+    private long starttime = 0;
+    private long endtime = 0;
+    private long size = 0;
+
+    public boolean overlapsRange(long starttime, long endtime) {
+	return (starttime <= this.endtime && endtime >= this.starttime);
+    }
+    
+    public long size() { return size; }
+    public long getStartTime() { return starttime; }
+    public long getEndTime() { return endtime; }
+
+    private class MergedLogSourceIterator implements LogIterator {
+	private LogEntry next = null;
+	private long start = 0;
+	private long end = 0;
+	private MergedLogSource src = null;
+	private LogIterator[] sources = null;
+	private LogEntry[] nexts = null;
+	private FilterOp filter = null;
+	
+	public MergedLogSourceIterator(MergedLogSource src, long starttime, long endtime, FilterOp filter) throws IllegalArgumentException, FilterException {
+	    Vector<LogIterator> iters = new Vector<LogIterator>();
+	    for (LogSource s : src.sources) {
+		if (s.overlapsRange(starttime, endtime)) {
+		    iters.add(s.iterator(starttime, endtime, filter));
+		}
+	    }
+	    
+	    sources = new LogIterator[iters.size()];
+	    sources = iters.toArray(sources);
+	    nexts = new LogEntry[iters.size()];
+	    for (int i = 0; i < sources.length; i++) {
+		if (sources[i].hasNext()) 
+		    nexts[i] = sources[i].next();
+	    }
+	    this.filter = filter;
+	}
+		    
+	public MergedLogSourceIterator(MergedLogSource src, long starttime, long endtime) throws IllegalArgumentException, FilterException {
+	    this(src, starttime, endtime, null);
+	}
+	
+	public long size() throws IOException {
+	    long size = 0;
+	    for (LogIterator i : sources) {
+		size += i.size();
+	    }
+	    return size;
+	}
+
+	public boolean hasNext() {
+	    for (LogEntry n : nexts) {
+		if (n != null) return true;
+	    }
+	    return false;
+	}
+	
+	public LogEntry next() {
+	    int min = -1;
+	    for (int i = 0; i < nexts.length; i++) {
+		if (nexts[i] != null) {
+		    if (min == -1) {
+			min = i;
+		    } else if (nexts[i].getTimestamp() < nexts[min].getTimestamp()) {
+			min = i;
+		    }
+		}
+	    }
+	    if (min == -1) {
+		return null;
+	    } else {
+		LogEntry e =  nexts[min];
+		nexts[min] = sources[min].next();
+		return e;
+	    }
+	}
+
+	public void remove() throws UnsupportedOperationException {
+	    throw new UnsupportedOperationException("remove not supported for Merged logs");
+	}
+	
+	public void close() throws IOException {
+	    for (LogIterator i : sources) {
+		i.close();
+	    }
+	}
+    }
+
+    public LogIterator iterator(long starttime, long endtime) throws IllegalArgumentException {
+	try {
+	    return iterator(starttime, endtime, null);
+	} catch (FilterException fe) {
+	    assert(false); // shouldn't happen without filter
+	    return null;
+	}
+    }
+
+    public LogIterator iterator(long starttime, long endtime, FilterOp filter) throws IllegalArgumentException, FilterException {
+	// sanitise start and end times
+	if (endtime < starttime) {
+	    throw new IllegalArgumentException("End time (" +  endtime + ") must be greater or equal to starttime (" + starttime + ")");
+	}
+
+	return new MergedLogSourceIterator(this, starttime, endtime, filter);
+    }
+
+    public LogIterator iterator() throws IllegalArgumentException {
+	return iterator(starttime, endtime+1);
+    }
+    
+    public MergedLogSource(String[] files) throws IOException {
+	sources = new Vector<LogSource>();
+	for (String f : files) {
+	    addSource(f);
+	}
+    }
+    
+    public void addSource(String f) throws IOException {
+	LogSource s = null;
+	if (TxnLogSource.isTransactionFile(f)) {
+	    s = new TxnLogSource(f);
+	} else {
+	    s = new Log4JSource(f);
+	}
+
+	size += s.size();
+	endtime = s.getEndTime() > endtime ? s.getEndTime() : endtime;
+	starttime = s.getStartTime() < starttime || starttime == 0 ? s.getStartTime() : starttime;
+	sources.add(s);
+    }
+
+    public String toString() {
+	String s = "MergedLogSource(size=" + size + ", start=" + starttime + ", end=" + endtime +")";
+	for (LogSource src : sources) {
+	    s += "\n\t- " +src;
+	}
+	return s;
+    }
+
+    public static void main(String[] args) throws IOException {
+	System.out.println("Time: " + System.currentTimeMillis());
+	MergedLogSource s = new MergedLogSource(args);
+	System.out.println(s);
+
+	LogIterator iter;
+
+	iter = s.iterator();
+	System.out.println("Time: " + System.currentTimeMillis());
+	System.out.println("Iterator Size: " + iter.size());
+	System.out.println("Time: " + System.currentTimeMillis());
+	/*	while (iter.hasNext()) {
+	    System.out.println(iter.next());
+	    }*/
+	iter.close();
+	System.out.println("Time: " + System.currentTimeMillis());
+    }
+}

+ 327 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/RandomAccessFileReader.java

@@ -0,0 +1,327 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph;
+
+import java.io.File;
+import java.io.Reader;
+import java.io.IOException;
+import java.io.EOFException;
+import java.io.RandomAccessFile;
+import java.io.FileNotFoundException;
+
+import java.io.DataInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.DataInput;
+
+import org.apache.log4j.Logger;
+
+public class RandomAccessFileReader extends Reader implements DataInput {
+    private static final Logger LOG = Logger.getLogger(RandomAccessFileReader.class);
+    private RandomAccessFile file;
+    private byte[] buffer;
+    private int buffersize;
+    private int bufferoffset;
+    private long fileoffset;
+    private long fp;
+
+    private static final int DEFAULT_BUFFER_SIZE = 512*1024; // 512k
+    private int point = 0;
+
+    public RandomAccessFileReader(File f) throws FileNotFoundException {
+	file = new RandomAccessFile(f, "r");
+	if (LOG.isDebugEnabled()) {
+	    try {
+		LOG.debug("Opened file(" + f + ") with FD (" + file.getFD() + ")");
+	    } catch (IOException ioe) { 
+		LOG.debug("Opened file(" + f + ") coulds get FD");
+	    }
+	}
+
+	buffer = new byte[DEFAULT_BUFFER_SIZE];
+	buffersize = 0;
+	bufferoffset = 0;
+	fileoffset = 0;
+	fp = 0;
+    }
+
+    /**
+       fill the buffer from the file.
+       fp keeps track of the file pointer.
+       fileoffset is the offset into the file to where the buffer came from.
+    */
+    private int fill() throws IOException {
+	fileoffset = fp;
+	int read = file.read(buffer, 0, buffer.length);
+
+	if (LOG.isDebugEnabled()) {
+	    String buf = new String(buffer, 0, 40, "UTF-8");
+	    LOG.debug("fill(buffer=" + buf + ")");
+	}
+
+	if (read == -1) { // eof reached
+	    buffersize = 0;
+	} else {
+	    buffersize = read;
+	}
+	fp += buffersize;
+	bufferoffset = 0;
+
+	return buffersize;
+    }
+
+    /**
+     * Reader interface 
+     */
+    public boolean markSupported() { return false; }
+
+    /**
+       copy what we can from buffer. if it's not enough, fill buffer again and copy again
+    */
+    synchronized public int read(char[] cbuf, int off, int len) throws IOException {
+	// This could be faster, but probably wont be used
+	byte[] b = new byte[2];
+	int bytesread = 0;
+	while (len > 0) {
+	    int read = read(b, 0, 2);
+	    bytesread += read;
+	    if (read < 2) {
+		return bytesread;
+	    }
+	    cbuf[off] = (char)((b[0] << 8) | (b[1] & 0xff));
+	    off += read;
+	    len -= read;
+	}
+
+	return bytesread;
+    }
+
+    synchronized public int read(byte[] buf, int off, int len) throws IOException {
+	if (LOG.isTraceEnabled()) {
+	    LOG.trace("read(buf, off=" + off + ", len=" + len);
+	}
+
+	int read = 0;
+	while (len > 0) {
+	    if (buffersize == 0) {
+		fill();
+		if (buffersize == 0) {
+		    break;
+		}
+	    }
+
+	    int tocopy = Math.min(len, buffersize);
+	    if (LOG.isTraceEnabled()) {
+		LOG.trace("tocopy=" + tocopy);
+	    }
+
+	    System.arraycopy(buffer, bufferoffset, buf, off, tocopy);
+	    buffersize -= tocopy;
+	    bufferoffset += tocopy;
+
+	    len -= tocopy;
+	    read += tocopy;
+	    off += tocopy;
+	}
+	if (LOG.isTraceEnabled()) {
+	    LOG.trace("read=" + read);
+	}
+
+	return read;
+    }
+
+    public void close() throws IOException {
+	file.close();
+    }
+
+    /**
+     * Seek interface 
+     */
+    public long getPosition() {
+	return bufferoffset + fileoffset;
+    }
+    
+    synchronized public void seek(long pos) throws IOException {
+	if (LOG.isDebugEnabled()) {
+	    LOG.debug("seek(" + pos + ")");
+	}
+	file.seek(pos);
+	fp = pos;
+	buffersize = 0; // force a buffer fill on next read
+    }
+
+    /**
+       works like the usual readLine but disregards \r to make things easier
+    */
+    synchronized public String readLine() throws IOException {
+	StringBuffer s = null;
+	
+	// go through buffer until i find a \n, if i reach end of buffer first, put whats in buffer into string buffer,
+	// repeat
+	buffering:
+	for (;;) {
+	    if (buffersize == 0) {
+		fill();
+		if (buffersize == 0) {
+		    break;
+		}
+	    }
+
+	    for (int i = 0; i < buffersize; i++) {
+		if (buffer[bufferoffset + i] == '\n') { 
+		    if (i > 0) { // if \n is first char in buffer, leave the string buffer empty
+			if (s == null) { s = new StringBuffer(); }
+			s.append(new String(buffer, bufferoffset, i, "UTF-8"));
+		    }
+		    bufferoffset += i+1;
+		    buffersize -= i+1; 
+		    break buffering;
+		}
+	    }
+
+	    // We didn't find \n, read the whole buffer into string buffer
+	    if (s == null) { s = new StringBuffer(); }
+	    s.append(new String(buffer, bufferoffset, buffersize, "UTF-8"));
+	    buffersize = 0; 
+	}
+
+	if (s == null) {
+	    return null;
+	} else {
+	    return s.toString();
+	}	    
+    }
+
+    /**
+       DataInput interface
+    */
+    public void readFully(byte[] b) throws IOException {
+	readFully(b, 0, b.length);
+    }
+
+    public void readFully(byte[] b, int off, int len) throws IOException
+    {
+	while (len > 0) {
+	    int read = read(b, off, len);
+	    len -= read;
+	    off += read;
+
+	    if (read == 0) {
+		throw new EOFException("End of file reached");
+	    }	    
+	}
+    }
+
+    public int skipBytes(int n) throws IOException {
+	seek(getPosition() + n);
+	return n;
+    }
+
+    public boolean readBoolean() throws IOException {
+	return (readByte() != 0);	    
+    }
+
+    public byte readByte() throws IOException {
+	byte[] b = new byte[1];
+	readFully(b, 0, 1);
+	return b[0];
+    }
+
+    public int readUnsignedByte() throws IOException {
+	return (int)readByte();
+    }
+
+    public short readShort() throws IOException {
+	byte[] b = new byte[2];
+	readFully(b, 0, 2);
+	return (short)((b[0] << 8) | (b[1] & 0xff));
+    }
+    
+    public int readUnsignedShort() throws IOException {
+	byte[] b = new byte[2];
+	readFully(b, 0, 2);
+	return (((b[0] & 0xff) << 8) | (b[1] & 0xff));
+    }
+
+    public char readChar() throws IOException {
+	return (char)readShort();
+    }
+
+    public int readInt() throws IOException {
+	byte[] b = new byte[4];
+	readFully(b, 0, 4);
+	return (((b[0] & 0xff) << 24) | ((b[1] & 0xff) << 16) |  ((b[2] & 0xff) << 8) | (b[3] & 0xff));
+    }
+
+    public long readLong() throws IOException {
+	byte[] b = new byte[8];
+	readFully(b, 0, 8);
+	
+	return (((long)(b[0] & 0xff) << 56) |  ((long)(b[1] & 0xff) << 48) |
+		((long)(b[2] & 0xff) << 40) |  ((long)(b[3] & 0xff) << 32) |
+		((long)(b[4] & 0xff) << 24) |  ((long)(b[5] & 0xff) << 16) |
+		((long)(b[6] & 0xff) <<  8) |  ((long)(b[7] & 0xff)));
+    }
+
+    public float readFloat() throws IOException {
+	return Float.intBitsToFloat(readInt());
+    }
+
+    public double readDouble() throws IOException {
+	return Double.longBitsToDouble(readLong());
+    }
+
+    public String readUTF() throws IOException {
+	int len = readUnsignedShort();
+	byte[] bytes = new byte[len+2];
+	bytes[0] = (byte)((len >> 8) & 0xFF);
+	bytes[1] = (byte)(len & 0xFF);
+	readFully(bytes, 2, len);
+	DataInputStream dis = new DataInputStream(new ByteArrayInputStream(bytes));
+	return dis.readUTF();
+    }
+
+    public static void main(String[] args) throws IOException {
+	RandomAccessFileReader f = new RandomAccessFileReader(new File(args[0]));
+	
+	long pos0 = f.getPosition();
+	for (int i = 0; i < 5; i++) {
+	    System.out.println(f.readLine());
+	}
+	System.out.println("=============");
+	long pos1 = f.getPosition();
+	System.out.println("pos: " + pos1);
+	for (int i = 0; i < 5; i++) {
+	    System.out.println(f.readLine());
+	}
+	System.out.println("=============");
+	f.seek(pos1);
+	for (int i = 0; i < 5; i++) {
+	    System.out.println(f.readLine());
+	}
+	System.out.println("=============");
+	f.seek(pos0);
+	for (int i = 0; i < 5; i++) {
+	    System.out.println(f.readLine());
+	}
+	long pos2 = f.getPosition();
+	System.out.println("=============");
+	System.out.println(f.readLine());
+	f.seek(pos2);
+	System.out.println(f.readLine());
+    }
+};

+ 59 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/TransactionEntry.java

@@ -0,0 +1,59 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph;
+
+public class TransactionEntry extends LogEntry {
+    public TransactionEntry(long timestamp, long clientId, long Cxid, long Zxid, String op) {
+	this(timestamp, clientId, Cxid, Zxid, op, "");
+    }
+
+    public TransactionEntry(long timestamp, long clientId, long Cxid, long Zxid, String op, String extra) {
+	super(timestamp);
+	setAttribute("client-id", new Long(clientId));
+	setAttribute("cxid", new Long(Cxid));
+	setAttribute("zxid", new Long(Zxid));
+	setAttribute("operation", op);
+	setAttribute("extra", extra);
+    }
+
+    public long getClientId() {
+	return (Long)getAttribute("client-id");
+    }
+
+    public long getCxid() {
+	return (Long)getAttribute("cxid");
+    }
+
+    public long getZxid() {
+	return (Long)getAttribute("zxid");
+    }
+
+    public String getOp() {
+	return (String)getAttribute("operation");
+    }
+
+    public String getExtra() {
+	return (String)getAttribute("extra");
+    }
+
+    public String toString() {
+	return getTimestamp() + ":::session(0x" + Long.toHexString(getClientId()) + ") cxid(0x" + Long.toHexString(getCxid()) + ") zxid(0x" + Long.toHexString(getZxid()) + ") op(" + getOp() + ") extra(" + getExtra() +")";
+    }
+
+    public Type getType() { return LogEntry.Type.TXN; }
+}

+ 377 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/TxnLogSource.java

@@ -0,0 +1,377 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph;
+
+import java.io.ByteArrayInputStream;
+import java.io.EOFException;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.text.DateFormat;
+import java.util.Date;
+import java.util.zip.Adler32;
+import java.util.zip.Checksum;
+import java.util.HashMap;
+
+import org.apache.jute.BinaryInputArchive;
+import org.apache.jute.InputArchive;
+import org.apache.jute.Record;
+import org.apache.zookeeper.server.TraceFormatter;
+import org.apache.zookeeper.server.persistence.FileHeader;
+import org.apache.zookeeper.server.persistence.FileTxnLog;
+import org.apache.zookeeper.server.util.SerializeUtils;
+import org.apache.zookeeper.txn.TxnHeader;
+
+import org.apache.zookeeper.ZooDefs.OpCode;
+
+import org.apache.zookeeper.txn.CreateSessionTxn;
+import org.apache.zookeeper.txn.CreateTxn;
+import org.apache.zookeeper.txn.DeleteTxn;
+import org.apache.zookeeper.txn.ErrorTxn;
+import org.apache.zookeeper.txn.SetACLTxn;
+import org.apache.zookeeper.txn.SetDataTxn;
+import org.apache.zookeeper.txn.TxnHeader;
+
+import java.io.File;
+import java.io.Closeable;
+import java.io.FileNotFoundException;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+import org.apache.log4j.Logger;
+
+public class TxnLogSource implements LogSource {
+    private static final Logger LOG = Logger.getLogger(TxnLogSource.class);
+
+    private LogSkipList skiplist = null;
+    private static final int skipN = 10000;
+
+    private String file = null;
+    private long starttime = 0;
+    private long endtime = 0;
+    private long size = 0;
+
+    public boolean overlapsRange(long starttime, long endtime) {
+	return (starttime <= this.endtime && endtime >= this.starttime);
+    }
+
+    public long size() { return size; }
+    public long getStartTime() { return starttime; }
+    public long getEndTime() { return endtime; }
+    public LogSkipList getSkipList() { return skiplist; }
+
+    public static boolean isTransactionFile(String file) throws IOException {
+        RandomAccessFileReader reader = new RandomAccessFileReader(new File(file));
+        BinaryInputArchive logStream = new BinaryInputArchive(reader);
+        FileHeader fhdr = new FileHeader();
+        fhdr.deserialize(logStream, "fileheader");
+	reader.close();
+
+        return fhdr.getMagic() == FileTxnLog.TXNLOG_MAGIC;
+    }
+
+    private class TxnLogSourceIterator implements LogIterator {
+	private LogEntry next = null;
+	private long starttime = 0;
+	private long endtime = 0;
+	private TxnLogSource src = null;
+	private RandomAccessFileReader reader = null;
+	private BinaryInputArchive logStream = null;
+	private long skippedAtStart = 0;
+	private FilterOp filter = null;
+
+	public TxnLogSourceIterator(TxnLogSource src, long starttime, long endtime) throws IllegalArgumentException, FilterException {
+	    this(src,starttime,endtime,null);
+	}
+	
+	public TxnLogSourceIterator(TxnLogSource src, long starttime, long endtime, FilterOp filter) throws IllegalArgumentException, FilterException {
+	    try {
+		this.src = src;
+		this.starttime = starttime;
+		this.endtime = endtime;
+		reader = new RandomAccessFileReader(new File(src.file));
+		logStream = new BinaryInputArchive(reader);
+		FileHeader fhdr = new FileHeader();
+		fhdr.deserialize(logStream, "fileheader");
+	    } catch (Exception e) {
+		throw new IllegalArgumentException("Cannot open transaction log ("+src.file+") :" + e);
+	    }
+	    
+	    LogSkipList.Mark start = src.getSkipList().findMarkBefore(starttime);
+	    try {
+		reader.seek(start.getBytes());
+		skippedAtStart = start.getEntriesSkipped();
+	    } catch (IOException ioe) {
+		// if we can't skip, we should just read from the start
+	    }
+
+	    this.filter = filter;
+
+	    LogEntry e;
+	    while ((e = readNextEntry()) != null && e.getTimestamp() < endtime) {
+		if (e.getTimestamp() >= starttime && (filter == null || filter.matches(e))  ) {
+		    next = e;
+		    return;
+		}
+		skippedAtStart++;
+	    }
+
+
+	}
+	
+	public long size() throws IOException {
+	    if (this.endtime >= src.getEndTime()) {
+		return src.size() - skippedAtStart;
+	    }
+	    
+	    long pos = reader.getPosition();
+	    LogEntry e;
+
+	    LogSkipList.Mark lastseg = src.getSkipList().findMarkBefore(this.endtime);
+	    reader.seek(lastseg.getBytes());
+	    // number of entries skipped to get to the end of the iterator, less the number skipped to get to the start
+	    long count = lastseg.getEntriesSkipped() - skippedAtStart; 
+
+	    while ((e = readNextEntry()) != null) {
+		if (e.getTimestamp() > this.endtime) {
+		    break;
+		}
+		count++;
+	    }
+	    reader.seek(pos);;
+
+	    return count;
+	}
+	
+	private LogEntry readNextEntry() {
+	    LogEntry e = null;
+	    try {
+		long crcValue;
+		byte[] bytes;
+		try {
+		    crcValue = logStream.readLong("crcvalue");
+		    
+		    bytes = logStream.readBuffer("txnEntry");
+		} catch (EOFException ex) {
+		    return null;
+		}
+		
+		if (bytes.length == 0) {
+		    return null;
+		}
+		Checksum crc = new Adler32();
+		crc.update(bytes, 0, bytes.length);
+		if (crcValue != crc.getValue()) {
+		    throw new IOException("CRC doesn't match " + crcValue +
+					  " vs " + crc.getValue());
+		}
+		InputArchive iab = BinaryInputArchive.getArchive(new ByteArrayInputStream(bytes));
+		TxnHeader hdr = new TxnHeader();
+		Record r = SerializeUtils.deserializeTxn(iab, hdr);
+
+		switch (hdr.getType()) {
+		case OpCode.createSession: {
+		    e = new TransactionEntry(hdr.getTime(), hdr.getClientId(), hdr.getCxid(), hdr.getZxid(), "createSession");
+		}
+		    break;
+		case OpCode.closeSession: {
+		    e = new TransactionEntry(hdr.getTime(), hdr.getClientId(), hdr.getCxid(), hdr.getZxid(), "closeSession");
+		}
+		    break;
+		case OpCode.create:
+		    if (r != null) {
+			CreateTxn create = (CreateTxn)r;
+			String path = create.getPath();
+			e = new TransactionEntry(hdr.getTime(), hdr.getClientId(), hdr.getCxid(), hdr.getZxid(), "create", path);
+		    }
+		    break;
+		case OpCode.setData:
+		    if (r != null) {
+			SetDataTxn set = (SetDataTxn)r;
+			String path = set.getPath();
+			e = new TransactionEntry(hdr.getTime(), hdr.getClientId(), hdr.getCxid(), hdr.getZxid(), "setData", path);
+		    }
+		    break;
+		case OpCode.setACL:
+		    if (r != null) {
+			SetACLTxn setacl = (SetACLTxn)r;
+			String path = setacl.getPath();
+		    e = new TransactionEntry(hdr.getTime(), hdr.getClientId(), hdr.getCxid(), hdr.getZxid(), "setACL", path);
+		    }
+		    break;
+		case OpCode.error:
+		    if (r != null)  {
+			ErrorTxn error = (ErrorTxn)r;
+			
+			e = new TransactionEntry(hdr.getTime(), hdr.getClientId(), hdr.getCxid(), hdr.getZxid(), "error", "Error: " + error.getErr());
+		    }
+		    break;
+		default:
+		    LOG.info("Unknown op: " + hdr.getType());
+		    break;
+		}
+		
+		if (logStream.readByte("EOR") != 'B') {
+		    throw new EOFException("Last transaction was partial.");
+		}
+	    } catch (Exception ex) {
+		LOG.error("Error reading transaction from (" + src.file + ") :" + e);
+		return null;
+	    }
+	    return e;
+	}
+
+	public boolean hasNext() {
+	    return next != null;
+	}
+	
+	public LogEntry next() throws NoSuchElementException {
+	    LogEntry ret = next;
+	    LogEntry e = readNextEntry();
+
+	    if (filter != null) {
+		try {
+		    while (e != null && !filter.matches(e)) {
+			e = readNextEntry();
+		    }
+		} catch (FilterException fe) {
+		    throw new NoSuchElementException(fe.toString());
+		}
+	    }
+	    if (e != null && e.getTimestamp() < endtime) {
+		next = e;
+	    } else {
+		next = null;
+	    }
+	    return ret;
+	}
+
+	public void remove() throws UnsupportedOperationException {
+	    throw new UnsupportedOperationException("remove not supported for Txn logs");
+	}
+	
+	public void close() throws IOException {
+	    reader.close();
+	}
+    }
+
+    public LogIterator iterator(long starttime, long endtime) throws IllegalArgumentException {
+	try {
+	    return iterator(starttime, endtime, null);
+	} catch (FilterException fe) {
+	    assert(false); // should never ever happen
+	    return null;
+	}
+    }
+
+    public LogIterator iterator(long starttime, long endtime, FilterOp filter) throws IllegalArgumentException, FilterException {
+	// sanitise start and end times
+	if (endtime < starttime) {
+	    throw new IllegalArgumentException("End time (" +  endtime + ") must be greater or equal to starttime (" + starttime + ")");
+	}
+
+	return new TxnLogSourceIterator(this, starttime, endtime, filter);
+    }
+
+    public LogIterator iterator() throws IllegalArgumentException {
+	return iterator(starttime, endtime+1);
+    }
+    
+    public TxnLogSource(String file) throws IOException {
+	this.file = file;
+
+	skiplist = new LogSkipList();
+
+	RandomAccessFileReader reader = new RandomAccessFileReader(new File(file));
+	try {
+	    BinaryInputArchive logStream = new BinaryInputArchive(reader);
+	    FileHeader fhdr = new FileHeader();
+	    fhdr.deserialize(logStream, "fileheader");
+	    
+	    byte[] bytes = null;
+	    while (true) {
+		long lastFp = reader.getPosition();
+
+		long crcValue;
+
+		try {
+		    crcValue = logStream.readLong("crcvalue");
+		    bytes = logStream.readBuffer("txnEntry");
+		} catch (EOFException e) {
+		    break;
+		}
+		
+		if (bytes.length == 0) {
+		    break;
+		}
+		Checksum crc = new Adler32();
+		crc.update(bytes, 0, bytes.length);
+		if (crcValue != crc.getValue()) {
+		    throw new IOException("CRC doesn't match " + crcValue +
+					  " vs " + crc.getValue());
+		}
+		if (logStream.readByte("EOR") != 'B') {
+		    throw new EOFException("Last transaction was partial.");
+		}
+		InputArchive iab = BinaryInputArchive.getArchive(new ByteArrayInputStream(bytes));
+		TxnHeader hdr = new TxnHeader();
+		Record r = SerializeUtils.deserializeTxn(iab, hdr);
+		
+		if (starttime == 0) {
+		    starttime = hdr.getTime();
+		}
+		endtime = hdr.getTime();
+
+		if (size % skipN == 0) {
+		    skiplist.addMark(hdr.getTime(), lastFp, size);
+		}
+		size++;
+	    }
+	    if (bytes == null) {
+		throw new IOException("Nothing read from ("+file+")");
+	    }
+	} finally {
+	    reader.close();
+	}
+    }
+
+    public String toString() {
+	return "TxnLogSource(file=" + file + ", size=" + size + ", start=" + starttime + ", end=" + endtime +")";
+    }
+
+    public static void main(String[] args) throws IOException, FilterException {
+	TxnLogSource s = new TxnLogSource(args[0]);
+	System.out.println(s);
+
+	LogIterator iter;
+
+	if (args.length == 3) {
+	    long starttime = Long.valueOf(args[1]);
+	    long endtime = Long.valueOf(args[2]);
+	    FilterOp fo = new FilterParser("(or (and (> zxid 0x2f0bd6f5e0) (< zxid 0x2f0bd6f5e9)) (= operation \"error\"))").parse();
+	    System.out.println("fo: " + fo);
+	    iter = s.iterator(starttime, endtime, fo);
+	} else {
+	    iter = s.iterator();
+	}
+	System.out.println(iter);
+	while (iter.hasNext()) {
+	    	    System.out.println(iter.next());
+	}
+	iter.close();
+    }
+}

+ 33 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/filterops/AndOp.java

@@ -0,0 +1,33 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph.filterops;
+
+import org.apache.zookeeper.graph.LogEntry;
+import org.apache.zookeeper.graph.FilterOp;
+import org.apache.zookeeper.graph.FilterException;
+
+public class AndOp extends FilterOp {
+    public boolean matches(LogEntry entry) throws FilterException {
+	for (FilterOp f : subOps) {
+	    if (!f.matches(entry)) {
+		return false;
+	    }
+	}
+	return true;
+    }
+}

+ 36 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/filterops/Arg.java

@@ -0,0 +1,36 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph.filterops;
+
+import org.apache.zookeeper.graph.FilterOp.*;
+
+public class Arg<T> {
+    private ArgType type;
+    protected T value;
+    
+    protected Arg(ArgType type) {
+	this.type = type;
+    }
+    
+    public ArgType getType() { return type; }
+    public T getValue() { return value; }
+
+    public String toString() {
+	return "[" + type + ":" + value + "]";
+    }
+}

+ 44 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/filterops/EqualsOp.java

@@ -0,0 +1,44 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph.filterops;
+
+import org.apache.zookeeper.graph.LogEntry;
+import org.apache.zookeeper.graph.FilterOp;
+import org.apache.zookeeper.graph.FilterException;
+
+public class EqualsOp extends FilterOp {
+    public boolean matches(LogEntry entry) throws FilterException {
+
+	Object last = null;
+	for (Arg a : args) {
+	    Object v = a.getValue();
+	    if (a.getType() == FilterOp.ArgType.SYMBOL) {
+		String key = (String)a.getValue();
+		v = entry.getAttribute(key);
+	    }
+
+	    if (last != null
+		&& !last.equals(v)) {
+		return false;
+	    }
+	    last = v;
+	}
+
+	return true;
+    }
+}    

+ 70 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/filterops/GreaterThanOp.java

@@ -0,0 +1,70 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph.filterops;
+
+import org.apache.zookeeper.graph.LogEntry;
+import org.apache.zookeeper.graph.FilterOp;
+import org.apache.zookeeper.graph.FilterException;
+
+public class GreaterThanOp extends FilterOp {
+    public boolean matches(LogEntry entry) throws FilterException {
+	Arg first = args.get(0);
+	
+	if (first != null) {
+	    FilterOp.ArgType type = first.getType();
+	    if (type == FilterOp.ArgType.SYMBOL) {
+		String key = (String)first.getValue();
+		Object v = entry.getAttribute(key);
+		if (v instanceof String) {
+		    type = FilterOp.ArgType.STRING;
+		} else if (v instanceof Double || v instanceof Long || v instanceof Integer || v instanceof Short) {
+		    type = FilterOp.ArgType.NUMBER;
+		} else {
+		    throw new FilterException("LessThanOp: Invalid argument, first argument resolves to neither a String nor a Number");
+		}
+	    }
+	    
+	    Object last = null;
+	    for (Arg a : args) {
+		Object v = a.getValue();
+		if (a.getType() == FilterOp.ArgType.SYMBOL) {
+		    String key = (String)a.getValue();
+		    v = entry.getAttribute(key);
+		}
+
+		if (last != null) {
+		    if (type == FilterOp.ArgType.STRING) {
+			if (((String)last).compareTo((String)v) <= 0) {
+			    return false;
+			}
+		    } else if (type == FilterOp.ArgType.NUMBER) {
+			//			System.out.println("last[" + ((Number)last).longValue() + "] v["+ ((Number)v).longValue() + "]");
+			if (((Number)last).longValue() <= ((Number)v).longValue()) {
+			    return false;
+			}
+		    }
+		}
+		last = v;
+	    }
+	    return true;
+	} else { 
+	    return true; 
+	}
+    }
+	
+}    

+ 69 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/filterops/LessThanOp.java

@@ -0,0 +1,69 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph.filterops;
+
+import org.apache.zookeeper.graph.LogEntry;
+import org.apache.zookeeper.graph.FilterOp;
+import org.apache.zookeeper.graph.FilterException;
+
+public class LessThanOp extends FilterOp {
+    public boolean matches(LogEntry entry) throws FilterException {
+	Arg first = args.get(0);
+	
+	if (first != null) {
+	    FilterOp.ArgType type = first.getType();
+	    if (type == FilterOp.ArgType.SYMBOL) {
+		String key = (String)first.getValue();
+		Object v = entry.getAttribute(key);
+		if (v instanceof String) {
+		    type = FilterOp.ArgType.STRING;
+		} else if (v instanceof Double || v instanceof Long || v instanceof Integer || v instanceof Short) {
+		    type = FilterOp.ArgType.NUMBER;
+		} else {
+		    throw new FilterException("LessThanOp: Invalid argument, first argument resolves to neither a String nor a Number");
+		}
+	    }
+	    
+	    Object last = null;
+	    for (Arg a : args) {
+		Object v = a.getValue();
+		if (a.getType() == FilterOp.ArgType.SYMBOL) {
+		    String key = (String)a.getValue();
+		    v = entry.getAttribute(key);
+		}
+
+		if (last != null) {
+		    if (type == FilterOp.ArgType.STRING) {
+			if (((String)last).compareTo((String)v) >= 0) {
+			    return false;
+			}
+		    } else if (type == FilterOp.ArgType.NUMBER) {
+			if (((Number)last).doubleValue() >= ((Number)v).doubleValue()) {
+			    return false;
+			}
+		    }
+		}
+		last = v;
+	    }
+	    return true;
+	} else { 
+	    return true; 
+	}
+    }
+	
+}    

+ 31 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/filterops/NotOp.java

@@ -0,0 +1,31 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph.filterops;
+
+import org.apache.zookeeper.graph.LogEntry;
+import org.apache.zookeeper.graph.FilterOp;
+import org.apache.zookeeper.graph.FilterException;
+
+public class NotOp extends FilterOp {
+    public boolean matches(LogEntry entry) throws FilterException {
+	if (subOps.size() != 1) {
+	    throw new FilterException("Not operation can only take one argument");
+	}
+	return !subOps.get(0).matches(entry);
+    }
+}

+ 28 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/filterops/NumberArg.java

@@ -0,0 +1,28 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph.filterops;
+
+import org.apache.zookeeper.graph.FilterOp.*;
+
+public class NumberArg extends Arg<Long> {
+    public NumberArg(Long value) {
+	super(ArgType.NUMBER);
+	this.value = value;
+    }
+};
+

+ 33 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/filterops/OrOp.java

@@ -0,0 +1,33 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph.filterops;
+
+import org.apache.zookeeper.graph.LogEntry;
+import org.apache.zookeeper.graph.FilterOp;
+import org.apache.zookeeper.graph.FilterException;
+
+public class OrOp extends FilterOp {
+    public boolean matches(LogEntry entry) throws FilterException {
+	for (FilterOp f : subOps) {
+	    if (f.matches(entry)) {
+		return true;
+	    }
+	}
+	return false;
+    }
+}    

+ 28 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/filterops/StringArg.java

@@ -0,0 +1,28 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph.filterops;
+
+import org.apache.zookeeper.graph.FilterOp.*;
+
+public class StringArg extends Arg<String> {
+    public StringArg(String value) {
+	super(ArgType.STRING);
+	    this.value = value;
+    }
+};
+

+ 27 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/filterops/SymbolArg.java

@@ -0,0 +1,27 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph.filterops;
+
+import org.apache.zookeeper.graph.FilterOp.*;
+
+public class SymbolArg extends Arg<String> {
+    public SymbolArg(String value) {
+	super(ArgType.SYMBOL);
+	this.value = value;
+    }
+};

+ 40 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/filterops/XorOp.java

@@ -0,0 +1,40 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph.filterops;
+
+import org.apache.zookeeper.graph.LogEntry;
+import org.apache.zookeeper.graph.FilterOp;
+import org.apache.zookeeper.graph.FilterException;
+
+public class XorOp extends FilterOp {
+    public boolean matches(LogEntry entry) throws FilterException {
+	int count = 0;
+	for (FilterOp f : subOps) {
+	    if (f.matches(entry)) {
+		count++;
+		if (count > 1) {
+		    return false;
+		}
+	    }
+	}
+	if (count == 1) {
+	    return true;
+	}
+	return false;
+    }
+}    

+ 60 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/servlets/FileLoader.java

@@ -0,0 +1,60 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph.servlets;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.FileNotFoundException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.JSONValue;
+
+import org.apache.zookeeper.graph.*;
+
+public class FileLoader extends JsonServlet
+{
+    private MergedLogSource source = null;
+    
+    public FileLoader(MergedLogSource src) throws Exception {
+	source = src;
+    }
+
+    String handleRequest(JsonRequest request) throws Exception
+    {
+	String output = "";
+		
+	String file = request.getString("path", "/");
+	JSONObject o = new JSONObject();
+	try {
+	    this.source.addSource(file);
+	    o.put("status", "OK");
+	
+	} catch (Exception e) {
+	    o.put("status", "ERR");
+	    o.put("error",  e.toString());
+	}
+	
+	return JSONValue.toJSONString(o);
+    }
+}

+ 69 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/servlets/Fs.java

@@ -0,0 +1,69 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph.servlets;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.FileNotFoundException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.JSONValue;
+import java.util.Arrays;
+import java.util.Comparator;
+
+public class Fs extends JsonServlet
+{
+    String handleRequest(JsonRequest request) throws Exception
+    {
+	String output = "";
+	JSONArray filelist = new JSONArray();
+
+	File base = new File(request.getString("path", "/"));
+	if (!base.exists() || !base.isDirectory()) {
+	    throw new FileNotFoundException("Couldn't find [" + request + "]");
+	}
+	File[] files = base.listFiles();
+	Arrays.sort(files, new Comparator<File>() { 
+		public int compare(File o1, File o2) {
+		    if (o1.isDirectory() != o2.isDirectory()) {
+			if (o1.isDirectory()) {
+			    return -1;
+			} else {
+			    return 1;
+			}
+		    }
+		    return o1.getName().compareToIgnoreCase(o2.getName());
+		} 
+	    });
+	
+	for (File f : files) {
+	    JSONObject o = new JSONObject();
+	    o.put("file", f.getName());
+	    o.put("type", f.isDirectory() ? "D" : "F");
+	    o.put("path", f.getCanonicalPath());
+	    filelist.add(o);
+	}
+	return JSONValue.toJSONString(filelist);
+    }
+}

+ 84 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/servlets/GraphData.java

@@ -0,0 +1,84 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph.servlets;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.FileNotFoundException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.JSONValue;
+
+import org.apache.zookeeper.graph.*;
+import org.apache.log4j.Logger;
+
+public class GraphData extends JsonServlet
+{
+    private static final Logger LOG = Logger.getLogger(GraphData.class);
+    private static final int DEFAULT_PERIOD = 1000;
+
+    private LogSource source = null;
+
+    public GraphData(LogSource src) throws Exception {
+	this.source = src; 
+    }
+
+    String handleRequest(JsonRequest request) throws Exception {
+	
+
+	long starttime = 0;
+	long endtime = 0;
+	long period = 0;
+	FilterOp fo = null;
+
+	starttime = request.getNumber("start", 0);
+	endtime = request.getNumber("end", 0);
+	period = request.getNumber("period", 0);
+	String filterstr = request.getString("filter", "");
+
+	if (filterstr.length() > 0) {
+	    fo = new FilterParser(filterstr).parse();
+	}
+	
+	if (starttime == 0) { starttime = source.getStartTime(); }
+	if (endtime == 0) { 
+	    if (period > 0) {
+		endtime = starttime + period;
+	    } else {
+		endtime = starttime + DEFAULT_PERIOD; 
+	    }
+	}
+
+	if (LOG.isDebugEnabled()) {
+	    LOG.debug("handle(start= " + starttime + ", end=" + endtime + ", period=" + period + ")");
+	}
+	
+	LogIterator iterator = (fo != null) ? 
+	    source.iterator(starttime, endtime, fo) : source.iterator(starttime, endtime);
+	return new JsonGenerator(iterator).toString();
+    }
+}

+ 85 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/servlets/JsonServlet.java

@@ -0,0 +1,85 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph.servlets;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.json.simple.JSONObject;
+import org.json.simple.JSONValue;
+
+import java.util.Map;
+
+abstract public class JsonServlet extends HttpServlet {
+    abstract String handleRequest(JsonRequest request) throws Exception;
+
+    protected class JsonRequest {
+	private Map map;
+
+	public JsonRequest(ServletRequest request) {
+	    map = request.getParameterMap();
+	}
+	
+	public long getNumber(String name, long defaultnum) {
+	    String[] vals = (String[])map.get(name);
+	    if (vals == null || vals.length == 0) {
+		return defaultnum;
+	    }
+
+	    try {
+		return Long.valueOf(vals[0]);
+	    } catch (NumberFormatException e) {
+		return defaultnum;
+	    }
+	}
+	
+	public String getString(String name, String defaultstr) {
+	    String[] vals = (String[])map.get(name);
+	    if (vals == null || vals.length == 0) {
+		return defaultstr;
+	    } else {
+		return vals[0];
+	    }
+	}
+    }
+
+    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
+    {
+        response.setContentType("text/plain;charset=utf-8");
+        response.setStatus(HttpServletResponse.SC_OK);
+	
+	try {
+	    String req = request.getRequestURI().substring(request.getServletPath().length());
+
+	    response.getWriter().println(handleRequest(new JsonRequest(request)));
+	} catch (Exception e) {
+	    JSONObject o = new JSONObject();
+	    o.put("error", e.toString());
+	    response.getWriter().println(JSONValue.toJSONString(o));
+	} catch (java.lang.OutOfMemoryError oom) {
+	    JSONObject o = new JSONObject();
+	    o.put("error", "Out of memory. Perhaps you've requested too many logs. Try narrowing you're filter criteria.");
+	    response.getWriter().println(JSONValue.toJSONString(o));
+	}
+    }
+}

+ 86 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/servlets/NumEvents.java

@@ -0,0 +1,86 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph.servlets;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.FileNotFoundException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.JSONValue;
+
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+
+import org.apache.zookeeper.graph.*;
+import org.apache.log4j.Logger;
+
+
+public class NumEvents extends JsonServlet
+{
+    private static final Logger LOG = Logger.getLogger(NumEvents.class);
+    private static final int DEFAULT_PERIOD = 1000;
+
+    private LogSource source = null;
+
+    public NumEvents(LogSource src) throws Exception {
+	this.source = src;
+    }
+
+    String handleRequest(JsonRequest request) throws Exception {
+	String output = "";
+
+	long starttime = 0;
+	long endtime = 0;
+	long period = 0;
+
+	starttime = request.getNumber("start", 0);
+	endtime = request.getNumber("end", 0);
+	period = request.getNumber("period", 0);
+
+	if (starttime == 0) { starttime = source.getStartTime(); }
+	if (endtime == 0) { 
+	    if (period > 0) {
+		endtime = starttime + period;
+	    } else {
+		endtime = source.getEndTime(); 
+	    }
+	}
+	
+	LogIterator iter = source.iterator(starttime, endtime);
+	JSONObject data = new JSONObject();
+	data.put("startTime", starttime);
+	data.put("endTime", endtime);
+	long size = 0;
+	
+	size = iter.size();
+	
+	data.put("numEntries",  size);
+	if (LOG.isDebugEnabled()) {
+	    LOG.debug("handle(start= " + starttime + ", end=" + endtime + ", numEntries=" + size +")");
+	}
+	return JSONValue.toJSONString(data);
+    }
+}
+

+ 50 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/servlets/StaticContent.java

@@ -0,0 +1,50 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph.servlets;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.BufferedReader;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class StaticContent extends HttpServlet {
+    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
+    {
+	String path = request.getRequestURI().substring(request.getServletPath().length());
+
+	InputStream resource = ClassLoader.getSystemResourceAsStream("org/apache/zookeeper/graph/resources" + path);	  
+	if (resource == null) {
+	    response.getWriter().println(path + " not found!");
+	    response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+	    return;
+	}
+	
+	while (resource.available() > 0) {
+	    response.getWriter().write(resource.read());
+	}
+	//        response.setContentType("text/plain;charset=utf-8");
+        response.setStatus(HttpServletResponse.SC_OK);
+    }
+
+}

+ 124 - 0
src/contrib/loggraph/src/java/org/apache/zookeeper/graph/servlets/Throughput.java

@@ -0,0 +1,124 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.zookeeper.graph.servlets;
+
+import java.io.IOException;
+import java.io.BufferedOutputStream;
+import java.io.FileOutputStream;
+import java.io.DataOutputStream;
+import java.io.PrintStream;
+
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+
+import org.apache.zookeeper.graph.*;
+import org.apache.log4j.Logger;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.JSONValue;
+
+
+public class Throughput extends JsonServlet
+{
+    private static final int MS_PER_SEC = 1000;
+    private static final int MS_PER_MIN = MS_PER_SEC*60;
+    private static final int MS_PER_HOUR = MS_PER_MIN*60;
+
+    private LogSource source = null;
+
+    public Throughput(LogSource src) throws Exception {
+	this.source = src; 
+    }
+
+    public String handleRequest(JsonRequest request) throws Exception {
+	long starttime = 0;
+	long endtime = 0;
+	long period = 0;
+	long scale = 0;
+	
+	starttime = request.getNumber("start", 0);
+	endtime = request.getNumber("end", 0);
+	period = request.getNumber("period", 0);
+	
+
+	if (starttime == 0) { starttime = source.getStartTime(); }
+	if (endtime == 0) { 
+	    if (period > 0) {
+		endtime = starttime + period;
+	    } else {
+		endtime = source.getEndTime(); 
+	    }
+	}
+	
+	String scalestr = request.getString("scale", "minutes");
+	if (scalestr.equals("seconds")) {
+	    scale = MS_PER_SEC;
+	} else if (scalestr.equals("hours")) {
+	    scale = MS_PER_HOUR;
+	} else {
+	    scale = MS_PER_MIN;
+	} 	
+	
+	LogIterator iter = source.iterator(starttime, endtime);
+	
+	long current = 0;
+	long currentms = 0;
+	HashSet<Long> zxids_ms = new HashSet<Long>();
+	long zxidcount = 0;
+
+	JSONArray events = new JSONArray();
+	while (iter.hasNext()) {
+	    LogEntry e = iter.next();
+	    if (e.getType() != LogEntry.Type.TXN) {
+		continue;
+	    }
+
+	    TransactionEntry cxn = (TransactionEntry)e;
+	    
+	    long ms = cxn.getTimestamp();
+	    long inscale = ms/scale;
+
+	    if (currentms != ms && currentms != 0) {
+		zxidcount += zxids_ms.size();
+		zxids_ms.clear();
+	    }
+
+	    if (inscale != current && current != 0) {
+		JSONObject o = new JSONObject();
+		o.put("time", current*scale);
+		o.put("count", zxidcount);
+		events.add(o);
+		zxidcount = 0;
+	    }
+	    current = inscale;
+	    currentms = ms;
+
+	    zxids_ms.add(cxn.getZxid());
+	}
+	JSONObject o = new JSONObject();
+	o.put("time", current*scale);
+	o.put("count", zxidcount);
+	events.add(o);
+
+	iter.close();
+	
+	return JSONValue.toJSONString(events);
+    }
+
+};

+ 11 - 0
src/contrib/loggraph/web/org/apache/zookeeper/graph/log4j.properties

@@ -0,0 +1,11 @@
+log4j.rootLogger=TRACE, CONSOLE
+
+# Print the date in ISO 8601 format
+log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
+log4j.appender.CONSOLE.Threshold=TRACE
+log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
+log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} - %-5p [%t:%C{1}@%L] - %m%n
+
+log4j.logger.org.apache.zookeeper.graph.LogSkipList=off
+log4j.logger.org.apache.zookeeper.graph.RandomAccessFileReader=off
+#log4j.logger.org.apache.zookeeper.graph.Log4JSource=off

+ 126 - 0
src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/date.format.js

@@ -0,0 +1,126 @@
+/*
+ * Date Format 1.2.3
+ * (c) 2007-2009 Steven Levithan <stevenlevithan.com>
+ * MIT license
+ *
+ * Includes enhancements by Scott Trenda <scott.trenda.net>
+ * and Kris Kowal <cixar.com/~kris.kowal/>
+ *
+ * Accepts a date, a mask, or a date and a mask.
+ * Returns a formatted version of the given date.
+ * The date defaults to the current date/time.
+ * The mask defaults to dateFormat.masks.default.
+ */
+
+var dateFormat = function () {
+	var	token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g,
+		timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g,
+		timezoneClip = /[^-+\dA-Z]/g,
+		pad = function (val, len) {
+			val = String(val);
+			len = len || 2;
+			while (val.length < len) val = "0" + val;
+			return val;
+		};
+
+	// Regexes and supporting functions are cached through closure
+	return function (date, mask, utc) {
+		var dF = dateFormat;
+
+		// You can't provide utc if you skip other args (use the "UTC:" mask prefix)
+		if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) {
+			mask = date;
+			date = undefined;
+		}
+
+		// Passing date through Date applies Date.parse, if necessary
+		date = date ? new Date(date) : new Date;
+		if (isNaN(date)) throw SyntaxError("invalid date");
+
+		mask = String(dF.masks[mask] || mask || dF.masks["default"]);
+
+		// Allow setting the utc argument via the mask
+		if (mask.slice(0, 4) == "UTC:") {
+			mask = mask.slice(4);
+			utc = true;
+		}
+
+		var	_ = utc ? "getUTC" : "get",
+			d = date[_ + "Date"](),
+			D = date[_ + "Day"](),
+			m = date[_ + "Month"](),
+			y = date[_ + "FullYear"](),
+			H = date[_ + "Hours"](),
+			M = date[_ + "Minutes"](),
+			s = date[_ + "Seconds"](),
+			L = date[_ + "Milliseconds"](),
+			o = utc ? 0 : date.getTimezoneOffset(),
+			flags = {
+				d:    d,
+				dd:   pad(d),
+				ddd:  dF.i18n.dayNames[D],
+				dddd: dF.i18n.dayNames[D + 7],
+				m:    m + 1,
+				mm:   pad(m + 1),
+				mmm:  dF.i18n.monthNames[m],
+				mmmm: dF.i18n.monthNames[m + 12],
+				yy:   String(y).slice(2),
+				yyyy: y,
+				h:    H % 12 || 12,
+				hh:   pad(H % 12 || 12),
+				H:    H,
+				HH:   pad(H),
+				M:    M,
+				MM:   pad(M),
+				s:    s,
+				ss:   pad(s),
+				l:    pad(L, 3),
+				L:    pad(L > 99 ? Math.round(L / 10) : L),
+				t:    H < 12 ? "a"  : "p",
+				tt:   H < 12 ? "am" : "pm",
+				T:    H < 12 ? "A"  : "P",
+				TT:   H < 12 ? "AM" : "PM",
+				Z:    utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""),
+				o:    (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4),
+				S:    ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10]
+			};
+
+		return mask.replace(token, function ($0) {
+			return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1);
+		});
+	};
+}();
+
+// Some common format strings
+dateFormat.masks = {
+	"default":      "ddd mmm dd yyyy HH:MM:ss",
+	shortDate:      "m/d/yy",
+	mediumDate:     "mmm d, yyyy",
+	longDate:       "mmmm d, yyyy",
+	fullDate:       "dddd, mmmm d, yyyy",
+	shortTime:      "h:MM TT",
+	mediumTime:     "h:MM:ss TT",
+	longTime:       "h:MM:ss TT Z",
+	isoDate:        "yyyy-mm-dd",
+	isoTime:        "HH:MM:ss",
+	isoDateTime:    "yyyy-mm-dd'T'HH:MM:ss",
+	isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'"
+};
+
+// Internationalization strings
+dateFormat.i18n = {
+	dayNames: [
+		"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
+		"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
+	],
+	monthNames: [
+		"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
+		"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
+	]
+};
+
+// For convenience...
+Date.prototype.format = function (mask, utc) {
+	return dateFormat(this, mask, utc);
+};
+

+ 385 - 0
src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/g.bar.js

@@ -0,0 +1,385 @@
+/*
+ * g.Raphael 0.4 - Charting library, based on Raphaël
+ *
+ * Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com)
+ * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
+ */
+Raphael.fn.g.barchart = function (x, y, width, height, values, opts) {
+    opts = opts || {};
+    var type = {round: "round", sharp: "sharp", soft: "soft"}[opts.type] || "square",
+        gutter = parseFloat(opts.gutter || "20%"),
+        chart = this.set(),
+        bars = this.set(),
+        covers = this.set(),
+        covers2 = this.set(),
+        total = Math.max.apply(Math, values),
+        stacktotal = [],
+        paper = this,
+        multi = 0,
+        colors = opts.colors || this.g.colors,
+        len = values.length;
+    if (this.raphael.is(values[0], "array")) {
+        total = [];
+        multi = len;
+        len = 0;
+        for (var i = values.length; i--;) {
+            bars.push(this.set());
+            total.push(Math.max.apply(Math, values[i]));
+            len = Math.max(len, values[i].length);
+        }
+        if (opts.stacked) {
+            for (var i = len; i--;) {
+                var tot = 0;
+                for (var j = values.length; j--;) {
+                    tot +=+ values[j][i] || 0;
+                }
+                stacktotal.push(tot);
+            }
+        }
+        for (var i = values.length; i--;) {
+            if (values[i].length < len) {
+                for (var j = len; j--;) {
+                    values[i].push(0);
+                }
+            }
+        }
+        total = Math.max.apply(Math, opts.stacked ? stacktotal : total);
+    }
+    
+    total = (opts.to) || total;
+    var barwidth = width / (len * (100 + gutter) + gutter) * 100,
+        barhgutter = barwidth * gutter / 100,
+        barvgutter = opts.vgutter == null ? 20 : opts.vgutter,
+        stack = [],
+        X = x + barhgutter,
+        Y = (height - 2 * barvgutter) / total;
+    if (!opts.stretch) {
+        barhgutter = Math.round(barhgutter);
+        barwidth = Math.floor(barwidth);
+    }
+    !opts.stacked && (barwidth /= multi || 1);
+    for (var i = 0; i < len; i++) {
+        stack = [];
+        for (var j = 0; j < (multi || 1); j++) {
+            var h = Math.round((multi ? values[j][i] : values[i]) * Y),
+                top = y + height - barvgutter - h,
+                bar = this.g.finger(Math.round(X + barwidth / 2), top + h, barwidth, h, true, type).attr({stroke: colors[multi ? j : i], fill: colors[multi ? j : i]});
+            if (multi) {
+                bars[j].push(bar);
+            } else {
+                bars.push(bar);
+            }
+            bar.y = top;
+            bar.x = Math.round(X + barwidth / 2);
+            bar.w = barwidth;
+            bar.h = h;
+            bar.value = multi ? values[j][i] : values[i];
+            if (!opts.stacked) {
+                X += barwidth;
+            } else {
+                stack.push(bar);
+            }
+        }
+        if (opts.stacked) {
+            var cvr;
+            covers2.push(cvr = this.rect(stack[0].x - stack[0].w / 2, y, barwidth, height).attr(this.g.shim));
+            cvr.bars = this.set();
+            var size = 0;
+            for (var s = stack.length; s--;) {
+                stack[s].toFront();
+            }
+            for (var s = 0, ss = stack.length; s < ss; s++) {
+                var bar = stack[s],
+                    cover,
+                    h = (size + bar.value) * Y,
+                    path = this.g.finger(bar.x, y + height - barvgutter - !!size * .5, barwidth, h, true, type, 1);
+                cvr.bars.push(bar);
+                size && bar.attr({path: path});
+                bar.h = h;
+                bar.y = y + height - barvgutter - !!size * .5 - h;
+                covers.push(cover = this.rect(bar.x - bar.w / 2, bar.y, barwidth, bar.value * Y).attr(this.g.shim));
+                cover.bar = bar;
+                cover.value = bar.value;
+                size += bar.value;
+            }
+            X += barwidth;
+        }
+        X += barhgutter;
+    }
+    covers2.toFront();
+    X = x + barhgutter;
+    if (!opts.stacked) {
+        for (var i = 0; i < len; i++) {
+            for (var j = 0; j < (multi || 1); j++) {
+                var cover;
+                covers.push(cover = this.rect(Math.round(X), y + barvgutter, barwidth, height - barvgutter).attr(this.g.shim));
+                cover.bar = multi ? bars[j][i] : bars[i];
+                cover.value = cover.bar.value;
+                X += barwidth;
+            }
+            X += barhgutter;
+        }
+    }
+    chart.label = function (labels, isBottom) {
+        labels = labels || [];
+        this.labels = paper.set();
+        var L, l = -Infinity;
+        if (opts.stacked) {
+            for (var i = 0; i < len; i++) {
+                var tot = 0;
+                for (var j = 0; j < (multi || 1); j++) {
+                    tot += multi ? values[j][i] : values[i];
+                    if (j == multi - 1) {
+                        var label = paper.g.labelise(labels[i], tot, total);
+                        L = paper.g.text(bars[i * (multi || 1) + j].x, y + height - barvgutter / 2, label).insertBefore(covers[i * (multi || 1) + j]);
+                        var bb = L.getBBox();
+                        if (bb.x - 7 < l) {
+                            L.remove();
+                        } else {
+                            this.labels.push(L);
+                            l = bb.x + bb.width;
+                        }
+                    }
+                }
+            }
+        } else {
+            for (var i = 0; i < len; i++) {
+                for (var j = 0; j < (multi || 1); j++) {
+                    var label = paper.g.labelise(multi ? labels[j] && labels[j][i] : labels[i], multi ? values[j][i] : values[i], total);
+                    L = paper.g.text(bars[i * (multi || 1) + j].x, isBottom ? y + height - barvgutter / 2 : bars[i * (multi || 1) + j].y - 10, label).insertBefore(covers[i * (multi || 1) + j]);
+                    var bb = L.getBBox();
+                    if (bb.x - 7 < l) {
+                        L.remove();
+                    } else {
+                        this.labels.push(L);
+                        l = bb.x + bb.width;
+                    }
+                }
+            }
+        }
+        return this;
+    };
+    chart.hover = function (fin, fout) {
+        covers2.hide();
+        covers.show();
+        covers.mouseover(fin).mouseout(fout);
+        return this;
+    };
+    chart.hoverColumn = function (fin, fout) {
+        covers.hide();
+        covers2.show();
+        fout = fout || function () {};
+        covers2.mouseover(fin).mouseout(fout);
+        return this;
+    };
+    chart.click = function (f) {
+        covers2.hide();
+        covers.show();
+        covers.click(f);
+        return this;
+    };
+    chart.each = function (f) {
+        if (!Raphael.is(f, "function")) {
+            return this;
+        }
+        for (var i = covers.length; i--;) {
+            f.call(covers[i]);
+        }
+        return this;
+    };
+    chart.eachColumn = function (f) {
+        if (!Raphael.is(f, "function")) {
+            return this;
+        }
+        for (var i = covers2.length; i--;) {
+            f.call(covers2[i]);
+        }
+        return this;
+    };
+    chart.clickColumn = function (f) {
+        covers.hide();
+        covers2.show();
+        covers2.click(f);
+        return this;
+    };
+    chart.push(bars, covers, covers2);
+    chart.bars = bars;
+    chart.covers = covers;
+    return chart;
+};
+Raphael.fn.g.hbarchart = function (x, y, width, height, values, opts) {
+    opts = opts || {};
+    var type = {round: "round", sharp: "sharp", soft: "soft"}[opts.type] || "square",
+        gutter = parseFloat(opts.gutter || "20%"),
+        chart = this.set(),
+        bars = this.set(),
+        covers = this.set(),
+        covers2 = this.set(),
+        total = Math.max.apply(Math, values),
+        stacktotal = [],
+        paper = this,
+        multi = 0,
+        colors = opts.colors || this.g.colors,
+        len = values.length;
+    if (this.raphael.is(values[0], "array")) {
+        total = [];
+        multi = len;
+        len = 0;
+        for (var i = values.length; i--;) {
+            bars.push(this.set());
+            total.push(Math.max.apply(Math, values[i]));
+            len = Math.max(len, values[i].length);
+        }
+        if (opts.stacked) {
+            for (var i = len; i--;) {
+                var tot = 0;
+                for (var j = values.length; j--;) {
+                    tot +=+ values[j][i] || 0;
+                }
+                stacktotal.push(tot);
+            }
+        }
+        for (var i = values.length; i--;) {
+            if (values[i].length < len) {
+                for (var j = len; j--;) {
+                    values[i].push(0);
+                }
+            }
+        }
+        total = Math.max.apply(Math, opts.stacked ? stacktotal : total);
+    }
+    
+    total = (opts.to) || total;
+    var barheight = Math.floor(height / (len * (100 + gutter) + gutter) * 100),
+        bargutter = Math.floor(barheight * gutter / 100),
+        stack = [],
+        Y = y + bargutter,
+        X = (width - 1) / total;
+    !opts.stacked && (barheight /= multi || 1);
+    for (var i = 0; i < len; i++) {
+        stack = [];
+        for (var j = 0; j < (multi || 1); j++) {
+            var val = multi ? values[j][i] : values[i],
+                bar = this.g.finger(x, Y + barheight / 2, Math.round(val * X), barheight - 1, false, type).attr({stroke: colors[multi ? j : i], fill: colors[multi ? j : i]});
+            if (multi) {
+                bars[j].push(bar);
+            } else {
+                bars.push(bar);
+            }
+            bar.x = x + Math.round(val * X);
+            bar.y = Y + barheight / 2;
+            bar.w = Math.round(val * X);
+            bar.h = barheight;
+            bar.value = +val;
+            if (!opts.stacked) {
+                Y += barheight;
+            } else {
+                stack.push(bar);
+            }
+        }
+        if (opts.stacked) {
+            var cvr = this.rect(x, stack[0].y - stack[0].h / 2, width, barheight).attr(this.g.shim);
+            covers2.push(cvr);
+            cvr.bars = this.set();
+            var size = 0;
+            for (var s = stack.length; s--;) {
+                stack[s].toFront();
+            }
+            for (var s = 0, ss = stack.length; s < ss; s++) {
+                var bar = stack[s],
+                    cover,
+                    val = Math.round((size + bar.value) * X),
+                    path = this.g.finger(x, bar.y, val, barheight - 1, false, type, 1);
+                cvr.bars.push(bar);
+                size && bar.attr({path: path});
+                bar.w = val;
+                bar.x = x + val;
+                covers.push(cover = this.rect(x + size * X, bar.y - bar.h / 2, bar.value * X, barheight).attr(this.g.shim));
+                cover.bar = bar;
+                size += bar.value;
+            }
+            Y += barheight;
+        }
+        Y += bargutter;
+    }
+    covers2.toFront();
+    Y = y + bargutter;
+    if (!opts.stacked) {
+        for (var i = 0; i < len; i++) {
+            for (var j = 0; j < multi; j++) {
+                var cover = this.rect(x, Y, width, barheight).attr(this.g.shim);
+                covers.push(cover);
+                cover.bar = bars[j][i];
+                Y += barheight;
+            }
+            Y += bargutter;
+        }
+    }
+    chart.label = function (labels, isRight) {
+        labels = labels || [];
+        this.labels = paper.set();
+        for (var i = 0; i < len; i++) {
+            for (var j = 0; j < multi; j++) {
+                var  label = paper.g.labelise(multi ? labels[j] && labels[j][i] : labels[i], multi ? values[j][i] : values[i], total);
+                var X = isRight ? bars[i * (multi || 1) + j].x - barheight / 2 + 3 : x + 5,
+                    A = isRight ? "end" : "start",
+                    L;
+                this.labels.push(L = paper.g.text(X, bars[i * (multi || 1) + j].y, label).attr({"text-anchor": A}).insertBefore(covers[0]));
+                if (L.getBBox().x < x + 5) {
+                    L.attr({x: x + 5, "text-anchor": "start"});
+                } else {
+                    bars[i * (multi || 1) + j].label = L;
+                }
+            }
+        }
+        return this;
+    };
+    chart.hover = function (fin, fout) {
+        covers2.hide();
+        covers.show();
+        fout = fout || function () {};
+        covers.mouseover(fin).mouseout(fout);
+        return this;
+    };
+    chart.hoverColumn = function (fin, fout) {
+        covers.hide();
+        covers2.show();
+        fout = fout || function () {};
+        covers2.mouseover(fin).mouseout(fout);
+        return this;
+    };
+    chart.each = function (f) {
+        if (!Raphael.is(f, "function")) {
+            return this;
+        }
+        for (var i = covers.length; i--;) {
+            f.call(covers[i]);
+        }
+        return this;
+    };
+    chart.eachColumn = function (f) {
+        if (!Raphael.is(f, "function")) {
+            return this;
+        }
+        for (var i = covers2.length; i--;) {
+            f.call(covers2[i]);
+        }
+        return this;
+    };
+    chart.click = function (f) {
+        covers2.hide();
+        covers.show();
+        covers.click(f);
+        return this;
+    };
+    chart.clickColumn = function (f) {
+        covers.hide();
+        covers2.show();
+        covers2.click(f);
+        return this;
+    };
+    chart.push(bars, covers, covers2);
+    chart.bars = bars;
+    chart.covers = covers;
+    return chart;
+};

+ 110 - 0
src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/g.dot.js

@@ -0,0 +1,110 @@
+/*
+ * g.Raphael 0.4 - Charting library, based on Raphaël
+ *
+ * Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com)
+ * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
+ */
+Raphael.fn.g.dotchart = function (x, y, width, height, valuesx, valuesy, size, opts) {
+    function drawAxis(ax) {
+        +ax[0] && (ax[0] = paper.g.axis(x + gutter, y + gutter, width - 2 * gutter, minx, maxx, opts.axisxstep || Math.floor((width - 2 * gutter) / 20), 2, opts.axisxlabels || null, opts.axisxtype || "t"));
+        +ax[1] && (ax[1] = paper.g.axis(x + width - gutter, y + height - gutter, height - 2 * gutter, miny, maxy, opts.axisystep || Math.floor((height - 2 * gutter) / 20), 3, opts.axisylabels || null, opts.axisytype || "t"));
+        +ax[2] && (ax[2] = paper.g.axis(x + gutter, y + height - gutter + maxR, width - 2 * gutter, minx, maxx, opts.axisxstep || Math.floor((width - 2 * gutter) / 20), 0, opts.axisxlabels || null, opts.axisxtype || "t"));
+        +ax[3] && (ax[3] = paper.g.axis(x + gutter - maxR, y + height - gutter, height - 2 * gutter, miny, maxy, opts.axisystep || Math.floor((height - 2 * gutter) / 20), 1, opts.axisylabels || null, opts.axisytype || "t"));
+    }
+    opts = opts || {};
+    var xdim = this.g.snapEnds(Math.min.apply(Math, valuesx), Math.max.apply(Math, valuesx), valuesx.length - 1),
+        minx = xdim.from,
+        maxx = xdim.to,
+        gutter = opts.gutter || 10,
+        ydim = this.g.snapEnds(Math.min.apply(Math, valuesy), Math.max.apply(Math, valuesy), valuesy.length - 1),
+        miny = ydim.from,
+        maxy = ydim.to,
+        len = Math.max(valuesx.length, valuesy.length, size.length),
+        symbol = this.g.markers[opts.symbol] || "disc",
+        res = this.set(),
+        series = this.set(),
+        max = opts.max || 100,
+        top = Math.max.apply(Math, size),
+        R = [],
+        paper = this,
+        k = Math.sqrt(top / Math.PI) * 2 / max;
+
+    for (var i = 0; i < len; i++) {
+        R[i] = Math.min(Math.sqrt(size[i] / Math.PI) * 2 / k, max);
+    }
+    gutter = Math.max.apply(Math, R.concat(gutter));
+    var axis = this.set(),
+        maxR = Math.max.apply(Math, R);
+    if (opts.axis) {
+        var ax = (opts.axis + "").split(/[,\s]+/);
+        drawAxis(ax);
+        var g = [], b = [];
+        for (var i = 0, ii = ax.length; i < ii; i++) {
+            var bb = ax[i].all ? ax[i].all.getBBox()[["height", "width"][i % 2]] : 0;
+            g[i] = bb + gutter;
+            b[i] = bb;
+        }
+        gutter = Math.max.apply(Math, g.concat(gutter));
+        for (var i = 0, ii = ax.length; i < ii; i++) if (ax[i].all) {
+            ax[i].remove();
+            ax[i] = 1;
+        }
+        drawAxis(ax);
+        for (var i = 0, ii = ax.length; i < ii; i++) if (ax[i].all) {
+            axis.push(ax[i].all);
+        }
+        res.axis = axis;
+    }
+    var kx = (width - gutter * 2) / ((maxx - minx) || 1),
+        ky = (height - gutter * 2) / ((maxy - miny) || 1);
+    for (var i = 0, ii = valuesy.length; i < ii; i++) {
+        var sym = this.raphael.is(symbol, "array") ? symbol[i] : symbol,
+            X = x + gutter + (valuesx[i] - minx) * kx,
+            Y = y + height - gutter - (valuesy[i] - miny) * ky;
+        sym && R[i] && series.push(this.g[sym](X, Y, R[i]).attr({fill: opts.heat ? this.g.colorValue(R[i], maxR) : Raphael.fn.g.colors[0], "fill-opacity": opts.opacity ? R[i] / max : 1, stroke: "none"}));
+    }
+    var covers = this.set();
+    for (var i = 0, ii = valuesy.length; i < ii; i++) {
+        var X = x + gutter + (valuesx[i] - minx) * kx,
+            Y = y + height - gutter - (valuesy[i] - miny) * ky;
+        covers.push(this.circle(X, Y, maxR).attr(this.g.shim));
+        opts.href && opts.href[i] && covers[i].attr({href: opts.href[i]});
+        covers[i].r = +R[i].toFixed(3);
+        covers[i].x = +X.toFixed(3);
+        covers[i].y = +Y.toFixed(3);
+        covers[i].X = valuesx[i];
+        covers[i].Y = valuesy[i];
+        covers[i].value = size[i] || 0;
+        covers[i].dot = series[i];
+    }
+    res.covers = covers;
+    res.series = series;
+    res.push(series, axis, covers);
+    res.hover = function (fin, fout) {
+        covers.mouseover(fin).mouseout(fout);
+        return this;
+    };
+    res.click = function (f) {
+        covers.click(f);
+        return this;
+    };
+    res.each = function (f) {
+        if (!Raphael.is(f, "function")) {
+            return this;
+        }
+        for (var i = covers.length; i--;) {
+            f.call(covers[i]);
+        }
+        return this;
+    };
+    res.href = function (map) {
+        var cover;
+        for (var i = covers.length; i--;) {
+            cover = covers[i];
+            if (cover.X == map.x && cover.Y == map.y && cover.value == map.value) {
+                cover.attr({href: map.href});
+            }
+        }
+    };
+    return res;
+};

+ 230 - 0
src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/g.line.js

@@ -0,0 +1,230 @@
+/*
+ * g.Raphael 0.4 - Charting library, based on Raphaël
+ *
+ * Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com)
+ * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
+ */
+Raphael.fn.g.linechart = function (x, y, width, height, valuesx, valuesy, opts) {
+    function shrink(values, dim) {
+        var k = values.length / dim,
+            j = 0,
+            l = k,
+            sum = 0,
+            res = [];
+        while (j < values.length) {
+            l--;
+            if (l < 0) {
+                sum += values[j] * (1 + l);
+                res.push(sum / k);
+                sum = values[j++] * -l;
+                l += k;
+            } else {
+                sum += values[j++];
+            }
+        }
+        return res;
+    }
+    opts = opts || {};
+    if (!this.raphael.is(valuesx[0], "array")) {
+        valuesx = [valuesx];
+    }
+    if (!this.raphael.is(valuesy[0], "array")) {
+        valuesy = [valuesy];
+    }
+    var allx = Array.prototype.concat.apply([], valuesx),
+        ally = Array.prototype.concat.apply([], valuesy),
+        xdim = this.g.snapEnds(Math.min.apply(Math, allx), Math.max.apply(Math, allx), valuesx[0].length - 1),
+        minx = xdim.from,
+        maxx = xdim.to,
+        gutter = opts.gutter || 10,
+        kx = (width - gutter * 2) / (maxx - minx),
+        ydim = this.g.snapEnds(Math.min.apply(Math, ally), Math.max.apply(Math, ally), valuesy[0].length - 1),
+        miny = ydim.from,
+        maxy = ydim.to,
+        ky = (height - gutter * 2) / (maxy - miny),
+        len = Math.max(valuesx[0].length, valuesy[0].length),
+        symbol = opts.symbol || "",
+        colors = opts.colors || Raphael.fn.g.colors,
+        that = this,
+        columns = null,
+        dots = null,
+        chart = this.set(),
+        path = [];
+
+    for (var i = 0, ii = valuesy.length; i < ii; i++) {
+        len = Math.max(len, valuesy[i].length);
+    }
+    var shades = this.set();
+    for (var i = 0, ii = valuesy.length; i < ii; i++) {
+        if (opts.shade) {
+            shades.push(this.path().attr({stroke: "none", fill: colors[i], opacity: opts.nostroke ? 1 : .3}));
+        }
+        if (valuesy[i].length > width - 2 * gutter) {
+            valuesy[i] = shrink(valuesy[i], width - 2 * gutter);
+            len = width - 2 * gutter;
+        }
+        if (valuesx[i] && valuesx[i].length > width - 2 * gutter) {
+            valuesx[i] = shrink(valuesx[i], width - 2 * gutter);
+        }
+    }
+    var axis = this.set();
+    if (opts.axis) {
+        var ax = (opts.axis + "").split(/[,\s]+/);
+        +ax[0] && axis.push(this.g.axis(x + gutter, y + gutter, width - 2 * gutter, minx, maxx, opts.axisxstep || Math.floor((width - 2 * gutter) / 20), 2, opts.northlabels));
+        +ax[1] && axis.push(this.g.axis(x + width - gutter, y + height - gutter, height - 2 * gutter, miny, maxy, opts.axisystep || Math.floor((height - 2 * gutter) / 20), 3, opts.eastlabels));
+        +ax[2] && axis.push(this.g.axis(x + gutter, y + height - gutter, width - 2 * gutter, minx, maxx, opts.axisxstep || Math.floor((width - 2 * gutter) / 20), 0, opts.southlabels));
+        +ax[3] && axis.push(this.g.axis(x + gutter, y + height - gutter, height - 2 * gutter, miny, maxy, opts.axisystep || Math.floor((height - 2 * gutter) / 20), 1, opts.westlabels));
+    }
+    if (opts.northAxisLabel) {
+	this.g.text(x + gutter + width/2, gutter, opts.northAxisLabel);
+    }
+    if (opts.southAxisLabel) {
+	this.g.text(x + gutter + width/2, y + height + 20, opts.southAxisLabel);
+    }
+    if (opts.westAxisLabel) {
+	this.g.text(gutter, y + gutter + height/2, opts.westAxisLabel).attr({rotation: -90});
+    }
+    if (opts.eastAxisLabel) {
+	this.g.text(x + gutter + width + 20, y + gutter + height/2, opts.eastAxisLabel).attr({rotation: 90});
+    }
+
+    var lines = this.set(),
+        symbols = this.set(),
+        line;
+    for (var i = 0, ii = valuesy.length; i < ii; i++) {
+        if (!opts.nostroke) {
+            lines.push(line = this.path().attr({
+                stroke: colors[i],
+                "stroke-width": opts.width || 2,
+                "stroke-linejoin": "round",
+                "stroke-linecap": "round",
+                "stroke-dasharray": opts.dash || ""
+            }));
+        }
+        var sym = this.raphael.is(symbol, "array") ? symbol[i] : symbol,
+            symset = this.set();
+        path = [];
+        for (var j = 0, jj = valuesy[i].length; j < jj; j++) {
+            var X = x + gutter + ((valuesx[i] || valuesx[0])[j] - minx) * kx;
+            var Y = y + height - gutter - (valuesy[i][j] - miny) * ky;
+            (Raphael.is(sym, "array") ? sym[j] : sym) && symset.push(this.g[Raphael.fn.g.markers[this.raphael.is(sym, "array") ? sym[j] : sym]](X, Y, (opts.width || 2) * 3).attr({fill: colors[i], stroke: "none"}));
+            path = path.concat([j ? "L" : "M", X, Y]);
+        }
+        symbols.push(symset);
+        if (opts.shade) {
+            shades[i].attr({path: path.concat(["L", X, y + height - gutter, "L",  x + gutter + ((valuesx[i] || valuesx[0])[0] - minx) * kx, y + height - gutter, "z"]).join(",")});
+        }
+        !opts.nostroke && line.attr({path: path.join(",")});
+    }
+    function createColumns(f) {
+        // unite Xs together
+        var Xs = [];
+        for (var i = 0, ii = valuesx.length; i < ii; i++) {
+            Xs = Xs.concat(valuesx[i]);
+        }
+        Xs.sort();
+        // remove duplicates
+        var Xs2 = [],
+            xs = [];
+        for (var i = 0, ii = Xs.length; i < ii; i++) {
+            Xs[i] != Xs[i - 1] && Xs2.push(Xs[i]) && xs.push(x + gutter + (Xs[i] - minx) * kx);
+        }
+        Xs = Xs2;
+        ii = Xs.length;
+        var cvrs = f || that.set();
+        for (var i = 0; i < ii; i++) {
+            var X = xs[i] - (xs[i] - (xs[i - 1] || x)) / 2,
+                w = ((xs[i + 1] || x + width) - xs[i]) / 2 + (xs[i] - (xs[i - 1] || x)) / 2,
+                C;
+            f ? (C = {}) : cvrs.push(C = that.rect(X - 1, y, Math.max(w + 1, 1), height).attr({stroke: "none", fill: "#000", opacity: 0}));
+            C.values = [];
+            C.symbols = that.set();
+            C.y = [];
+            C.x = xs[i];
+            C.axis = Xs[i];
+            for (var j = 0, jj = valuesy.length; j < jj; j++) {
+                Xs2 = valuesx[j] || valuesx[0];
+                for (var k = 0, kk = Xs2.length; k < kk; k++) {
+                    if (Xs2[k] == Xs[i]) {
+                        C.values.push(valuesy[j][k]);
+                        C.y.push(y + height - gutter - (valuesy[j][k] - miny) * ky);
+                        C.symbols.push(chart.symbols[j][k]);
+                    }
+                }
+            }
+            f && f.call(C);
+        }
+        !f && (columns = cvrs);
+    }
+    function createDots(f) {
+        var cvrs = f || that.set(),
+            C;
+        for (var i = 0, ii = valuesy.length; i < ii; i++) {
+            for (var j = 0, jj = valuesy[i].length; j < jj; j++) {
+                var X = x + gutter + ((valuesx[i] || valuesx[0])[j] - minx) * kx,
+                    nearX = x + gutter + ((valuesx[i] || valuesx[0])[j ? j - 1 : 1] - minx) * kx,
+                    Y = y + height - gutter - (valuesy[i][j] - miny) * ky;
+                f ? (C = {}) : cvrs.push(C = that.circle(X, Y, Math.abs(nearX - X) / 2).attr({stroke: "none", fill: "#000", opacity: 0}));
+                C.x = X;
+                C.y = Y;
+                C.value = valuesy[i][j];
+                C.line = chart.lines[i];
+                C.shade = chart.shades[i];
+                C.symbol = chart.symbols[i][j];
+                C.symbols = chart.symbols[i];
+                C.axis = (valuesx[i] || valuesx[0])[j];
+                f && f.call(C);
+            }
+        }
+        !f && (dots = cvrs);
+    }
+    chart.push(lines, shades, symbols, axis, columns, dots);
+    chart.lines = lines;
+    chart.shades = shades;
+    chart.symbols = symbols;
+    chart.axis = axis;
+    chart.hoverColumn = function (fin, fout) {
+        !columns && createColumns();
+        columns.mouseover(fin).mouseout(fout);
+        return this;
+    };
+    chart.clickColumn = function (f) {
+        !columns && createColumns();
+        columns.click(f);
+        return this;
+    };
+    chart.hrefColumn = function (cols) {
+        var hrefs = that.raphael.is(arguments[0], "array") ? arguments[0] : arguments;
+        if (!(arguments.length - 1) && typeof cols == "object") {
+            for (var x in cols) {
+                for (var i = 0, ii = columns.length; i < ii; i++) if (columns[i].axis == x) {
+                    columns[i].attr("href", cols[x]);
+                }
+            }
+        }
+        !columns && createColumns();
+        for (var i = 0, ii = hrefs.length; i < ii; i++) {
+            columns[i] && columns[i].attr("href", hrefs[i]);
+        }
+        return this;
+    };
+    chart.hover = function (fin, fout) {
+        !dots && createDots();
+        dots.mouseover(fin).mouseout(fout);
+        return this;
+    };
+    chart.click = function (f) {
+        !dots && createDots();
+        dots.click(f);
+        return this;
+    };
+    chart.each = function (f) {
+        createDots(f);
+        return this;
+    };
+    chart.eachColumn = function (f) {
+        createColumns(f);
+        return this;
+    };
+    return chart;
+};

+ 205 - 0
src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/g.pie.js

@@ -0,0 +1,205 @@
+/*
+ * g.Raphael 0.4 - Charting library, based on Raphaël
+ *
+ * Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com)
+ * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
+ */
+Raphael.fn.g.piechart = function (cx, cy, r, values, opts) {
+    opts = opts || {};
+    var paper = this,
+        sectors = [],
+        covers = this.set(),
+        chart = this.set(),
+        series = this.set(),
+        order = [],
+        len = values.length,
+        angle = 0,
+        total = 0,
+        others = 0,
+        cut = 9,
+        defcut = true;
+    chart.covers = covers;
+    if (len == 1) {
+        series.push(this.circle(cx, cy, r).attr({fill: this.g.colors[0], stroke: opt.stroke || "#fff", "stroke-width": opts.strokewidth == null ? 1 : opts.strokewidth}));
+        covers.push(this.circle(cx, cy, r).attr(this.g.shim));
+        total = values[0];
+        values[0] = {value: values[0], order: 0, valueOf: function () { return this.value; }};
+        series[0].middle = {x: cx, y: cy};
+        series[0].mangle = 180;
+    } else {
+        function sector(cx, cy, r, startAngle, endAngle, fill) {
+            var rad = Math.PI / 180,
+                x1 = cx + r * Math.cos(-startAngle * rad),
+                x2 = cx + r * Math.cos(-endAngle * rad),
+                xm = cx + r / 2 * Math.cos(-(startAngle + (endAngle - startAngle) / 2) * rad),
+                y1 = cy + r * Math.sin(-startAngle * rad),
+                y2 = cy + r * Math.sin(-endAngle * rad),
+                ym = cy + r / 2 * Math.sin(-(startAngle + (endAngle - startAngle) / 2) * rad),
+                res = ["M", cx, cy, "L", x1, y1, "A", r, r, 0, +(Math.abs(endAngle - startAngle) > 180), 1, x2, y2, "z"];
+            res.middle = {x: xm, y: ym};
+            return res;
+        }
+        for (var i = 0; i < len; i++) {
+            total += values[i];
+            values[i] = {value: values[i], order: i, valueOf: function () { return this.value; }};
+        }
+        values.sort(function (a, b) {
+            return b.value - a.value;
+        });
+        for (var i = 0; i < len; i++) {
+            if (defcut && values[i] * 360 / total <= 1.5) {
+                cut = i;
+                defcut = false;
+            }
+            if (i > cut) {
+                defcut = false;
+                values[cut].value += values[i];
+                values[cut].others = true;
+                others = values[cut].value;
+            }
+        }
+        len = Math.min(cut + 1, values.length);
+        others && values.splice(len) && (values[cut].others = true);
+        for (var i = 0; i < len; i++) {
+            var mangle = angle - 360 * values[i] / total / 2;
+            if (!i) {
+                angle = 90 - mangle;
+                mangle = angle - 360 * values[i] / total / 2;
+            }
+            if (opts.init) {
+                var ipath = sector(cx, cy, 1, angle, angle - 360 * values[i] / total).join(",");
+            }
+            var path = sector(cx, cy, r, angle, angle -= 360 * values[i] / total);
+            var p = this.path(opts.init ? ipath : path).attr({fill: opts.colors && opts.colors[i] || this.g.colors[i] || "#666", stroke: opts.stroke || "#fff", "stroke-width": (opts.strokewidth == null ? 1 : opts.strokewidth), "stroke-linejoin": "round"});
+            p.value = values[i];
+            p.middle = path.middle;
+            p.mangle = mangle;
+            sectors.push(p);
+            series.push(p);
+            opts.init && p.animate({path: path.join(",")}, (+opts.init - 1) || 1000, ">");
+        }
+        for (var i = 0; i < len; i++) {
+            var p = paper.path(sectors[i].attr("path")).attr(this.g.shim);
+            opts.href && opts.href[i] && p.attr({href: opts.href[i]});
+            p.attr = function () {};
+            covers.push(p);
+            series.push(p);
+        }
+    }
+
+    chart.hover = function (fin, fout) {
+        fout = fout || function () {};
+        var that = this;
+        for (var i = 0; i < len; i++) {
+            (function (sector, cover, j) {
+                var o = {
+                    sector: sector,
+                    cover: cover,
+                    cx: cx,
+                    cy: cy,
+                    mx: sector.middle.x,
+                    my: sector.middle.y,
+                    mangle: sector.mangle,
+                    r: r,
+                    value: values[j],
+                    total: total,
+                    label: that.labels && that.labels[j]
+                };
+                cover.mouseover(function () {
+                    fin.call(o);
+                }).mouseout(function () {
+                    fout.call(o);
+                });
+            })(series[i], covers[i], i);
+        }
+        return this;
+    };
+    // x: where label could be put
+    // y: where label could be put
+    // value: value to show
+    // total: total number to count %
+    chart.each = function (f) {
+        var that = this;
+        for (var i = 0; i < len; i++) {
+            (function (sector, cover, j) {
+                var o = {
+                    sector: sector,
+                    cover: cover,
+                    cx: cx,
+                    cy: cy,
+                    x: sector.middle.x,
+                    y: sector.middle.y,
+                    mangle: sector.mangle,
+                    r: r,
+                    value: values[j],
+                    total: total,
+                    label: that.labels && that.labels[j]
+                };
+                f.call(o);
+            })(series[i], covers[i], i);
+        }
+        return this;
+    };
+    chart.click = function (f) {
+        var that = this;
+        for (var i = 0; i < len; i++) {
+            (function (sector, cover, j) {
+                var o = {
+                    sector: sector,
+                    cover: cover,
+                    cx: cx,
+                    cy: cy,
+                    mx: sector.middle.x,
+                    my: sector.middle.y,
+                    mangle: sector.mangle,
+                    r: r,
+                    value: values[j],
+                    total: total,
+                    label: that.labels && that.labels[j]
+                };
+                cover.click(function () { f.call(o); });
+            })(series[i], covers[i], i);
+        }
+        return this;
+    };
+    chart.inject = function (element) {
+        element.insertBefore(covers[0]);
+    };
+    var legend = function (labels, otherslabel, mark, dir) {
+        var x = cx + r + r / 5,
+            y = cy,
+            h = y + 10;
+        labels = labels || [];
+        dir = (dir && dir.toLowerCase && dir.toLowerCase()) || "east";
+        mark = paper.g.markers[mark && mark.toLowerCase()] || "disc";
+        chart.labels = paper.set();
+        for (var i = 0; i < len; i++) {
+            var clr = series[i].attr("fill"),
+                j = values[i].order,
+                txt;
+            values[i].others && (labels[j] = otherslabel || "Others");
+            labels[j] = paper.g.labelise(labels[j], values[i], total);
+            chart.labels.push(paper.set());
+            chart.labels[i].push(paper.g[mark](x + 5, h, 5).attr({fill: clr, stroke: "none"}));
+            chart.labels[i].push(txt = paper.text(x + 20, h, labels[j] || values[j]).attr(paper.g.txtattr).attr({fill: opts.legendcolor || "#000", "text-anchor": "start"}));
+            covers[i].label = chart.labels[i];
+            h += txt.getBBox().height * 1.2;
+        }
+        var bb = chart.labels.getBBox(),
+            tr = {
+                east: [0, -bb.height / 2],
+                west: [-bb.width - 2 * r - 20, -bb.height / 2],
+                north: [-r - bb.width / 2, -r - bb.height - 10],
+                south: [-r - bb.width / 2, r + 10]
+            }[dir];
+        chart.labels.translate.apply(chart.labels, tr);
+        chart.push(chart.labels);
+    };
+    if (opts.legend) {
+        legend(opts.legend, opts.legendothers, opts.legendmark, opts.legendpos);
+    }
+    chart.push(series, covers);
+    chart.series = series;
+    chart.covers = covers;
+    return chart;
+};

+ 481 - 0
src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/g.raphael.js

@@ -0,0 +1,481 @@
+/*
+ * g.Raphael 0.4 - Charting library, based on Raphaël
+ *
+ * Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com)
+ * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
+ */
+ 
+ 
+(function () {
+    Raphael.fn.g = Raphael.fn.g || {};
+    Raphael.fn.g.markers = {
+        disc: "disc",
+        o: "disc",
+        flower: "flower",
+        f: "flower",
+        diamond: "diamond",
+        d: "diamond",
+        square: "square",
+        s: "square",
+        triangle: "triangle",
+        t: "triangle",
+        star: "star",
+        "*": "star",
+        cross: "cross",
+        x: "cross",
+        plus: "plus",
+        "+": "plus",
+        arrow: "arrow",
+        "->": "arrow"
+    };
+    Raphael.fn.g.shim = {stroke: "none", fill: "#000", "fill-opacity": 0};
+    Raphael.fn.g.txtattr = {font: "12px Arial, sans-serif"};
+    Raphael.fn.g.colors = [];
+    var hues = [.6, .2, .05, .1333, .75, 0];
+    for (var i = 0; i < 10; i++) {
+        if (i < hues.length) {
+            Raphael.fn.g.colors.push("hsb(" + hues[i] + ", .75, .75)");
+        } else {
+            Raphael.fn.g.colors.push("hsb(" + hues[i - hues.length] + ", 1, .5)");
+        }
+    }
+    Raphael.fn.g.text = function (x, y, text) {
+        return this.text(x, y, text).attr(this.g.txtattr);
+    };
+    Raphael.fn.g.labelise = function (label, val, total) {
+        if (label) {
+            return (label + "").replace(/(##+(?:\.#+)?)|(%%+(?:\.%+)?)/g, function (all, value, percent) {
+                if (value) {
+                    return (+val).toFixed(value.replace(/^#+\.?/g, "").length);
+                }
+                if (percent) {
+                    return (val * 100 / total).toFixed(percent.replace(/^%+\.?/g, "").length) + "%";
+                }
+            });
+        } else {
+            return (+val).toFixed(0);
+        }
+    };
+
+    Raphael.fn.g.finger = function (x, y, width, height, dir, ending, isPath) {
+        // dir 0 for horisontal and 1 for vertical
+        if ((dir && !height) || (!dir && !width)) {
+            return isPath ? "" : this.path();
+        }
+        ending = {square: "square", sharp: "sharp", soft: "soft"}[ending] || "round";
+        var path;
+        height = Math.round(height);
+        width = Math.round(width);
+        x = Math.round(x);
+        y = Math.round(y);
+        switch (ending) {
+            case "round":
+            if (!dir) {
+                var r = Math.floor(height / 2);
+                if (width < r) {
+                    r = width;
+                    path = ["M", x + .5, y + .5 - Math.floor(height / 2), "l", 0, 0, "a", r, Math.floor(height / 2), 0, 0, 1, 0, height, "l", 0, 0, "z"];
+                } else {
+                    path = ["M", x + .5, y + .5 - r, "l", width - r, 0, "a", r, r, 0, 1, 1, 0, height, "l", r - width, 0, "z"];
+                }
+            } else {
+                var r = Math.floor(width / 2);
+                if (height < r) {
+                    r = height;
+                    path = ["M", x - Math.floor(width / 2), y, "l", 0, 0, "a", Math.floor(width / 2), r, 0, 0, 1, width, 0, "l", 0, 0, "z"];
+                } else {
+                    path = ["M", x - r, y, "l", 0, r - height, "a", r, r, 0, 1, 1, width, 0, "l", 0, height - r, "z"];
+                }
+            }
+            break;
+            case "sharp":
+            if (!dir) {
+                var half = Math.floor(height / 2);
+                path = ["M", x, y + half, "l", 0, -height, Math.max(width - half, 0), 0, Math.min(half, width), half, -Math.min(half, width), half + (half * 2 < height), "z"];
+            } else {
+                var half = Math.floor(width / 2);
+                path = ["M", x + half, y, "l", -width, 0, 0, -Math.max(height - half, 0), half, -Math.min(half, height), half, Math.min(half, height), half, "z"];
+            }
+            break;
+            case "square":
+            if (!dir) {
+                path = ["M", x, y + Math.floor(height / 2), "l", 0, -height, width, 0, 0, height, "z"];
+            } else {
+                path = ["M", x + Math.floor(width / 2), y, "l", 1 - width, 0, 0, -height, width - 1, 0, "z"];
+            }
+            break;
+            case "soft":
+            var r;
+            if (!dir) {
+                r = Math.min(width, Math.round(height / 5));
+                path = ["M", x + .5, y + .5 - Math.floor(height / 2), "l", width - r, 0, "a", r, r, 0, 0, 1, r, r, "l", 0, height - r * 2, "a", r, r, 0, 0, 1, -r, r, "l", r - width, 0, "z"];
+            } else {
+                r = Math.min(Math.round(width / 5), height);
+                path = ["M", x - Math.floor(width / 2), y, "l", 0, r - height, "a", r, r, 0, 0, 1, r, -r, "l", width - 2 * r, 0, "a", r, r, 0, 0, 1, r, r, "l", 0, height - r, "z"];
+            }
+        }
+        if (isPath) {
+            return path.join(",");
+        } else {
+            return this.path(path);
+        }
+    };
+
+    // Symbols
+    Raphael.fn.g.disc = function (cx, cy, r) {
+        return this.circle(cx, cy, r);
+    };
+    Raphael.fn.g.line = function (cx, cy, r) {
+        return this.rect(cx - r, cy - r / 5, 2 * r, 2 * r / 5);
+    };
+    Raphael.fn.g.square = function (cx, cy, r) {
+        r = r * .7;
+        return this.rect(cx - r, cy - r, 2 * r, 2 * r);
+    };
+    Raphael.fn.g.triangle = function (cx, cy, r) {
+        r *= 1.75;
+        return this.path("M".concat(cx, ",", cy, "m0-", r * .58, "l", r * .5, ",", r * .87, "-", r, ",0z"));
+    };
+    Raphael.fn.g.diamond = function (cx, cy, r) {
+        return this.path(["M", cx, cy - r, "l", r, r, -r, r, -r, -r, r, -r, "z"]);
+    };
+    Raphael.fn.g.flower = function (cx, cy, r, n) {
+        r = r * 1.25;
+        var rout = r,
+            rin = rout * .5;
+        n = +n < 3 || !n ? 5 : n;
+        var points = ["M", cx, cy + rin, "Q"],
+            R;
+        for (var i = 1; i < n * 2 + 1; i++) {
+            R = i % 2 ? rout : rin;
+            points = points.concat([+(cx + R * Math.sin(i * Math.PI / n)).toFixed(3), +(cy + R * Math.cos(i * Math.PI / n)).toFixed(3)]);
+        }
+        points.push("z");
+        return this.path(points.join(","));
+    };
+    Raphael.fn.g.star = function (cx, cy, r, r2) {
+        r2 = r2 || r * .5;
+        var points = ["M", cx, cy + r2, "L"],
+            R;
+        for (var i = 1; i < 10; i++) {
+            R = i % 2 ? r : r2;
+            points = points.concat([(cx + R * Math.sin(i * Math.PI * .2)).toFixed(3), (cy + R * Math.cos(i * Math.PI * .2)).toFixed(3)]);
+        }
+        points.push("z");
+        return this.path(points.join(","));
+    };
+    Raphael.fn.g.cross = function (cx, cy, r) {
+        r = r / 2.5;
+        return this.path("M".concat(cx - r, ",", cy, "l", [-r, -r, r, -r, r, r, r, -r, r, r, -r, r, r, r, -r, r, -r, -r, -r, r, -r, -r, "z"]));
+    };
+    Raphael.fn.g.plus = function (cx, cy, r) {
+        r = r / 2;
+        return this.path("M".concat(cx - r / 2, ",", cy - r / 2, "l", [0, -r, r, 0, 0, r, r, 0, 0, r, -r, 0, 0, r, -r, 0, 0, -r, -r, 0, 0, -r, "z"]));
+    };
+    Raphael.fn.g.arrow = function (cx, cy, r) {
+        return this.path("M".concat(cx - r * .7, ",", cy - r * .4, "l", [r * .6, 0, 0, -r * .4, r, r * .8, -r, r * .8, 0, -r * .4, -r * .6, 0], "z"));
+    };
+
+    // Tooltips
+    Raphael.fn.g.tag = function (x, y, text, angle, r) {
+        angle = angle || 0;
+        r = r == null ? 5 : r;
+        text = text == null ? "$9.99" : text;
+        var R = .5522 * r,
+            res = this.set(),
+            d = 3;
+        res.push(this.path().attr({fill: "#000", stroke: "none"}));
+        res.push(this.text(x, y, text).attr(this.g.txtattr).attr({fill: "#fff"}));
+        res.update = function () {
+            this.rotate(0, x, y);
+            var bb = this[1].getBBox();
+            if (bb.height >= r * 2) {
+                this[0].attr({path: ["M", x, y + r, "a", r, r, 0, 1, 1, 0, -r * 2, r, r, 0, 1, 1, 0, r * 2, "m", 0, -r * 2 -d, "a", r + d, r + d, 0, 1, 0, 0, (r + d) * 2, "L", x + r + d, y + bb.height / 2 + d, "l", bb.width + 2 * d, 0, 0, -bb.height - 2 * d, -bb.width - 2 * d, 0, "L", x, y - r - d].join(",")});
+            } else {
+                var dx = Math.sqrt(Math.pow(r + d, 2) - Math.pow(bb.height / 2 + d, 2));
+                // ["c", -R, 0, -r, R - r, -r, -r, 0, -R, r - R, -r, r, -r, R, 0, r, r - R, r, r, 0, R, R - r, r, -r, r]
+                // "a", r, r, 0, 1, 1, 0, -r * 2, r, r, 0, 1, 1, 0, r * 2,
+                this[0].attr({path: ["M", x, y + r, "c", -R, 0, -r, R - r, -r, -r, 0, -R, r - R, -r, r, -r, R, 0, r, r - R, r, r, 0, R, R - r, r, -r, r, "M", x + dx, y - bb.height / 2 - d, "a", r + d, r + d, 0, 1, 0, 0, bb.height + 2 * d, "l", r + d - dx + bb.width + 2 * d, 0, 0, -bb.height - 2 * d, "L", x + dx, y - bb.height / 2 - d].join(",")});
+            }
+            this[1].attr({x: x + r + d + bb.width / 2, y: y});
+            angle = (360 - angle) % 360;
+            this.rotate(angle, x, y);
+            angle > 90 && angle < 270 && this[1].attr({x: x - r - d - bb.width / 2, y: y, rotation: [180 + angle, x, y]});
+            return this;
+        };
+        res.update();
+        return res;
+    };
+    Raphael.fn.g.popupit = function (x, y, set, dir, size) {
+        dir = dir == null ? 2 : dir;
+        size = size || 5;
+        x = Math.round(x) + .5;
+        y = Math.round(y) + .5;
+        var bb = set.getBBox(),
+            w = Math.round(bb.width / 2),
+            h = Math.round(bb.height / 2),
+            dx = [0, w + size * 2, 0, -w - size * 2],
+            dy = [-h * 2 - size * 3, -h - size, 0, -h - size],
+            p = ["M", x - dx[dir], y - dy[dir], "l", -size, (dir == 2) * -size, -Math.max(w - size, 0), 0, "a", size, size, 0, 0, 1, -size, -size,
+                "l", 0, -Math.max(h - size, 0), (dir == 3) * -size, -size, (dir == 3) * size, -size, 0, -Math.max(h - size, 0), "a", size, size, 0, 0, 1, size, -size,
+                "l", Math.max(w - size, 0), 0, size, !dir * -size, size, !dir * size, Math.max(w - size, 0), 0, "a", size, size, 0, 0, 1, size, size,
+                "l", 0, Math.max(h - size, 0), (dir == 1) * size, size, (dir == 1) * -size, size, 0, Math.max(h - size, 0), "a", size, size, 0, 0, 1, -size, size,
+                "l", -Math.max(w - size, 0), 0, "z"].join(","),
+            xy = [{x: x, y: y + size * 2 + h}, {x: x - size * 2 - w, y: y}, {x: x, y: y - size * 2 - h}, {x: x + size * 2 + w, y: y}][dir];
+        set.translate(xy.x - w - bb.x, xy.y - h - bb.y);
+        return this.path(p).attr({fill: "#000", stroke: "none"}).insertBefore(set.node ? set : set[0]);
+    };
+    Raphael.fn.g.popup = function (x, y, text, dir, size) {
+        dir = dir == null ? 2 : dir;
+        size = size || 5;
+        text = text || "$9.99";
+        var res = this.set(),
+            d = 3;
+        res.push(this.path().attr({fill: "#000", stroke: "none"}));
+        res.push(this.text(x, y, text).attr(this.g.txtattr).attr({fill: "#fff"}));
+        res.update = function (X, Y, withAnimation) {
+            X = X || x;
+            Y = Y || y;
+            var bb = this[1].getBBox(),
+                w = bb.width / 2,
+                h = bb.height / 2,
+                dx = [0, w + size * 2, 0, -w - size * 2],
+                dy = [-h * 2 - size * 3, -h - size, 0, -h - size],
+                p = ["M", X - dx[dir], Y - dy[dir], "l", -size, (dir == 2) * -size, -Math.max(w - size, 0), 0, "a", size, size, 0, 0, 1, -size, -size,
+                    "l", 0, -Math.max(h - size, 0), (dir == 3) * -size, -size, (dir == 3) * size, -size, 0, -Math.max(h - size, 0), "a", size, size, 0, 0, 1, size, -size,
+                    "l", Math.max(w - size, 0), 0, size, !dir * -size, size, !dir * size, Math.max(w - size, 0), 0, "a", size, size, 0, 0, 1, size, size,
+                    "l", 0, Math.max(h - size, 0), (dir == 1) * size, size, (dir == 1) * -size, size, 0, Math.max(h - size, 0), "a", size, size, 0, 0, 1, -size, size,
+                    "l", -Math.max(w - size, 0), 0, "z"].join(","),
+                xy = [{x: X, y: Y + size * 2 + h}, {x: X - size * 2 - w, y: Y}, {x: X, y: Y - size * 2 - h}, {x: X + size * 2 + w, y: Y}][dir];
+            if (withAnimation) {
+                this[0].animate({path: p}, 500, ">");
+                this[1].animate(xy, 500, ">");
+            } else {
+                this[0].attr({path: p});
+                this[1].attr(xy);
+            }
+            return this;
+        };
+        return res.update(x, y);
+    };
+    Raphael.fn.g.flag = function (x, y, text, angle) {
+        angle = angle || 0;
+        text = text || "$9.99";
+        var res = this.set(),
+            d = 3;
+        res.push(this.path().attr({fill: "#000", stroke: "none"}));
+        res.push(this.text(x, y, text).attr(this.g.txtattr).attr({fill: "#fff"}));
+        res.update = function (x, y) {
+            this.rotate(0, x, y);
+            var bb = this[1].getBBox(),
+                h = bb.height / 2;
+            this[0].attr({path: ["M", x, y, "l", h + d, -h - d, bb.width + 2 * d, 0, 0, bb.height + 2 * d, -bb.width - 2 * d, 0, "z"].join(",")});
+            this[1].attr({x: x + h + d + bb.width / 2, y: y});
+            angle = 360 - angle;
+            this.rotate(angle, x, y);
+            angle > 90 && angle < 270 && this[1].attr({x: x - r - d - bb.width / 2, y: y, rotation: [180 + angle, x, y]});
+            return this;
+        };
+        return res.update(x, y);
+    };
+    Raphael.fn.g.label = function (x, y, text) {
+        var res = this.set();
+        res.push(this.rect(x, y, 10, 10).attr({stroke: "none", fill: "#000"}));
+        res.push(this.text(x, y, text).attr(this.g.txtattr).attr({fill: "#fff"}));
+        res.update = function () {
+            var bb = this[1].getBBox(),
+                r = Math.min(bb.width + 10, bb.height + 10) / 2;
+            this[0].attr({x: bb.x - r / 2, y: bb.y - r / 2, width: bb.width + r, height: bb.height + r, r: r});
+        };
+        res.update();
+        return res;
+    };
+    Raphael.fn.g.labelit = function (set) {
+        var bb = set.getBBox(),
+            r = Math.min(20, bb.width + 10, bb.height + 10) / 2;
+        return this.rect(bb.x - r / 2, bb.y - r / 2, bb.width + r, bb.height + r, r).attr({stroke: "none", fill: "#000"}).insertBefore(set[0]);
+    };
+    Raphael.fn.g.drop = function (x, y, text, size, angle) {
+        size = size || 30;
+        angle = angle || 0;
+        var res = this.set();
+        res.push(this.path(["M", x, y, "l", size, 0, "A", size * .4, size * .4, 0, 1, 0, x + size * .7, y - size * .7, "z"]).attr({fill: "#000", stroke: "none", rotation: [22.5 - angle, x, y]}));
+        angle = (angle + 90) * Math.PI / 180;
+        res.push(this.text(x + size * Math.sin(angle), y + size * Math.cos(angle), text).attr(this.g.txtattr).attr({"font-size": size * 12 / 30, fill: "#fff"}));
+        res.drop = res[0];
+        res.text = res[1];
+        return res;
+    };
+    Raphael.fn.g.blob = function (x, y, text, angle, size) {
+        angle = (+angle + 1 ? angle : 45) + 90;
+        size = size || 12;
+        var rad = Math.PI / 180,
+            fontSize = size * 12 / 12;
+        var res = this.set();
+        res.push(this.path().attr({fill: "#000", stroke: "none"}));
+        res.push(this.text(x + size * Math.sin((angle) * rad), y + size * Math.cos((angle) * rad) - fontSize / 2, text).attr(this.g.txtattr).attr({"font-size": fontSize, fill: "#fff"}));
+        res.update = function (X, Y, withAnimation) {
+            X = X || x;
+            Y = Y || y;
+            var bb = this[1].getBBox(),
+                w = Math.max(bb.width + fontSize, size * 25 / 12),
+                h = Math.max(bb.height + fontSize, size * 25 / 12),
+                x2 = X + size * Math.sin((angle - 22.5) * rad),
+                y2 = Y + size * Math.cos((angle - 22.5) * rad),
+                x1 = X + size * Math.sin((angle + 22.5) * rad),
+                y1 = Y + size * Math.cos((angle + 22.5) * rad),
+                dx = (x1 - x2) / 2,
+                dy = (y1 - y2) / 2,
+                rx = w / 2,
+                ry = h / 2,
+                k = -Math.sqrt(Math.abs(rx * rx * ry * ry - rx * rx * dy * dy - ry * ry * dx * dx) / (rx * rx * dy * dy + ry * ry * dx * dx)),
+                cx = k * rx * dy / ry + (x1 + x2) / 2,
+                cy = k * -ry * dx / rx + (y1 + y2) / 2;
+            if (withAnimation) {
+                this.animate({x: cx, y: cy, path: ["M", x, y, "L", x1, y1, "A", rx, ry, 0, 1, 1, x2, y2, "z"].join(",")}, 500, ">");
+            } else {
+                this.attr({x: cx, y: cy, path: ["M", x, y, "L", x1, y1, "A", rx, ry, 0, 1, 1, x2, y2, "z"].join(",")});
+            }
+            return this;
+        };
+        res.update(x, y);
+        return res;
+    };
+
+    Raphael.fn.g.colorValue = function (value, total, s, b) {
+        return "hsb(" + [Math.min((1 - value / total) * .4, 1), s || .75, b || .75] + ")";
+    };
+
+    Raphael.fn.g.snapEnds = function (from, to, steps) {
+        var f = from,
+            t = to;
+        if (f == t) {
+            return {from: f, to: t, power: 0};
+        }
+        function round(a) {
+            return Math.abs(a - .5) < .25 ? Math.floor(a) + .5 : Math.round(a);
+        }
+        var d = (t - f) / steps,
+            r = Math.floor(d),
+            R = r,
+            i = 0;
+        if (r) {
+            while (R) {
+                i--;
+                R = Math.floor(d * Math.pow(10, i)) / Math.pow(10, i);
+            }
+            i ++;
+        } else {
+            while (!r) {
+                i = i || 1;
+                r = Math.floor(d * Math.pow(10, i)) / Math.pow(10, i);
+                i++;
+            }
+            i && i--;
+        }
+        var t = round(to * Math.pow(10, i)) / Math.pow(10, i);
+        if (t < to) {
+            t = round((to + .5) * Math.pow(10, i)) / Math.pow(10, i);
+        }
+        var f = round((from - (i > 0 ? 0 : .5)) * Math.pow(10, i)) / Math.pow(10, i);
+        return {from: f, to: t, power: i};
+    };
+    Raphael.fn.g.axis = function (x, y, length, from, to, steps, orientation, labels, type, dashsize) {
+        dashsize = dashsize == null ? 3 : dashsize;
+        type = type || "t";
+        steps = steps || 10;
+        var path = type == "|" || type == " " ? ["M", x + .5, y, "l", 0, .001] : orientation == 1 || orientation == 3 ? ["M", x + .5, y, "l", 0, -length] : ["M", x, y + .5, "l", length, 0],
+            ends = this.g.snapEnds(from, to, steps),
+            f = ends.from,
+            t = ends.to,
+            i = ends.power,
+            j = 0,
+            text = this.set();
+        d = (t - f) / steps;
+        var label = f,
+            rnd = i > 0 ? i : 0;
+            dx = length / steps;
+        if (+orientation == 1 || +orientation == 3) {
+            var Y = y,
+                addon = (orientation - 1 ? 1 : -1) * (dashsize + 3 + !!(orientation - 1));
+            while (Y >= y - length) {
+                type != "-" && type != " " && (path = path.concat(["M", x - (type == "+" || type == "|" ? dashsize : !(orientation - 1) * dashsize * 2), Y + .5, "l", dashsize * 2 + 1, 0]));
+                text.push(this.text(x + addon, Y, (labels && labels[j++]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(this.g.txtattr).attr({"text-anchor": orientation - 1 ? "start" : "end"}));
+                label += d;
+                Y -= dx;
+            }
+            if (Math.round(Y + dx - (y - length))) {
+                type != "-" && type != " " && (path = path.concat(["M", x - (type == "+" || type == "|" ? dashsize : !(orientation - 1) * dashsize * 2), y - length + .5, "l", dashsize * 2 + 1, 0]));
+                text.push(this.text(x + addon, y - length, (labels && labels[j]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(this.g.txtattr).attr({"text-anchor": orientation - 1 ? "start" : "end"}));
+            }
+        } else {
+            var X = x,
+                label = f,
+                rnd = i > 0 ? i : 0,
+                addon = (orientation ? -1 : 1) * (dashsize + 9 + !orientation),
+                dx = length / steps,
+                txt = 0,
+                prev = 0;
+            while (X <= x + length) {
+
+                text.push(txt = this.text(X, y + addon, (labels && labels[j++]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(this.g.txtattr));
+		var bb = txt.getBBox();
+		var ds = dashsize;
+                if (prev >= bb.x - 5) {
+		    text.pop(text.length - 1).remove();
+		    ds = 1;
+                } else {
+                    prev = bb.x + bb.width;
+                }
+
+		type != "-" && type != " " && (path = path.concat(["M", X + .5, y - (type == "+" ? ds : !!orientation * ds * 2), "l", 0, ds * 2 + 1]));
+                
+                label += d;
+                X += dx;
+            }
+            if (Math.round(X - dx - x - length)) {
+                type != "-" && type != " " && (path = path.concat(["M", x + length + .5, y - (type == "+" ? dashsize : !!orientation * dashsize * 2), "l", 0, dashsize * 2 + 1]));
+                text.push(this.text(x + length, y + addon, (labels && labels[j]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(this.g.txtattr));
+            }
+        }
+        var res = this.path(path);
+        res.text = text;
+        res.all = this.set([res, text]);
+        res.remove = function () {
+            this.text.remove();
+            this.constructor.prototype.remove.call(this);
+        };
+        return res;
+    };
+
+    Raphael.el.lighter = function (times) {
+        times = times || 2;
+        var fs = [this.attrs.fill, this.attrs.stroke];
+        this.fs = this.fs || [fs[0], fs[1]];
+        fs[0] = Raphael.rgb2hsb(Raphael.getRGB(fs[0]).hex);
+        fs[1] = Raphael.rgb2hsb(Raphael.getRGB(fs[1]).hex);
+        fs[0].b = Math.min(fs[0].b * times, 1);
+        fs[0].s = fs[0].s / times;
+        fs[1].b = Math.min(fs[1].b * times, 1);
+        fs[1].s = fs[1].s / times;
+        this.attr({fill: "hsb(" + [fs[0].h, fs[0].s, fs[0].b] + ")", stroke: "hsb(" + [fs[1].h, fs[1].s, fs[1].b] + ")"});
+    };
+    Raphael.el.darker = function (times) {
+        times = times || 2;
+        var fs = [this.attrs.fill, this.attrs.stroke];
+        this.fs = this.fs || [fs[0], fs[1]];
+        fs[0] = Raphael.rgb2hsb(Raphael.getRGB(fs[0]).hex);
+        fs[1] = Raphael.rgb2hsb(Raphael.getRGB(fs[1]).hex);
+        fs[0].s = Math.min(fs[0].s * times, 1);
+        fs[0].b = fs[0].b / times;
+        fs[1].s = Math.min(fs[1].s * times, 1);
+        fs[1].b = fs[1].b / times;
+        this.attr({fill: "hsb(" + [fs[0].h, fs[0].s, fs[0].b] + ")", stroke: "hsb(" + [fs[1].h, fs[1].s, fs[1].b] + ")"});
+    };
+    Raphael.el.original = function () {
+        if (this.fs) {
+            this.attr({fill: this.fs[0], stroke: this.fs[1]});
+            delete this.fs;
+        }
+    };
+})();

BIN
src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/load-big.gif


BIN
src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/load.gif


+ 54 - 0
src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/loggraph.css

@@ -0,0 +1,54 @@
+/*
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+body { font-family: sans-serif; }
+
+div.fileSelector { border: solid 3px black; position: absolute; background: white; 
+		   -moz-border-radius: 10px; border-radius: 10px; 
+		   padding: 5px; font-family: sans-serif;
+		   right: 10px; top: 10px;
+		 }
+div.fileSelector a { cursor: pointer; }
+.fileSelector li.selectedFile { background: lightgreen; }
+
+div.selector { border: solid 3px black; position: absolute; background: white; 
+		   -moz-border-radius: 10px; border-radius: 10px; 
+		   padding: 5px; 
+		   right: 10px; top: 10px; background: #aaaaaa; opacity: 0.7;
+		 }
+div.selector a { cursor: pointer; }
+.fileSelector li.selectedFile { background: lightgreen; }
+
+#fileLoader  { -moz-border-radius: 10px; border-radius: 10px; background: #aaaaaa; opacity: 0.7; position: absolute; left: 20px; top: 20px; }
+#loadingScreen { position: absolute; top: 100px; margin-left: 40%; margin-right: 40%; width: 500px; background: #aaaaaa; opacity: 0.7; -moz-border-radius: 10px; border-radius: 10px; text-align: center }
+#filterinput { width: 500px; height: 100px; }
+/* main interface */
+#actions { float: right; }
+#views { float: left; }
+
+.closebutton { position: absolute; right: 5px; float: right; display: block; cursor: pointer; }
+
+.actionbutton { color: blue; text-decoration: none; padding: 3px; cursor: pointer; }
+span:hover.actionbutton { background: lightblue;  }
+
+#status { text-align: center; }
+
+#canvas { width: 100%; height: 1000px; }
+
+#logtable { width: 100%; }
+.popUp { border: 3px solid black; -moz-border-radius: 10px; border-radius: 10px; position: absolute; background: white; padding: 10px; min-width: 300px; }
+
+.errorpage { position: absolute; top: 100px; margin-left: 40%; margin-right: 40%; width: 500px; background: #aaaaaa; opacity: 0.7; -moz-border-radius: 10px; border-radius: 10px; padding: 10px; }

+ 262 - 0
src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/loggraph.js

@@ -0,0 +1,262 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+LogGraph = function(canvas, status) {
+    this.canvas = document.getElementById(canvas);
+    this.status = document.getElementById(status);
+    this.starttime = 0;
+    this.endtime = 0;
+    this.period = 0;
+    this.numEntries = 0;
+    this.currentRender = 0;
+    this.filter = "";
+
+    this.saveFilters = function () {
+	localStorage.starttime = this.starttime;
+	localStorage.endtime = this.endtime;
+	localStorage.period = this.period;
+	localStorage.filter = this.filter;
+	
+    };
+    this.loadFilters = function () {
+	if (localStorage.starttime) { this.starttime = parseInt(localStorage.starttime); }
+	if (localStorage.endtime) { this.endtime = parseInt(localStorage.endtime); }
+	if (localStorage.period) { this.period = parseInt(localStorage.period); }
+	if (localStorage.filter) { this.filter = localStorage.filter; }
+    };
+    this.loadFilters();
+    var self = this;
+
+    var updateStatus = function (starttime, period, filter, numEntries) {
+	self.starttime = starttime;
+	self.endtime = starttime + period;
+	self.period = period;
+	self.filter = filter;
+	self.saveFilters(); 
+       
+	self.status.innerHTML = dateFormat(starttime, "HH:MM:ss,l") + " &rArr; " + dateFormat(self.endtime, "HH:MM:ss,l") + " &nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp; " + numEntries + " entries  &nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp; " +  (filter ? filter : "No filter");
+	
+	if (self.currentRender) {
+	    self.currentRender();
+	}
+    };
+	
+    YUI().use("io-base", function(Y) {
+	    var uri = "/info";
+	    if (self.starttime) {
+		var uri = "/info?start=" + self.starttime + "&period=" + self.period + "&filter=" + self.filter;
+	    }
+	    
+	    function complete(id, o, args) {
+		var data = eval("(" + o.responseText + ")"); // Response data.
+		var period = data.endTime - data.startTime;
+		updateStatus(data.startTime, period, self.filter, data.numEntries);
+	    };
+	    
+	    Y.on('io:complete', complete, Y, []);
+	    var request = Y.io(uri);
+	});
+    
+    this.addLogs = function() {
+	new LogGraph.fileSelector(function (files) { new LogGraph.fileLoader(files); });
+    };
+
+    this.editFilters = function() {
+	new LogGraph.filterSelector(this.starttime, this.period, this.filter, updateStatus);
+    };	
+	
+    this.getCleanCanvas = function () {
+	this.canvas.innerHTML = "";
+	return this.canvas;
+    };
+
+    this.showLoadingScreen = function () {
+	this.loadingScreen = document.createElement("div");
+	this.loadingScreen.id = "loadingScreen";
+	this.loadingScreen.innerHTML = "<img src=\"load-big.gif\" /> <p>Loading...</p>";
+	document.body.appendChild(this.loadingScreen);
+    };
+
+    this.hideLoadingScreen = function () {
+	document.body.removeChild(this.loadingScreen);
+	this.loadingScreen.style.visibility = "hidden";
+    };
+
+
+    /***
+     * TODO: refactor these to load the data first, before handing to a draw funciton. 
+     *       We shouldn't pass the async q into the drawing function
+     */
+    this.showLogs = function() {
+	var self= this;
+	YUI().use('async-queue', function(Y) {
+		var q = new Y.AsyncQueue(self.showLoadingScreen,
+					 // The second callback will pause the Queue and send an XHR for data
+					 function () {
+					     q.pause();
+					     var loggraph = new LogGraph.LogTable(q, self.getCleanCanvas(), self.starttime, self.endtime, self.filter);
+					     self.currentRender = self.showLogs;
+					 },
+					 self.hideLoadingScreen);
+		q.run();
+	    }
+	    );
+    };
+
+    this.serverGraph = function() {
+	var self= this;
+	YUI().use('async-queue', function(Y) {
+		var q = new Y.AsyncQueue(self.showLoadingScreen,
+					 // The second callback will pause the Queue and send an XHR for data
+					 function () {
+					     q.pause();
+					     var servergraph = new LogGraph.ServerGraph(q, self.getCleanCanvas(), self.starttime, self.endtime, self.filter);
+					     self.currentRender = self.showLogs;
+					 },
+					 self.hideLoadingScreen);
+		q.run();
+	    }
+	    );
+    };
+    
+    this.sessionGraph = function() {
+	var self= this;
+	YUI().use('async-queue', function(Y) {
+		var q = new Y.AsyncQueue(self.showLoadingScreen,
+					 // The second callback will pause the Queue and send an XHR for data
+					 function () {
+					     q.pause();
+					     var sessiongraph = new LogGraph.SessionGraph(q, self.getCleanCanvas(), self.starttime, self.endtime, self.filter);
+					     self.currentRender = self.sessionGraph;
+					 },
+					 self.hideLoadingScreen);
+		q.run();
+	    }
+	    );
+    };
+    
+    this.showStats = function() {
+	var self= this;
+	YUI().use('async-queue', function(Y) {
+		var q = new Y.AsyncQueue(self.showLoadingScreen,
+					 // The second callback will pause the Queue and send an XHR for data
+					 function () {
+					     q.pause();
+					     var statgraph = new LogGraph.StatsGraph(q, self.getCleanCanvas(), self.starttime, self.endtime, self.filter);
+					     self.currentRender = self.showStats;
+					 },
+					 self.hideLoadingScreen);
+		q.run();
+	    }
+	    );
+    };
+};
+
+LogGraph.error = function(description) {
+    var errorPage = document.createElement("div");
+    errorPage.className = "errorpage";
+    var p = document.createElement("p");
+    p.innerHTML = description;
+    errorPage.appendChild(p);
+    
+    var span = document.createElement("span");
+    p = document.createElement("p");
+    span.className = "actionButton";
+    span.innerHTML = "OK";
+    span.onclick = function (evt) {
+	document.body.removeChild(errorPage);
+	delete errorPage;
+    }
+    p.appendChild(span);
+    errorPage.appendChild(p);
+
+    document.body.appendChild(errorPage);
+};
+
+LogGraph.ticker =function(allow_dups) {
+    this.ticks = new Array();
+    this.current_tick = 0;
+    this.allow_dups = allow_dups;;
+    
+    this.tick = function(time) {
+	if (time == this.ticks[this.ticks.length - 1] && this.allow_dups == true) 
+	    return this.current_tick;
+	
+	this.ticks.push(time);
+	return this.current_tick++;
+    };
+    
+    this.current = function() {
+	return this.current_tick;
+    };
+    
+    this.reset = function() {
+	while (this.ticks.length) {
+	    this.ticks.pop();
+	}
+	this.current_tick = 0;
+    };
+};
+
+
+LogGraph.timescale = function(starttime, endtime) {
+    this.starttime = starttime;
+    this.endtime = endtime;
+    this.millis = endtime - starttime;
+    
+    this.draw = function(paper) {
+	var scale = paper.set();
+	scale.push(paper.path("M0 0 L" + paper.width + " 0"));
+	
+	for (var i = 0; i < paper.width; i += 100) {
+	    scale.push(paper.path("M" + i + " 0 L" + i + " 5"));
+	    //		var time = dateFormat((this.starttime + (i*ms_per_pixel)), "h:MM:ss,l");
+		//	paper.text(i + 5, 10, time);
+	}
+	
+	scale.attr({"stroke-width": 2});
+    };
+};
+
+/*
+  Fetch data from an uri and process it, the process data func returns true if any of the data is useful  
+*/
+LogGraph.loadData = function (asyncq, uri, processdata) {
+    YUI().use("io-base", function(Y) {
+	    function success(id, o, args) {
+		var data = eval("(" + o.responseText + ")"); // Response data.
+		if (data.error) {
+		    LogGraph.error(data.error);
+		} else {
+		    if (!processdata(data)) {
+			LogGraph.error("No data. Perhaps you should loosen your filter criteria.");
+		    }
+		}
+		asyncq.run();
+	    };
+	    function failure(id, o, args) {
+		LogGraph.error("Error contacting server: (" + o.status + ") " + o.statusText);
+		asyncq.run();
+	    };
+
+	    Y.on('io:success', success, Y, []);
+	    Y.on('io:failure', failure, Y, []);
+	    
+	    var request = Y.io(uri);
+	});
+}

+ 57 - 0
src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/loggraph.log.js

@@ -0,0 +1,57 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+LogGraph.LogTable = function (asyncq, canvas, starttime, endtime, filter) {
+    this.starttime = starttime;
+    this.endtime = endtime;
+    this.filter = filter;
+
+    var table = document.createElement("table");
+    table.id = "logtable";
+    canvas.appendChild(table);
+
+    this.addLogLine = function(time, text) {
+	var tr = document.createElement("tr");
+	table.appendChild(tr);
+	
+	var td = document.createElement("td");
+	td.innerHTML = dateFormat(time, "h:MM:ss,l");
+	tr.appendChild(td);
+
+	td = document.createElement("td");
+	td.innerHTML = text;
+	tr.appendChild(td);
+    }
+
+    var self = this;
+    var processdata = function(data) {
+	var events = data["events"];
+	var count = 0;
+	for (var i in events) {
+	    var e = events[i];
+	    if (e.type == "text") {
+		self.addLogLine(e.time, e.text);
+		count++;
+	    }
+	}
+	return count != 0;
+    };
+
+    var uri = "/data?start=" + self.starttime + "&end=" + self.endtime + "&filter=" + self.filter; 
+    LogGraph.loadData(asyncq, uri, processdata);
+};

+ 329 - 0
src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/loggraph.server.js

@@ -0,0 +1,329 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+LogGraph.ServerGraph = function(asyncq, canvas, starttime, endtime, filter) {
+    this.starttime = starttime;
+    this.endtime = endtime;
+    this.millis = endtime - starttime;
+    this.nextserverid = 0;
+    this.serveroffset = 100;
+    this.filter = filter;
+    
+    this.pixels_per_tick =  20;
+    this.ticker = new LogGraph.ticker();
+
+    
+    var paper = Raphael(canvas, 1, 1);
+
+    var self = this;
+
+    this.timescale = new LogGraph.timescale(starttime, endtime);
+    this.objects = new Array();
+    
+    this.add = function(obj) {
+	this.objects.push(obj);
+    }
+    
+    this.tick_to_x = function (timestamp) {
+	var x = timestamp * this.pixels_per_tick; 
+	return x;
+    }; 
+    
+    this._drawTime  = function(paper, x, time) {
+	var p = paper.path("M" + x + " 0 L" + x + " " + paper.height);
+	var t = paper.text(x, 10, dateFormat(time, "h:MM:ss,l"));
+	
+	t.hide();
+	p.mouseover(function(evt) {
+		t.show();
+		p.attr({stroke: "red"});
+	    });
+	p.mouseout(function(evt) {
+		t.hide();
+		p.attr({stroke: "lightgray"});
+	    });
+	
+	return p;
+    };
+    
+    this.draw = function(paper) {
+	var grid = paper.set();
+	for (var i = 0; i < paper.height; i += 20) {
+	    grid.push(paper.path("M0 " + i + " L" + paper.width + " " + i));
+	}
+	var lasttick = this.starttime;
+	var scale = 500; // 500 ms
+	
+	var y = 0;
+	
+	for (var t = 0, len = this.ticker.ticks.length; t < len; t++) {
+	    var basex = t * this.pixels_per_tick;
+	    var thistick = this.ticker.ticks[t];
+	    var nexttick = t + 1 == this.ticker.ticks.length ? this.endtime : this.ticker.ticks[t+1];
+	    if (nexttick == thistick) {
+		continue;
+	    }
+	    var time = thistick - lasttick;
+	    var first = scale - (lasttick % scale);
+	    
+	    /*		for (var i = 0; (first+scale*i) < time; i++) {
+			
+			var toffset = first+scale*i;
+			var x = basex + LogGraph._pixels_per_tick * toffset/time;
+			grid.push(this._drawTime(paper, x, lasttick + toffset, grid));
+			
+			}*/
+	    
+	    
+	    //grid.push(paper.path("M" + i + " 0 L" + i + " " + paper.height));
+	    lasttick = thistick;
+	}
+	grid.attr({stroke: "lightgray"});
+	this.timescale.draw(paper);
+	
+	for (o in this.objects) {
+	    this.objects[o].draw(paper);
+	}
+    };
+
+
+    var processdata = function(data) {
+	var servermap = {};
+	var servers = data.servers;
+	var count = 0;
+	for (s in servers) {
+	    var server = new LogGraph.ServerGraph.server(self, "Server " + servers[s]);
+	    servermap[servers[s]] = server;
+	    self.add(server);
+	    count++;
+	}
+	
+	var messages = {};
+	var events = data.events;
+	for (var i in events) {
+	    var e = events[i];
+	    var t = e.time;
+	    if (e.type == "stateChange") {
+		servermap[e.server].addState(e.state, self.ticker.tick(e.time));
+	    }
+	    if (e.type == "postmessage") {
+		src = servermap[e.src];
+		dst = servermap[e.dst];
+		var key = "key:s" + e.src + ",d" + e.dst + ",z" + e.zxid;
+		    
+		var m = new LogGraph.ServerGraph.message(self, src, self.ticker.tick(e.time), dst, e.zxid);
+		messages[key] = m;
+	    } 
+	    if (e.type == "delivermessage") {
+		var key = "key:s" + e.src + ",d" + e.dst + ",z" + e.zxid;
+		
+		var m = messages[key];
+		if (m) {
+		    m.dsttime = self.ticker.tick(e.time);
+		    m.name = "Propose";
+		    self.add(m);
+		    delete messages[key];
+		}
+	    } 
+	    if (e.type == "exception") {
+		servermap[e.server].addException(self.ticker.tick(e.time), e.text, e.time);
+	    }
+	    count++;
+	}
+	
+	for (var i in messages) {
+	    var m = messages[i];
+	    m.markIncomplete();
+	    self.add(m);
+	    count++;
+	}
+
+	if (count != 0) {
+	    paper.setSize(self.tick_to_x(self.ticker.current()), 1000);	    
+	    
+	    var line = paper.path("M0 0 L0 1000");	    
+	    line.attr({"stroke": "red", "stroke-dasharray": "- "});
+	    var base = canvas.offsetLeft;// + ((canvas.offsetWidth - paper.width)/2);
+	    canvas.onmousemove = function (evt) {
+		var x = evt.screenX - base;
+		
+		line.attr({"path": "M" + x + " 0 L"+ x +" 1000"});
+		
+	    };
+
+	    self.draw(paper);
+	    return true;
+	} else {
+	    return false;
+	}
+    };
+		
+    var uri = "/data?start=" + self.starttime + "&end=" + self.endtime + "&filter=" + filter;
+
+    LogGraph.loadData(asyncq, uri, processdata);    
+};
+
+LogGraph.ServerGraph.server = function (graph, name) {
+    this.graph = graph;
+    this.serverid = graph.nextserverid++;
+    this.name = name;
+    this.y = (this.serverid * 300 + graph.serveroffset);
+    this.states = new Array();
+    this.exception = new Array();
+    
+    this.addState = function(state, time) {
+	this.states.push([state, time]);
+    }
+    
+    this.addException = function(tick, exception, time) {
+	this.exception.push(new LogGraph.ServerGraph.exception(this.graph, tick, exception, time));
+    }
+    
+    this.draw = function(paper) {
+	var st = paper.set();
+	st.push(paper.path("M0 " + this.y + " L" + paper.width + " " + this.y));
+	st.push(paper.text(20, this.y - 10, this.name));
+	st.attr({stroke: "gray"});
+	
+	var numstates = this.states.length;
+	
+	for (s = 0; s < numstates; s++) {
+	    var style = {};
+	    switch (this.states[s][0]) {
+	    case "INIT": style = {stroke: "yellow", "stroke-width":3}; break;
+	    case "FOLLOWING": style = {stroke: "lightgreen", "stroke-width":7}; break;
+	    case "LEADING": style = {stroke: "green", "stroke-width":10}; break;
+	    case "LOOKING": style = {stroke: "orange", "stroke-width":5}; break;
+	    }
+	    var startx = this.graph.tick_to_x(this.states[s][1]);
+	    var endx = s + 1 < numstates ? this.graph.tick_to_x(this.states[(s+1)][1]) : paper.width;
+	    var p = paper.path("M" + startx + " " + this.y + " L" + endx + " " + this.y);
+	    p.attr(style);
+	}
+	
+	for (e in this.exception) {
+	    this.exception[e].draw(paper, this);
+	}
+    }
+};    
+    
+LogGraph.ServerGraph.message = function(graph, src, srctime, dst, zxid) {
+    this.graph = graph;
+    this.src = src;
+    this.srctime = srctime;
+    this.dst = dst;
+    this.dsttime = 0; //dsttime;
+    this.name = "Unknown";
+    this.zxid = zxid;
+    this.moreinfo = "No extra information";
+    this.incomplete = false;
+    
+    this.markIncomplete = function() {
+	this.incomplete = true;
+	this.dsttime = this.srctime;
+    }
+
+    this.draw = function(paper) {
+	var srcx = this.graph.tick_to_x(this.srctime);
+	var dstx = this.graph.tick_to_x(this.dsttime);
+	
+	var arrow = paper.set();
+	var p = paper.path("M" + srcx + " " + this.src.y + " L" + dstx + " " + this.dst.y);
+	arrow.push(p);
+	
+	var tx = (srcx + dstx)/2;
+	var ty = (this.src.y + this.dst.y)/2;
+	var t = paper.text(tx, ty, this.name);
+	
+	var gradiant = (this.dst.y - this.src.y)/(dstx - srcx);
+	var angle = Math.atan(gradiant) * 57.2958;
+	t.rotate(angle, true);
+	
+	var arrowl = paper.path("M" + dstx + " " + this.dst.y + " L" + (dstx - 10) +" " + this.dst.y);
+	arrowl.rotate(angle + 20, dstx, this.dst.y);
+	arrow.push(arrowl);
+	var arrowr = paper.path("M" + dstx + " " + this.dst.y + " L" + (dstx - 10) +" " + this.dst.y);
+	arrowr.rotate(angle - 20, dstx, this.dst.y);
+	arrow.push(arrowr);
+	
+	arrow.attr({"stroke-width": 2, stroke: "gray"});
+	if (this.incomplete) {
+	    arrow.attr({"stroke-dasharray": "- .", stroke: "pink", "stroke-width": 2});
+	}
+	arrow.mouseover(function(evt) {
+		t.attr({"font-size": 20});
+		arrow.attr({stroke: "red", "stroke-width": 3});
+	    });
+	arrow.mouseout(function(evt) {
+		t.attr({"font-size": 10});
+		
+		if (this.incomplete) {
+		    arrow.attr({stroke: "pink", "stroke-width": 2});
+		} else {
+		    arrow.attr({stroke: "gray", "stroke-width": 2});
+		}
+	    });
+	
+	
+
+	arrow.click(function(evt) { 
+		var popup = document.createElement("div");
+		popup.className = "popUp";
+		popup.innerHTML = "zxid: " + parseInt(this.zxid).toString(16);
+
+		popup.style.top = evt.clientY;
+		popup.style.left = evt.clientX;
+		document.body.appendChild(popup);
+
+		popup.onclick = function(evt) { 
+		    document.body.removeChild(popup);
+		};
+	    });
+    }
+};
+    
+LogGraph.ServerGraph.exception = function(graph, tick, exceptiontext, time) {
+    this.graph = graph;
+    this.time = time;
+    this.text = exceptiontext;
+    this.tick = tick;
+    
+    var self = this;
+
+    this.draw = function(paper, server) {
+	var center = this.graph.tick_to_x(this.tick);
+	var p = paper.circle(center, server.y, 5);
+	p.attr({stroke: "orange", fill: "red"});
+	
+	p.mouseover(function(evt) {
+		p.popup = document.createElement("div");
+		p.popup.className = "popUp";
+		p.popup.innerHTML = self.text.replace("\n", "<br/>");;
+		p.popup.style.top = server.y + 50;
+		p.popup.style.left = center + 25;
+		document.body.appendChild(p.popup);
+
+		p.animate({r: 10}, 500, "elastic");
+	    });
+	p.mouseout(function(evt) {
+		document.body.removeChild(p.popup);
+		p.animate({r: 5}, 100);
+	    });
+    }
+};
+    

+ 202 - 0
src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/loggraph.session.js

@@ -0,0 +1,202 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+LogGraph.SessionGraph = function (asyncq, canvas, starttime, endtime, filter) {
+    this.sessions = new Array();
+    this.counter = 0;
+    this.exceptions = new Array();
+    
+    this.pix_per_ticks = 4;
+    this.pix_per_session = 7;
+
+    var paper = Raphael(canvas, 1, 1);
+    this.ticker = new LogGraph.ticker();
+    var self = this;
+
+    this.starttime = starttime;
+    this.endtime = endtime;
+    this.filter = filter;
+
+    this.findOrCreateSession = function(id) {
+	if (this.sessions[id] == undefined) {
+	    this.sessions[id] = new LogGraph.SessionGraph.session(this, ++this.counter, id);
+	}
+	return this.sessions[id];
+    }
+    
+    this.height = function () { return this.counter * this.pix_per_session + 10; };
+    this.width = function () { return (self.ticker.current() * this.pix_per_ticks); };
+
+    this.draw = function(paper) {
+	
+
+	var line = paper.path("M0 0 L0 " + this.height());	    
+	line.attr({"stroke": "red", "stroke-dasharray": "- "});
+	var base = canvas.offsetLeft;
+	var width = this.width();
+	canvas.onmousemove = function (evt) {
+	    var x = evt.clientX - base;
+
+	    line.attr({"path": "M" + x + " 0 L" + x + " " + self.height() });
+	};	
+	
+	for (var i in this.sessions) {
+	    var s = this.sessions[i];
+	    s.draw(paper);
+	}
+    };
+
+    var processdata = function(data) {
+	var count = 0;
+	for (var i in data.events) {
+	    var e = data.events[i];
+	    if (e.type == "transaction") {
+		e.tick = self.ticker.tick(e.time, true);
+		var session = self.findOrCreateSession(e.client);
+		session.addEvent(e);
+		count++;
+	    }
+	}
+	paper.setSize(self.width(), self.height());
+	
+	if (count != 0) {
+	    self.draw(paper);
+	    return true;
+	} else {
+	    return false;
+	}
+    };
+    
+    var uri = "/data?start=" + self.starttime + "&end=" + self.endtime + "&filter=" + filter;
+
+    LogGraph.loadData(asyncq, uri, processdata);    
+};
+
+LogGraph.SessionGraph.sessionevent = function () {
+    this.time = time;
+    this.type = type;
+    this.client = client;
+    this.cxid = cxid;
+    this.zxid = zxid;
+    this.op = op;
+    this.extra = extra;
+};
+
+LogGraph.SessionGraph.sessionEventPopup = function (obj, e, x, y) {
+    obj.click(function(evt) {
+	    var popup = document.createElement("div");
+	    popup.className = "popUp";
+
+	    var closebutton = document.createElement("div");
+	    closebutton.className = "closebutton";
+	    closebutton.title = "Close popup";
+	    closebutton.innerHTML = "&times;";
+	    popup.appendChild(closebutton);
+	    closebutton.onclick= function(evt) { popup.style.visibility = "hidden"; document.body.removeChild(popup) };
+	    var txt = document.createElement("span");
+	    txt.innerHTML = "session: " + e.client + "<br/>op: " + e.op + "<br/>zxid: " + e.zxid + "<br/>time: " + e.time + "<br/>extra: " + e.extra;
+	    popup.appendChild(txt);
+	    
+	    popup.style.top = y;
+	    popup.style.left = x;
+	    document.body.appendChild(popup);
+	    
+	    YUI().use('dd-drag', function(Y) {
+		    //Selector of the node to make draggable
+		    var dd = new Y.DD.Drag({
+			    node: popup
+			});   
+		});
+	});
+};
+    
+LogGraph.SessionGraph.session = function (graph, index, id) {
+    this.index = index;
+    this.id = id;
+    this.graph = graph;
+    
+    this.events = new Array();
+    this.starttick = 0;
+    this.endtick = undefined;
+    
+    this.addEvent = function(e) {
+	this.events.push(e);
+	
+	if (e.op == "createSession") {
+	    //		document.write("createSession for " + id.toString(16));
+	    this.starttick = e.tick;
+	} else if (e.op == "closeSession") {
+	    this.endtick = e.tick;
+	}
+    },
+    
+    this._attach_action = function (sess, label) {
+	sess.mouseover(function(evt) {
+		label.show();
+		sess.attr({stroke: "gray"});
+	    });
+	
+	sess.mouseout(function(evt) {
+		label.hide();
+		sess.attr({stroke: "black"});
+	    });
+    },
+    
+    this.drawEvent = function (paper, y, e) {
+	var x = e.tick * this.graph.pix_per_ticks;;
+	var s = paper.path("M" + x + " " + (y - 3) + " L" + x + " " + (y + 3));
+	s.attr({"stroke-width": 2});
+	if (e.op == "error") {
+	    s.attr({"stroke": "red"});
+	} 
+	s.mouseover(function(evt) {
+		s.attr({"stroke-width": 5});
+	    });
+	
+	s.mouseout(function(evt) {
+		s.attr({"stroke-width": 2});
+	    });
+	
+	LogGraph.SessionGraph.sessionEventPopup(s, e, x, y);
+    },
+    
+    this.draw = function(paper) {
+	var y = this.index*this.graph.pix_per_session;;
+	var start = this.starttick * this.graph.pix_per_ticks;
+	var end = this.endtick * this.graph.pix_per_ticks;
+	
+	var sess = paper.set();
+	
+	if (this.endtick == undefined) {
+	    end = this.graph.width();
+	} 
+
+	sess.push(paper.path("M" + start + " " + y + " L" + end + " " + y));
+	for (var i in this.events) {
+	    var e = this.events[i];
+	    this.drawEvent(paper, y, e);
+	}
+	
+	//sess.attr({"stroke-width": 3});
+	label = paper.text(start + 100, y, this.id);
+	label.attr({"font-size": "14px"});
+	label.hide();
+	this._attach_action(sess, label);
+    }
+};
+

+ 44 - 0
src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/loggraph.stats.js

@@ -0,0 +1,44 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+LogGraph.StatsGraph = function (asyncq, canvas, starttime, endtime, filter) {
+    var processdata = function(data) {
+	var r = Raphael(canvas);
+	var x = data.map(function (x) { return x.time; });
+	var y = data.map(function (x) { return x.count; });
+	var xlabels = data.map(function (x) { return dateFormat(x.time, "HH:MM:ss,l"); } );
+	var h1 = function () {
+	    this.tags = r.set();
+	    for (var i = 0, ii = this.y.length; i < ii; i++) {
+		this.tags.push(r.g.tag(this.x, this.y[i], this.values[i], 160, 10).insertBefore(this).attr([{fill: "#fff"}, {fill: this.symbols[i].attr("fill")}]));
+	    }
+	};
+	var h2 = function () {
+	    this.tags && this.tags.remove();
+	};
+	r.g.linechart(40, 40, 1000, 500,  x, y, {shade: true, axis: "0 0 1 1", symbol: "x", southlabels: xlabels, axisxstep: xlabels.length - 1 , westAxisLabel: "Write requests", southAxisLabel: "Time (min)"}).hoverColumn(h1, h2);
+
+	return true;
+	//r.g.barchart(0, 0, 1000, 100,  y, {shade: true, symbol: "x"}).hoverColumn(h1, h2);
+    };
+    
+    var uri = "/throughput?scale=minutes";
+    LogGraph.loadData(asyncq, uri, processdata);    
+};
+
+    

+ 377 - 0
src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/loggraph.ui.js

@@ -0,0 +1,377 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Opens a window to load files into the engine
+LogGraph.fileSelector = function(callback) {
+    var self = this;	
+    this.callback = callback;
+    this.selectedFiles = new Array();
+    
+    var divTag = document.createElement("div");
+    divTag.id = "fileSelector" + Math.round(Math.random()*100000);
+    //	divTag.className = "popUp";
+    divTag.className = "selector fileSelector";
+    document.body.appendChild(divTag);
+    
+    YUI().use('dd-drag', function(Y) {
+	    //Selector of the node to make draggable
+	    var dd = new Y.DD.Drag({
+			node: '#' + divTag.id
+		});   
+	});
+    
+    var list = document.createElement("ul");
+    divTag.appendChild(list);
+    var selectedList = document.createElement("selectedlist");
+    divTag.appendChild(selectedList);
+    
+    var clearanchor = document.createElement("span");
+    clearanchor.innerHTML = "Remove All";
+    clearanchor.className = "actionbutton";
+    clearanchor.style.cssFloat = "right";
+    clearanchor.onclick = function () {
+	self.selectedFiles = new Array();
+	self.updateSelectedList();
+    };
+    divTag.appendChild(clearanchor);
+    
+    var doneanchor = document.createElement("span");
+    doneanchor.innerHTML = "Process Files";
+    doneanchor.className = "actionbutton";
+    doneanchor.style.cssFloat = "left";
+    doneanchor.onclick = function () {
+	self.callback(self.selectedFiles);
+	document.body.removeChild(divTag);
+	delete divTag;
+    };
+    divTag.appendChild(doneanchor);
+    
+    var cancelanchor = document.createElement("span");
+    cancelanchor.innerHTML = "Cancel";
+    cancelanchor.className = "actionbutton";
+    cancelanchor.style.cssFloat = "left";
+    cancelanchor.onclick = function () {
+	document.body.removeChild(divTag);
+	delete divTag;
+    };
+    divTag.appendChild(cancelanchor);
+    
+    this.createFileListItem = function (file) {
+	var li = document.createElement("li");
+	var a = document.createElement("a");
+	if (file.type == "D") {
+	    a.innerHTML = file.file + "/";
+	    a.onclick = function () { self.updateList(file.path); };
+	} else {
+	    a.innerHTML = file.file;
+	    a.onclick = function () { self.addSelectedFile(file.path); };
+	}
+	
+	a.fullpath = file.path;;
+	li.appendChild(a);
+	return li;
+    };
+    
+    this.addSelectedFile = function (file) {
+	if (this.selectedFiles.indexOf(file) == -1) {
+	    this.selectedFiles.push(file);
+	    this.updateSelectedList();
+	}
+    };
+    
+    this.removeSelectedFile = function (file) {
+	this.selectedFiles = this.selectedFiles.filter(function(f) { return !(file == f); });
+	this.updateSelectedList();
+    };
+    
+    this.createSelectedListItem = function (file) {
+	var li = document.createElement("li");
+	var a = document.createElement("a");
+	li.className = "selectedFile";
+	a.onclick = function () { self.removeSelectedFile(file); };
+	a.innerHTML = file;
+	li.appendChild(a);
+	return li;
+    };
+    
+    this.updateSelectedList = function () {
+	while (selectedList.firstChild) { selectedList.removeChild(selectedList.firstChild); }
+	
+	for (var i in this.selectedFiles) {
+	    var f = this.selectedFiles[i];
+	    selectedList.appendChild(this.createSelectedListItem(f));
+	}
+    };
+    
+    this.updateList = function (base) {
+	while (list.firstChild) list.removeChild(list.firstChild);
+	
+	// Create a YUI instance using io-base module.
+	YUI().use("io-base", function(Y) {
+		var uri = "/fs?path=" + base;
+		
+		// Define a function to handle the response data.
+		function complete(id, o, args) {
+		    var id = id; // Transaction ID.
+		    var data = eval("(" + o.responseText + ")"); // Response data.
+		    var parts = base.split("/").slice(0,-1);
+		    var parent = ""
+			if (parts.length < 2) {
+			    parent = "/";
+			} else {
+			    parent = parts.join("/");
+			}
+		    if (base != "/") {
+			var li = self.createFileListItem({"file": "..", type: "D", path: parent});
+			list.appendChild(li);
+		    }
+		    for (var i in data) {
+			var f = data[i];
+			if (f.file[0] != '.') {
+			    var li = self.createFileListItem(f);
+			    list.appendChild(li);
+			}
+		    }
+		};
+		
+		Y.on('io:complete', complete, Y, []);
+		var request = Y.io(uri);
+	    });
+    };
+    
+    this.updateList("/");
+};
+
+// Open a window which loads files into the engine
+LogGraph.fileLoader = function(files) {
+    var div = document.createElement("div");
+    div.id = "fileLoader";
+    
+    var imgArray = new Array();
+    var pArray = new Array();
+    for (var index in files) {
+	var f = files[index];
+	var p = document.createElement("p");
+	var i = document.createElement("img");
+	i.src = "load.gif";
+	i.style.visibility = "hidden";
+	imgArray.push(i);
+	pArray.push(p);
+	var span = document.createElement("span");
+	span.innerHTML = f;
+	
+	p.appendChild(span);
+	p.appendChild(i);
+	
+	div.appendChild(p);
+    }
+    
+    var loadFile = function (index) {
+	// Create a YUI instance using io-base module.
+	YUI().use("io-base", function(Y) {
+		var file = files[index];
+		    var uri = "/loadfile?path=" + file;
+		imgArray[index].style.visibility = "visible";
+		
+		// Define a function to handle the response data.
+		function complete(id, o, args) {
+		    var id = id; // Transaction ID.
+			var data = eval("(" + o.responseText + ")"); // Response data.
+			if (data.status == "ERR") {
+			    var err = document.createElement("div");
+			    err.innerHTML = data.error;
+			    pArray[index].appendChild(err);
+			} else if (data.status == "OK") {
+			    var ok = document.createElement("div");
+			    ok.innerHTML = "OK";
+			    pArray[index].appendChild(ok);
+			}
+			
+			imgArray[index].style.visibility = "hidden";
+			if (index + 1 < files.length) {
+			    loadFile(index + 1);
+			} else {
+			    //alert("DONE");
+			}
+		};
+		
+		Y.on('io:complete', complete, Y, []);
+		var request = Y.io(uri);
+	    });
+    };
+	
+    var doneanchor = document.createElement("a");
+    doneanchor.className = "actionbutton";
+    doneanchor.innerHTML = "Done";
+    doneanchor.onclick = function () {
+	document.body.removeChild(div);
+	delete div;
+    };
+    
+    document.body.appendChild(div);
+    if (files.length > 0) {
+	loadFile(0);
+    } else {
+	div.innerHTML ="No files to load";
+    }
+    div.appendChild(doneanchor);
+}
+
+// select a time period
+LogGraph.filterSelector = function(starttime, period, filter, callback) {
+    var self = this;	
+    this.callback = callback;
+    
+    // Container other widgets will be in
+    var container = document.createElement("div");
+    container.id = "filterSelector" + Math.round(Math.random()*100000);
+    container.className = "selector filterSelector";
+    document.body.appendChild(container);
+    
+    YUI().use('dd-drag', function(Y) {
+	    //Selector of the node to make draggable
+	    var dd = new Y.DD.Drag({
+		    node: '#' + container.id
+		});   
+	});
+    
+    // Temporary loading screen
+    var loadingp = document.createElement("p");
+    loadingp.innerHTML = "Loading...";
+    var loadimg = document.createElement("img");
+    loadimg.src = "load.gif";
+    loadingp.appendChild(loadimg);
+    container.appendChild(loadingp);
+    
+    var addWithLabel = function (container, labeltxt, object) {
+	var p = document.createElement("p");
+	var label = document.createElement("label");
+	label.innerHTML = labeltxt + ":";
+	p.appendChild(label);
+	p.appendChild(object);
+	container.appendChild(p);
+    };
+    var draw = function(minstart, maxstart, entries) { 
+	container.removeChild(loadingp);
+	var inittime = minstart > starttime ? minstart : starttime;
+	
+	var numEntries = 0;
+	var startspan = document.createElement("span");
+	addWithLabel(container, "Start time", startspan);
+	var startinput = document.createElement("input");
+	startinput.type = "hidden";
+	startinput.value = inittime;
+	container.appendChild(startinput);
+	var sliderspan = document.createElement("span");
+	container.appendChild(sliderspan);
+	
+	var countspan = document.createElement("p");
+	countspan.innerHTML = entries + " entries";;
+	container.appendChild(countspan);
+	
+	var windowinput = document.createElement("input");
+	windowinput.type = "text";
+	windowinput.value = period;
+	addWithLabel(container, "Time window (ms)", windowinput);
+	
+	var filterinput = document.createElement("textarea");
+	filterinput.id = "filterinput";
+	filterinput.value = filter;
+	addWithLabel(container, "Filter", filterinput);
+
+	/* done link, when clicked time is updated, */
+	var doneanchor = document.createElement("a");
+	doneanchor.className = "actionbutton";
+	doneanchor.innerHTML = "Done";
+	doneanchor.onclick = function () {
+	    var start = parseInt(startinput.value);
+	    var period = parseInt(windowinput.value);
+	    var filter = filterinput.value;
+	    document.body.removeChild(container);
+	    delete container;
+	    
+	    update(start, period, filter, function() {
+		    callback(start, period, filter, numEntries);
+		});
+	};
+	container.appendChild(doneanchor);
+	
+	var update = function(start, period, filter, thenrun) {
+	    startspan.innerHTML = dateFormat(start, "HH:MM:ss,l");
+	    // get the min and max start time
+	    YUI().use("io-base", function(Y) {
+		    var uri = "/info?start=" + start + "&period=" + period + "&filter=" + filter;
+		    function complete(id, o, args) {
+			var data = eval("(" + o.responseText + ")"); 
+			countspan.innerHTML = data.numEntries + " entries";
+			numEntries = data.numEntries;
+			if (thenrun) {
+			    thenrun();
+			}
+		    };
+		    
+		    Y.on('io:complete', complete, Y, []);
+		    var request = Y.io(uri);
+		});
+	};
+	
+	var updatewindow = function(evt) {		
+	    var start = parseInt(startinput.value);
+	    var period = parseInt(windowinput.value);
+	    var filter = filterinput.value;
+	    update(start, period, filter);
+	};
+	windowinput.onkeyup = updatewindow;
+
+	
+	YUI().use("slider", function (Y) {
+		var input, slider; 
+		
+		function updateInput( e ) {
+		    this.set( "value", e.newVal );
+		    
+		    update(parseInt(startinput.value), parseInt(windowinput.value), filterinput.value);
+		}
+		
+		xSlider = new Y.Slider({min: minstart, max: maxstart, value: inittime, length: "1000px" });
+		
+		// Link the input value to the Slider
+		xInput = Y.one( startinput );
+		xInput.setData( { slider: xSlider } );
+		
+		// Pass the input as the 'this' object inside updateInput
+		xSlider.after( "valueChange", updateInput, xInput );
+		
+		// Render the Slider next to the input
+		xSlider.render(sliderspan);
+	    });
+	update(inittime, windowinput.value, filterinput);
+    };
+    
+    // get the min and max start time
+    YUI().use("io-base", function(Y) {
+	    var uri = "/info";
+	    function complete(id, o, args) {
+		var data = eval("(" + o.responseText + ")"); 
+		draw(data.startTime, data.endTime, data.numEntries);
+	    };
+	    
+	    Y.on('io:complete', complete, Y, []);
+	    var request = Y.io(uri);
+	});
+}

+ 60 - 0
src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/main.html

@@ -0,0 +1,60 @@
+<!--
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+-->
+<html>
+<head>
+<script src="raphael.js"></script>
+<script src="date.format.js"></script>
+<script src="loggraph.js"></script>
+<script src="loggraph.ui.js"></script>
+<script src="loggraph.log.js"></script>
+<script src="loggraph.server.js"></script>
+<script src="loggraph.stats.js"></script>
+<script src="loggraph.session.js"></script>
+
+<script src="g.raphael.js"></script>
+<script src="g.line.js"></script>
+<script src="g.pie.js"></script>
+<script src="g.bar.js"></script>
+
+<script type="text/javascript" src="yui-min.js"></script>
+
+<link rel="stylesheet" type="text/css" href="loggraph.css">
+<script>
+var g;
+function init() {
+     g = new LogGraph("canvas", "status");
+}
+</script>
+</head>
+<body onLoad="init()" class="yui3-skin-sam  yui-skin-sam">
+
+<div id="actions" class="maininterface">
+  <span onclick="javascript:g.editFilters()" class="actionbutton">Edit Filters</span> 
+  <span onclick="g.addLogs()" class="actionbutton">Add logs</span>
+</div>
+<div id="views" class="maininterface">
+  <span onclick="g.showLogs()" class="actionbutton">Log view</span>
+  <span onclick="g.serverGraph()" class="actionbutton">Servers view</span>
+  <span onclick="g.sessionGraph()" class="actionbutton">Sessions view</span> 
+  <span onclick="g.showStats()" class="actionbutton">Statistics</span>
+</div>
+  <div id="status"></div>
+<div id="outercontainer">
+<div id="canvas"></div>
+</div>
+</body>
+</html>

+ 3296 - 0
src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/raphael.js

@@ -0,0 +1,3296 @@
+/*!
+ * Raphael 1.3.2 - JavaScript Vector Library
+ *
+ * Copyright (c) 2009 Dmitry Baranovskiy (http://raphaeljs.com)
+ * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
+ */
+ 
+Raphael = (function () {
+    var separator = /[, ]+/,
+        elements = /^(circle|rect|path|ellipse|text|image)$/,
+        proto = "prototype",
+        has = "hasOwnProperty",
+        doc = document,
+        win = window,
+        oldRaphael = {
+            was: Object[proto][has].call(win, "Raphael"),
+            is: win.Raphael
+        },
+        R = function () {
+            if (R.is(arguments[0], "array")) {
+                var a = arguments[0],
+                    cnv = create[apply](R, a.splice(0, 3 + R.is(a[0], nu))),
+                    res = cnv.set();
+                for (var i = 0, ii = a[length]; i < ii; i++) {
+                    var j = a[i] || {};
+                    elements.test(j.type) && res[push](cnv[j.type]().attr(j));
+                }
+                return res;
+            }
+            return create[apply](R, arguments);
+        },
+        Paper = function () {},
+        appendChild = "appendChild",
+        apply = "apply",
+        concat = "concat",
+        E = "",
+        S = " ",
+        split = "split",
+        events = "click dblclick mousedown mousemove mouseout mouseover mouseup"[split](S),
+        join = "join",
+        length = "length",
+        lowerCase = String[proto].toLowerCase,
+        math = Math,
+        mmax = math.max,
+        mmin = math.min,
+        nu = "number",
+        toString = "toString",
+        objectToString = Object[proto][toString],
+        paper = {},
+        pow = math.pow,
+        push = "push",
+        rg = /^(?=[\da-f]$)/,
+        ISURL = /^url\(['"]?([^\)]+?)['"]?\)$/i, //"
+        colourRegExp = /^\s*((#[a-f\d]{6})|(#[a-f\d]{3})|rgb\(\s*([\d\.]+\s*,\s*[\d\.]+\s*,\s*[\d\.]+)\s*\)|rgb\(\s*([\d\.]+%\s*,\s*[\d\.]+%\s*,\s*[\d\.]+%)\s*\)|hs[bl]\(\s*([\d\.]+\s*,\s*[\d\.]+\s*,\s*[\d\.]+)\s*\)|hs[bl]\(\s*([\d\.]+%\s*,\s*[\d\.]+%\s*,\s*[\d\.]+%)\s*\))\s*$/i,
+        round = math.round,
+        setAttribute = "setAttribute",
+        toFloat = parseFloat,
+        toInt = parseInt,
+        upperCase = String[proto].toUpperCase,
+        availableAttrs = {blur: 0, "clip-rect": "0 0 1e9 1e9", cursor: "default", cx: 0, cy: 0, fill: "#fff", "fill-opacity": 1, font: '10px "Arial"', "font-family": '"Arial"', "font-size": "10", "font-style": "normal", "font-weight": 400, gradient: 0, height: 0, href: "http://raphaeljs.com/", opacity: 1, path: "M0,0", r: 0, rotation: 0, rx: 0, ry: 0, scale: "1 1", src: "", stroke: "#000", "stroke-dasharray": "", "stroke-linecap": "butt", "stroke-linejoin": "butt", "stroke-miterlimit": 0, "stroke-opacity": 1, "stroke-width": 1, target: "_blank", "text-anchor": "middle", title: "Raphael", translation: "0 0", width: 0, x: 0, y: 0},
+        availableAnimAttrs = {along: "along", blur: nu, "clip-rect": "csv", cx: nu, cy: nu, fill: "colour", "fill-opacity": nu, "font-size": nu, height: nu, opacity: nu, path: "path", r: nu, rotation: "csv", rx: nu, ry: nu, scale: "csv", stroke: "colour", "stroke-opacity": nu, "stroke-width": nu, translation: "csv", width: nu, x: nu, y: nu},
+        rp = "replace";
+    R.version = "1.3.2";
+    R.type = (win.SVGAngle || doc.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure", "1.1") ? "SVG" : "VML");
+    if (R.type == "VML") {
+        var d = doc.createElement("div");
+        d.innerHTML = '<!--[if vml]><br><br><![endif]-->';
+        if (d.childNodes[length] != 2) {
+            return R.type = null;
+        }
+        d = null;
+    }
+    R.svg = !(R.vml = R.type == "VML");
+    Paper[proto] = R[proto];
+    R._id = 0;
+    R._oid = 0;
+    R.fn = {};
+    R.is = function (o, type) {
+        type = lowerCase.call(type);
+        return ((type == "object" || type == "undefined") && typeof o == type) || (o == null && type == "null") || lowerCase.call(objectToString.call(o).slice(8, -1)) == type;
+    };
+    R.setWindow = function (newwin) {
+        win = newwin;
+        doc = win.document;
+    };
+    // colour utilities
+    var toHex = function (color) {
+        if (R.vml) {
+            // http://dean.edwards.name/weblog/2009/10/convert-any-colour-value-to-hex-in-msie/
+            var trim = /^\s+|\s+$/g;
+            toHex = cacher(function (color) {
+                var bod;
+                color = (color + E)[rp](trim, E);
+                try {
+                    var docum = new win.ActiveXObject("htmlfile");
+                    docum.write("<body>");
+                    docum.close();
+                    bod = docum.body;
+                } catch(e) {
+                    bod = win.createPopup().document.body;
+                }
+                var range = bod.createTextRange();
+                try {
+                    bod.style.color = color;
+                    var value = range.queryCommandValue("ForeColor");
+                    value = ((value & 255) << 16) | (value & 65280) | ((value & 16711680) >>> 16);
+                    return "#" + ("000000" + value[toString](16)).slice(-6);
+                } catch(e) {
+                    return "none";
+                }
+            });
+        } else {
+            var i = doc.createElement("i");
+            i.title = "Rapha\xebl Colour Picker";
+            i.style.display = "none";
+            doc.body[appendChild](i);
+            toHex = cacher(function (color) {
+                i.style.color = color;
+                return doc.defaultView.getComputedStyle(i, E).getPropertyValue("color");
+            });
+        }
+        return toHex(color);
+    };
+    var hsbtoString = function () {
+        return "hsb(" + [this.h, this.s, this.b] + ")";
+    },
+    rgbtoString = function () {
+        return this.hex;
+    };
+    R.hsb2rgb = cacher(function (hue, saturation, brightness) {
+        if (R.is(hue, "object") && "h" in hue && "s" in hue && "b" in hue) {
+            brightness = hue.b;
+            saturation = hue.s;
+            hue = hue.h;
+        }
+        var red,
+            green,
+            blue;
+        if (brightness == 0) {
+            return {r: 0, g: 0, b: 0, hex: "#000"};
+        }
+        if (hue > 1 || saturation > 1 || brightness > 1) {
+            hue /= 255;
+            saturation /= 255;
+            brightness /= 255;
+        }
+        var i = ~~(hue * 6),
+            f = (hue * 6) - i,
+            p = brightness * (1 - saturation),
+            q = brightness * (1 - (saturation * f)),
+            t = brightness * (1 - (saturation * (1 - f)));
+        red = [brightness, q, p, p, t, brightness, brightness][i];
+        green = [t, brightness, brightness, q, p, p, t][i];
+        blue = [p, p, t, brightness, brightness, q, p][i];
+        red *= 255;
+        green *= 255;
+        blue *= 255;
+        var rgb = {r: red, g: green, b: blue, toString: rgbtoString},
+            r = (~~red)[toString](16),
+            g = (~~green)[toString](16),
+            b = (~~blue)[toString](16);
+        r = r[rp](rg, "0");
+        g = g[rp](rg, "0");
+        b = b[rp](rg, "0");
+        rgb.hex = "#" + r + g + b;
+        return rgb;
+    }, R);
+    R.rgb2hsb = cacher(function (red, green, blue) {
+        if (R.is(red, "object") && "r" in red && "g" in red && "b" in red) {
+            blue = red.b;
+            green = red.g;
+            red = red.r;
+        }
+        if (R.is(red, "string")) {
+            var clr = R.getRGB(red);
+            red = clr.r;
+            green = clr.g;
+            blue = clr.b;
+        }
+        if (red > 1 || green > 1 || blue > 1) {
+            red /= 255;
+            green /= 255;
+            blue /= 255;
+        }
+        var max = mmax(red, green, blue),
+            min = mmin(red, green, blue),
+            hue,
+            saturation,
+            brightness = max;
+        if (min == max) {
+            return {h: 0, s: 0, b: max};
+        } else {
+            var delta = (max - min);
+            saturation = delta / max;
+            if (red == max) {
+                hue = (green - blue) / delta;
+            } else if (green == max) {
+                hue = 2 + ((blue - red) / delta);
+            } else {
+                hue = 4 + ((red - green) / delta);
+            }
+            hue /= 6;
+            hue < 0 && hue++;
+            hue > 1 && hue--;
+        }
+        return {h: hue, s: saturation, b: brightness, toString: hsbtoString};
+    }, R);
+    var p2s = /,?([achlmqrstvxz]),?/gi;
+    R._path2string = function () {
+        return this.join(",")[rp](p2s, "$1");
+    };
+    function cacher(f, scope, postprocessor) {
+        function newf() {
+            var arg = Array[proto].slice.call(arguments, 0),
+                args = arg[join]("\u25ba"),
+                cache = newf.cache = newf.cache || {},
+                count = newf.count = newf.count || [];
+            if (cache[has](args)) {
+                return postprocessor ? postprocessor(cache[args]) : cache[args];
+            }
+            count[length] >= 1e3 && delete cache[count.shift()];
+            count[push](args);
+            cache[args] = f[apply](scope, arg);
+            return postprocessor ? postprocessor(cache[args]) : cache[args];
+        }
+        return newf;
+    }
+ 
+    R.getRGB = cacher(function (colour) {
+        if (!colour || !!((colour = colour + E).indexOf("-") + 1)) {
+            return {r: -1, g: -1, b: -1, hex: "none", error: 1};
+        }
+        if (colour == "none") {
+            return {r: -1, g: -1, b: -1, hex: "none"};
+        }
+        !(({hs: 1, rg: 1})[has](colour.substring(0, 2)) || colour.charAt() == "#") && (colour = toHex(colour));
+        var res,
+            red,
+            green,
+            blue,
+            t,
+            rgb = colour.match(colourRegExp);
+        if (rgb) {
+            if (rgb[2]) {
+                blue = toInt(rgb[2].substring(5), 16);
+                green = toInt(rgb[2].substring(3, 5), 16);
+                red = toInt(rgb[2].substring(1, 3), 16);
+            }
+            if (rgb[3]) {
+                blue = toInt((t = rgb[3].charAt(3)) + t, 16);
+                green = toInt((t = rgb[3].charAt(2)) + t, 16);
+                red = toInt((t = rgb[3].charAt(1)) + t, 16);
+            }
+            if (rgb[4]) {
+                rgb = rgb[4][split](/\s*,\s*/);
+                red = toFloat(rgb[0]);
+                green = toFloat(rgb[1]);
+                blue = toFloat(rgb[2]);
+            }
+            if (rgb[5]) {
+                rgb = rgb[5][split](/\s*,\s*/);
+                red = toFloat(rgb[0]) * 2.55;
+                green = toFloat(rgb[1]) * 2.55;
+                blue = toFloat(rgb[2]) * 2.55;
+            }
+            if (rgb[6]) {
+                rgb = rgb[6][split](/\s*,\s*/);
+                red = toFloat(rgb[0]);
+                green = toFloat(rgb[1]);
+                blue = toFloat(rgb[2]);
+                return R.hsb2rgb(red, green, blue);
+            }
+            if (rgb[7]) {
+                rgb = rgb[7][split](/\s*,\s*/);
+                red = toFloat(rgb[0]) * 2.55;
+                green = toFloat(rgb[1]) * 2.55;
+                blue = toFloat(rgb[2]) * 2.55;
+                return R.hsb2rgb(red, green, blue);
+            }
+            rgb = {r: red, g: green, b: blue};
+            var r = (~~red)[toString](16),
+                g = (~~green)[toString](16),
+                b = (~~blue)[toString](16);
+            r = r[rp](rg, "0");
+            g = g[rp](rg, "0");
+            b = b[rp](rg, "0");
+            rgb.hex = "#" + r + g + b;
+            return rgb;
+        }
+        return {r: -1, g: -1, b: -1, hex: "none", error: 1};
+    }, R);
+    R.getColor = function (value) {
+        var start = this.getColor.start = this.getColor.start || {h: 0, s: 1, b: value || .75},
+            rgb = this.hsb2rgb(start.h, start.s, start.b);
+        start.h += .075;
+        if (start.h > 1) {
+            start.h = 0;
+            start.s -= .2;
+            start.s <= 0 && (this.getColor.start = {h: 0, s: 1, b: start.b});
+        }
+        return rgb.hex;
+    };
+    R.getColor.reset = function () {
+        delete this.start;
+    };
+    // path utilities
+    var pathCommand = /([achlmqstvz])[\s,]*((-?\d*\.?\d*(?:e[-+]?\d+)?\s*,?\s*)+)/ig,
+        pathValues = /(-?\d*\.?\d*(?:e[-+]?\d+)?)\s*,?\s*/ig;
+    R.parsePathString = cacher(function (pathString) {
+        if (!pathString) {
+            return null;
+        }
+        var paramCounts = {a: 7, c: 6, h: 1, l: 2, m: 2, q: 4, s: 4, t: 2, v: 1, z: 0},
+            data = [];
+        if (R.is(pathString, "array") && R.is(pathString[0], "array")) { // rough assumption
+            data = pathClone(pathString);
+        }
+        if (!data[length]) {
+            (pathString + E)[rp](pathCommand, function (a, b, c) {
+                var params = [],
+                    name = lowerCase.call(b);
+                c[rp](pathValues, function (a, b) {
+                    b && params[push](+b);
+                });
+                if (name == "m" && params[length] > 2) {
+                    data[push]([b][concat](params.splice(0, 2)));
+                    name = "l";
+                    b = b == "m" ? "l" : "L";
+                }
+                while (params[length] >= paramCounts[name]) {
+                    data[push]([b][concat](params.splice(0, paramCounts[name])));
+                    if (!paramCounts[name]) {
+                        break;
+                    }
+                }
+            });
+        }
+        data[toString] = R._path2string;
+        return data;
+    });
+    R.findDotsAtSegment = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t) {
+        var t1 = 1 - t,
+            x = pow(t1, 3) * p1x + pow(t1, 2) * 3 * t * c1x + t1 * 3 * t * t * c2x + pow(t, 3) * p2x,
+            y = pow(t1, 3) * p1y + pow(t1, 2) * 3 * t * c1y + t1 * 3 * t * t * c2y + pow(t, 3) * p2y,
+            mx = p1x + 2 * t * (c1x - p1x) + t * t * (c2x - 2 * c1x + p1x),
+            my = p1y + 2 * t * (c1y - p1y) + t * t * (c2y - 2 * c1y + p1y),
+            nx = c1x + 2 * t * (c2x - c1x) + t * t * (p2x - 2 * c2x + c1x),
+            ny = c1y + 2 * t * (c2y - c1y) + t * t * (p2y - 2 * c2y + c1y),
+            ax = (1 - t) * p1x + t * c1x,
+            ay = (1 - t) * p1y + t * c1y,
+            cx = (1 - t) * c2x + t * p2x,
+            cy = (1 - t) * c2y + t * p2y,
+            alpha = (90 - math.atan((mx - nx) / (my - ny)) * 180 / math.PI);
+        (mx > nx || my < ny) && (alpha += 180);
+        return {x: x, y: y, m: {x: mx, y: my}, n: {x: nx, y: ny}, start: {x: ax, y: ay}, end: {x: cx, y: cy}, alpha: alpha};
+    };
+    var pathDimensions = cacher(function (path) {
+        if (!path) {
+            return {x: 0, y: 0, width: 0, height: 0};
+        }
+        path = path2curve(path);
+        var x = 0, 
+            y = 0,
+            X = [],
+            Y = [],
+            p;
+        for (var i = 0, ii = path[length]; i < ii; i++) {
+            p = path[i];
+            if (p[0] == "M") {
+                x = p[1];
+                y = p[2];
+                X[push](x);
+                Y[push](y);
+            } else {
+                var dim = curveDim(x, y, p[1], p[2], p[3], p[4], p[5], p[6]);
+                X = X[concat](dim.min.x, dim.max.x);
+                Y = Y[concat](dim.min.y, dim.max.y);
+                x = p[5];
+                y = p[6];
+            }
+        }
+        var xmin = mmin[apply](0, X),
+            ymin = mmin[apply](0, Y);
+        return {
+            x: xmin,
+            y: ymin,
+            width: mmax[apply](0, X) - xmin,
+            height: mmax[apply](0, Y) - ymin
+        };
+    }),
+        pathClone = function (pathArray) {
+            var res = [];
+            if (!R.is(pathArray, "array") || !R.is(pathArray && pathArray[0], "array")) { // rough assumption
+                pathArray = R.parsePathString(pathArray);
+            }
+            for (var i = 0, ii = pathArray[length]; i < ii; i++) {
+                res[i] = [];
+                for (var j = 0, jj = pathArray[i][length]; j < jj; j++) {
+                    res[i][j] = pathArray[i][j];
+                }
+            }
+            res[toString] = R._path2string;
+            return res;
+        },
+        pathToRelative = cacher(function (pathArray) {
+            if (!R.is(pathArray, "array") || !R.is(pathArray && pathArray[0], "array")) { // rough assumption
+                pathArray = R.parsePathString(pathArray);
+            }
+            var res = [],
+                x = 0,
+                y = 0,
+                mx = 0,
+                my = 0,
+                start = 0;
+            if (pathArray[0][0] == "M") {
+                x = pathArray[0][1];
+                y = pathArray[0][2];
+                mx = x;
+                my = y;
+                start++;
+                res[push](["M", x, y]);
+            }
+            for (var i = start, ii = pathArray[length]; i < ii; i++) {
+                var r = res[i] = [],
+                    pa = pathArray[i];
+                if (pa[0] != lowerCase.call(pa[0])) {
+                    r[0] = lowerCase.call(pa[0]);
+                    switch (r[0]) {
+                        case "a":
+                            r[1] = pa[1];
+                            r[2] = pa[2];
+                            r[3] = pa[3];
+                            r[4] = pa[4];
+                            r[5] = pa[5];
+                            r[6] = +(pa[6] - x).toFixed(3);
+                            r[7] = +(pa[7] - y).toFixed(3);
+                            break;
+                        case "v":
+                            r[1] = +(pa[1] - y).toFixed(3);
+                            break;
+                        case "m":
+                            mx = pa[1];
+                            my = pa[2];
+                        default:
+                            for (var j = 1, jj = pa[length]; j < jj; j++) {
+                                r[j] = +(pa[j] - ((j % 2) ? x : y)).toFixed(3);
+                            }
+                    }
+                } else {
+                    r = res[i] = [];
+                    if (pa[0] == "m") {
+                        mx = pa[1] + x;
+                        my = pa[2] + y;
+                    }
+                    for (var k = 0, kk = pa[length]; k < kk; k++) {
+                        res[i][k] = pa[k];
+                    }
+                }
+                var len = res[i][length];
+                switch (res[i][0]) {
+                    case "z":
+                        x = mx;
+                        y = my;
+                        break;
+                    case "h":
+                        x += +res[i][len - 1];
+                        break;
+                    case "v":
+                        y += +res[i][len - 1];
+                        break;
+                    default:
+                        x += +res[i][len - 2];
+                        y += +res[i][len - 1];
+                }
+            }
+            res[toString] = R._path2string;
+            return res;
+        }, 0, pathClone),
+        pathToAbsolute = cacher(function (pathArray) {
+            if (!R.is(pathArray, "array") || !R.is(pathArray && pathArray[0], "array")) { // rough assumption
+                pathArray = R.parsePathString(pathArray);
+            }
+            var res = [],
+                x = 0,
+                y = 0,
+                mx = 0,
+                my = 0,
+                start = 0;
+            if (pathArray[0][0] == "M") {
+                x = +pathArray[0][1];
+                y = +pathArray[0][2];
+                mx = x;
+                my = y;
+                start++;
+                res[0] = ["M", x, y];
+            }
+            for (var i = start, ii = pathArray[length]; i < ii; i++) {
+                var r = res[i] = [],
+                    pa = pathArray[i];
+                if (pa[0] != upperCase.call(pa[0])) {
+                    r[0] = upperCase.call(pa[0]);
+                    switch (r[0]) {
+                        case "A":
+                            r[1] = pa[1];
+                            r[2] = pa[2];
+                            r[3] = pa[3];
+                            r[4] = pa[4];
+                            r[5] = pa[5];
+                            r[6] = +(pa[6] + x);
+                            r[7] = +(pa[7] + y);
+                            break;
+                        case "V":
+                            r[1] = +pa[1] + y;
+                            break;
+                        case "H":
+                            r[1] = +pa[1] + x;
+                            break;
+                        case "M":
+                            mx = +pa[1] + x;
+                            my = +pa[2] + y;
+                        default:
+                            for (var j = 1, jj = pa[length]; j < jj; j++) {
+                                r[j] = +pa[j] + ((j % 2) ? x : y);
+                            }
+                    }
+                } else {
+                    for (var k = 0, kk = pa[length]; k < kk; k++) {
+                        res[i][k] = pa[k];
+                    }
+                }
+                switch (r[0]) {
+                    case "Z":
+                        x = mx;
+                        y = my;
+                        break;
+                    case "H":
+                        x = r[1];
+                        break;
+                    case "V":
+                        y = r[1];
+                        break;
+                    default:
+                        x = res[i][res[i][length] - 2];
+                        y = res[i][res[i][length] - 1];
+                }
+            }
+            res[toString] = R._path2string;
+            return res;
+        }, null, pathClone),
+        l2c = function (x1, y1, x2, y2) {
+            return [x1, y1, x2, y2, x2, y2];
+        },
+        q2c = function (x1, y1, ax, ay, x2, y2) {
+            var _13 = 1 / 3,
+                _23 = 2 / 3;
+            return [
+                    _13 * x1 + _23 * ax,
+                    _13 * y1 + _23 * ay,
+                    _13 * x2 + _23 * ax,
+                    _13 * y2 + _23 * ay,
+                    x2,
+                    y2
+                ];
+        },
+        a2c = function (x1, y1, rx, ry, angle, large_arc_flag, sweep_flag, x2, y2, recursive) {
+            // for more information of where this math came from visit:
+            // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
+            var PI = math.PI,
+                _120 = PI * 120 / 180,
+                rad = PI / 180 * (+angle || 0),
+                res = [],
+                xy,
+                rotate = cacher(function (x, y, rad) {
+                    var X = x * math.cos(rad) - y * math.sin(rad),
+                        Y = x * math.sin(rad) + y * math.cos(rad);
+                    return {x: X, y: Y};
+                });
+            if (!recursive) {
+                xy = rotate(x1, y1, -rad);
+                x1 = xy.x;
+                y1 = xy.y;
+                xy = rotate(x2, y2, -rad);
+                x2 = xy.x;
+                y2 = xy.y;
+                var cos = math.cos(PI / 180 * angle),
+                    sin = math.sin(PI / 180 * angle),
+                    x = (x1 - x2) / 2,
+                    y = (y1 - y2) / 2;
+                // rx = mmax(rx, math.abs(x));
+                // ry = mmax(ry, math.abs(y));
+                var h = (x * x) / (rx * rx) + (y * y) / (ry * ry);
+                if (h > 1) {
+                    h = math.sqrt(h);
+                    rx = h * rx;
+                    ry = h * ry;
+                }
+                var rx2 = rx * rx,
+                    ry2 = ry * ry,
+                    k = (large_arc_flag == sweep_flag ? -1 : 1) *
+                        math.sqrt(math.abs((rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x))),
+                    cx = k * rx * y / ry + (x1 + x2) / 2,
+                    cy = k * -ry * x / rx + (y1 + y2) / 2,
+                    f1 = math.asin(((y1 - cy) / ry).toFixed(7)),
+                    f2 = math.asin(((y2 - cy) / ry).toFixed(7));
+
+                f1 = x1 < cx ? PI - f1 : f1;
+                f2 = x2 < cx ? PI - f2 : f2;
+                f1 < 0 && (f1 = PI * 2 + f1);
+                f2 < 0 && (f2 = PI * 2 + f2);
+                if (sweep_flag && f1 > f2) {
+                    f1 = f1 - PI * 2;
+                }
+                if (!sweep_flag && f2 > f1) {
+                    f2 = f2 - PI * 2;
+                }
+            } else {
+                f1 = recursive[0];
+                f2 = recursive[1];
+                cx = recursive[2];
+                cy = recursive[3];
+            }
+            var df = f2 - f1;
+            if (math.abs(df) > _120) {
+                var f2old = f2,
+                    x2old = x2,
+                    y2old = y2;
+                f2 = f1 + _120 * (sweep_flag && f2 > f1 ? 1 : -1);
+                x2 = cx + rx * math.cos(f2);
+                y2 = cy + ry * math.sin(f2);
+                res = a2c(x2, y2, rx, ry, angle, 0, sweep_flag, x2old, y2old, [f2, f2old, cx, cy]);
+            }
+            df = f2 - f1;
+            var c1 = math.cos(f1),
+                s1 = math.sin(f1),
+                c2 = math.cos(f2),
+                s2 = math.sin(f2),
+                t = math.tan(df / 4),
+                hx = 4 / 3 * rx * t,
+                hy = 4 / 3 * ry * t,
+                m1 = [x1, y1],
+                m2 = [x1 + hx * s1, y1 - hy * c1],
+                m3 = [x2 + hx * s2, y2 - hy * c2],
+                m4 = [x2, y2];
+            m2[0] = 2 * m1[0] - m2[0];
+            m2[1] = 2 * m1[1] - m2[1];
+            if (recursive) {
+                return [m2, m3, m4][concat](res);
+            } else {
+                res = [m2, m3, m4][concat](res)[join]()[split](",");
+                var newres = [];
+                for (var i = 0, ii = res[length]; i < ii; i++) {
+                    newres[i] = i % 2 ? rotate(res[i - 1], res[i], rad).y : rotate(res[i], res[i + 1], rad).x;
+                }
+                // alert(newres);
+                return newres;
+            }
+        },
+        findDotAtSegment = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t) {
+            var t1 = 1 - t;
+            return {
+                x: pow(t1, 3) * p1x + pow(t1, 2) * 3 * t * c1x + t1 * 3 * t * t * c2x + pow(t, 3) * p2x,
+                y: pow(t1, 3) * p1y + pow(t1, 2) * 3 * t * c1y + t1 * 3 * t * t * c2y + pow(t, 3) * p2y
+            };
+        },
+        curveDim = cacher(function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) {
+            var a = (c2x - 2 * c1x + p1x) - (p2x - 2 * c2x + c1x),
+                b = 2 * (c1x - p1x) - 2 * (c2x - c1x),
+                c = p1x - c1x,
+                t1 = (-b + math.sqrt(b * b - 4 * a * c)) / 2 / a,
+                t2 = (-b - math.sqrt(b * b - 4 * a * c)) / 2 / a,
+                y = [p1y, p2y],
+                x = [p1x, p2x],
+                dot;
+            math.abs(t1) > 1e12 && (t1 = .5);
+            math.abs(t2) > 1e12 && (t2 = .5);
+            if (t1 > 0 && t1 < 1) {
+                dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t1);
+                x[push](dot.x);
+                y[push](dot.y);
+            }
+            if (t2 > 0 && t2 < 1) {
+                dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t2);
+                x[push](dot.x);
+                y[push](dot.y);
+            }
+            a = (c2y - 2 * c1y + p1y) - (p2y - 2 * c2y + c1y);
+            b = 2 * (c1y - p1y) - 2 * (c2y - c1y);
+            c = p1y - c1y;
+            t1 = (-b + math.sqrt(b * b - 4 * a * c)) / 2 / a;
+            t2 = (-b - math.sqrt(b * b - 4 * a * c)) / 2 / a;
+            math.abs(t1) > 1e12 && (t1 = .5);
+            math.abs(t2) > 1e12 && (t2 = .5);
+            if (t1 > 0 && t1 < 1) {
+                dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t1);
+                x[push](dot.x);
+                y[push](dot.y);
+            }
+            if (t2 > 0 && t2 < 1) {
+                dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t2);
+                x[push](dot.x);
+                y[push](dot.y);
+            }
+            return {
+                min: {x: mmin[apply](0, x), y: mmin[apply](0, y)},
+                max: {x: mmax[apply](0, x), y: mmax[apply](0, y)}
+            };
+        }),
+        path2curve = cacher(function (path, path2) {
+            var p = pathToAbsolute(path),
+                p2 = path2 && pathToAbsolute(path2),
+                attrs = {x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null},
+                attrs2 = {x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null},
+                processPath = function (path, d) {
+                    var nx, ny;
+                    if (!path) {
+                        return ["C", d.x, d.y, d.x, d.y, d.x, d.y];
+                    }
+                    !(path[0] in {T:1, Q:1}) && (d.qx = d.qy = null);
+                    switch (path[0]) {
+                        case "M":
+                            d.X = path[1];
+                            d.Y = path[2];
+                            break;
+                        case "A":
+                            path = ["C"][concat](a2c[apply](0, [d.x, d.y][concat](path.slice(1))));
+                            break;
+                        case "S":
+                            nx = d.x + (d.x - (d.bx || d.x));
+                            ny = d.y + (d.y - (d.by || d.y));
+                            path = ["C", nx, ny][concat](path.slice(1));
+                            break;
+                        case "T":
+                            d.qx = d.x + (d.x - (d.qx || d.x));
+                            d.qy = d.y + (d.y - (d.qy || d.y));
+                            path = ["C"][concat](q2c(d.x, d.y, d.qx, d.qy, path[1], path[2]));
+                            break;
+                        case "Q":
+                            d.qx = path[1];
+                            d.qy = path[2];
+                            path = ["C"][concat](q2c(d.x, d.y, path[1], path[2], path[3], path[4]));
+                            break;
+                        case "L":
+                            path = ["C"][concat](l2c(d.x, d.y, path[1], path[2]));
+                            break;
+                        case "H":
+                            path = ["C"][concat](l2c(d.x, d.y, path[1], d.y));
+                            break;
+                        case "V":
+                            path = ["C"][concat](l2c(d.x, d.y, d.x, path[1]));
+                            break;
+                        case "Z":
+                            path = ["C"][concat](l2c(d.x, d.y, d.X, d.Y));
+                            break;
+                    }
+                    return path;
+                },
+                fixArc = function (pp, i) {
+                    if (pp[i][length] > 7) {
+                        pp[i].shift();
+                        var pi = pp[i];
+                        while (pi[length]) {
+                            pp.splice(i++, 0, ["C"][concat](pi.splice(0, 6)));
+                        }
+                        pp.splice(i, 1);
+                        ii = mmax(p[length], p2 && p2[length] || 0);
+                    }
+                },
+                fixM = function (path1, path2, a1, a2, i) {
+                    if (path1 && path2 && path1[i][0] == "M" && path2[i][0] != "M") {
+                        path2.splice(i, 0, ["M", a2.x, a2.y]);
+                        a1.bx = 0;
+                        a1.by = 0;
+                        a1.x = path1[i][1];
+                        a1.y = path1[i][2];
+                        ii = mmax(p[length], p2 && p2[length] || 0);
+                    }
+                };
+            for (var i = 0, ii = mmax(p[length], p2 && p2[length] || 0); i < ii; i++) {
+                p[i] = processPath(p[i], attrs);
+                fixArc(p, i);
+                p2 && (p2[i] = processPath(p2[i], attrs2));
+                p2 && fixArc(p2, i);
+                fixM(p, p2, attrs, attrs2, i);
+                fixM(p2, p, attrs2, attrs, i);
+                var seg = p[i],
+                    seg2 = p2 && p2[i],
+                    seglen = seg[length],
+                    seg2len = p2 && seg2[length];
+                attrs.x = seg[seglen - 2];
+                attrs.y = seg[seglen - 1];
+                attrs.bx = toFloat(seg[seglen - 4]) || attrs.x;
+                attrs.by = toFloat(seg[seglen - 3]) || attrs.y;
+                attrs2.bx = p2 && (toFloat(seg2[seg2len - 4]) || attrs2.x);
+                attrs2.by = p2 && (toFloat(seg2[seg2len - 3]) || attrs2.y);
+                attrs2.x = p2 && seg2[seg2len - 2];
+                attrs2.y = p2 && seg2[seg2len - 1];
+            }
+            return p2 ? [p, p2] : p;
+        }, null, pathClone),
+        parseDots = cacher(function (gradient) {
+            var dots = [];
+            for (var i = 0, ii = gradient[length]; i < ii; i++) {
+                var dot = {},
+                    par = gradient[i].match(/^([^:]*):?([\d\.]*)/);
+                dot.color = R.getRGB(par[1]);
+                if (dot.color.error) {
+                    return null;
+                }
+                dot.color = dot.color.hex;
+                par[2] && (dot.offset = par[2] + "%");
+                dots[push](dot);
+            }
+            for (i = 1, ii = dots[length] - 1; i < ii; i++) {
+                if (!dots[i].offset) {
+                    var start = toFloat(dots[i - 1].offset || 0),
+                        end = 0;
+                    for (var j = i + 1; j < ii; j++) {
+                        if (dots[j].offset) {
+                            end = dots[j].offset;
+                            break;
+                        }
+                    }
+                    if (!end) {
+                        end = 100;
+                        j = ii;
+                    }
+                    end = toFloat(end);
+                    var d = (end - start) / (j - i + 1);
+                    for (; i < j; i++) {
+                        start += d;
+                        dots[i].offset = start + "%";
+                    }
+                }
+            }
+            return dots;
+        }),
+        getContainer = function (x, y, w, h) {
+            var container;
+            if (R.is(x, "string") || R.is(x, "object")) {
+                container = R.is(x, "string") ? doc.getElementById(x) : x;
+                if (container.tagName) {
+                    if (y == null) {
+                        return {
+                            container: container,
+                            width: container.style.pixelWidth || container.offsetWidth,
+                            height: container.style.pixelHeight || container.offsetHeight
+                        };
+                    } else {
+                        return {container: container, width: y, height: w};
+                    }
+                }
+            } else if (R.is(x, nu) && h != null) {
+                return {container: 1, x: x, y: y, width: w, height: h};
+            }
+        },
+        plugins = function (con, add) {
+            var that = this;
+            for (var prop in add) {
+                if (add[has](prop) && !(prop in con)) {
+                    switch (typeof add[prop]) {
+                        case "function":
+                            (function (f) {
+                                con[prop] = con === that ? f : function () { return f[apply](that, arguments); };
+                            })(add[prop]);
+                        break;
+                        case "object":
+                            con[prop] = con[prop] || {};
+                            plugins.call(this, con[prop], add[prop]);
+                        break;
+                        default:
+                            con[prop] = add[prop];
+                        break;
+                    }
+                }
+            }
+        },
+        tear = function (el, paper) {
+            el == paper.top && (paper.top = el.prev);
+            el == paper.bottom && (paper.bottom = el.next);
+            el.next && (el.next.prev = el.prev);
+            el.prev && (el.prev.next = el.next);
+        },
+        tofront = function (el, paper) {
+            if (paper.top === el) {
+                return;
+            }
+            tear(el, paper);
+            el.next = null;
+            el.prev = paper.top;
+            paper.top.next = el;
+            paper.top = el;
+        },
+        toback = function (el, paper) {
+            if (paper.bottom === el) {
+                return;
+            }
+            tear(el, paper);
+            el.next = paper.bottom;
+            el.prev = null;
+            paper.bottom.prev = el;
+            paper.bottom = el;
+        },
+        insertafter = function (el, el2, paper) {
+            tear(el, paper);
+            el2 == paper.top && (paper.top = el);
+            el2.next && (el2.next.prev = el);
+            el.next = el2.next;
+            el.prev = el2;
+            el2.next = el;
+        },
+        insertbefore = function (el, el2, paper) {
+            tear(el, paper);
+            el2 == paper.bottom && (paper.bottom = el);
+            el2.prev && (el2.prev.next = el);
+            el.prev = el2.prev;
+            el2.prev = el;
+            el.next = el2;
+        },
+        removed = function (methodname) {
+            return function () {
+                throw new Error("Rapha\xebl: you are calling to method \u201c" + methodname + "\u201d of removed object");
+            };
+        },
+        radial_gradient = /^r(?:\(([^,]+?)\s*,\s*([^\)]+?)\))?/;
+ 
+    // SVG
+    if (R.svg) {
+        Paper[proto].svgns = "http://www.w3.org/2000/svg";
+        Paper[proto].xlink = "http://www.w3.org/1999/xlink";
+        round = function (num) {
+            return +num + (~~num === num) * .5;
+        };
+        var roundPath = function (path) {
+            for (var i = 0, ii = path[length]; i < ii; i++) {
+                if (lowerCase.call(path[i][0]) != "a") {
+                    for (var j = 1, jj = path[i][length]; j < jj; j++) {
+                        path[i][j] = round(path[i][j]);
+                    }
+                } else {
+                    path[i][6] = round(path[i][6]);
+                    path[i][7] = round(path[i][7]);
+                }
+            }
+            return path;
+        },
+        $ = function (el, attr) {
+            if (attr) {
+                for (var key in attr) {
+                    if (attr[has](key)) {
+                        el[setAttribute](key, attr[key] + E);
+                    }
+                }
+            } else {
+                return doc.createElementNS(Paper[proto].svgns, el);
+            }
+        };
+        R[toString] = function () {
+            return  "Your browser supports SVG.\nYou are running Rapha\xebl " + this.version;
+        };
+        var thePath = function (pathString, SVG) {
+            var el = $("path");
+            SVG.canvas && SVG.canvas[appendChild](el);
+            var p = new Element(el, SVG);
+            p.type = "path";
+            setFillAndStroke(p, {fill: "none", stroke: "#000", path: pathString});
+            return p;
+        };
+        var addGradientFill = function (o, gradient, SVG) {
+            var type = "linear",
+                fx = .5, fy = .5,
+                s = o.style;
+            gradient = (gradient + E)[rp](radial_gradient, function (all, _fx, _fy) {
+                type = "radial";
+                if (_fx && _fy) {
+                    fx = toFloat(_fx);
+                    fy = toFloat(_fy);
+                    var dir = ((fy > .5) * 2 - 1);
+                    pow(fx - .5, 2) + pow(fy - .5, 2) > .25 &&
+                        (fy = math.sqrt(.25 - pow(fx - .5, 2)) * dir + .5) &&
+                        fy != .5 &&
+                        (fy = fy.toFixed(5) - 1e-5 * dir);
+                }
+                return E;
+            });
+            gradient = gradient[split](/\s*\-\s*/);
+            if (type == "linear") {
+                var angle = gradient.shift();
+                angle = -toFloat(angle);
+                if (isNaN(angle)) {
+                    return null;
+                }
+                var vector = [0, 0, math.cos(angle * math.PI / 180), math.sin(angle * math.PI / 180)],
+                    max = 1 / (mmax(math.abs(vector[2]), math.abs(vector[3])) || 1);
+                vector[2] *= max;
+                vector[3] *= max;
+                if (vector[2] < 0) {
+                    vector[0] = -vector[2];
+                    vector[2] = 0;
+                }
+                if (vector[3] < 0) {
+                    vector[1] = -vector[3];
+                    vector[3] = 0;
+                }
+            }
+            var dots = parseDots(gradient);
+            if (!dots) {
+                return null;
+            }
+            var id = o.getAttribute("fill");
+            id = id.match(/^url\(#(.*)\)$/);
+            id && SVG.defs.removeChild(doc.getElementById(id[1]));
+            
+            var el = $(type + "Gradient");
+            el.id = "r" + (R._id++)[toString](36);
+            $(el, type == "radial" ? {fx: fx, fy: fy} : {x1: vector[0], y1: vector[1], x2: vector[2], y2: vector[3]});
+            SVG.defs[appendChild](el);
+            for (var i = 0, ii = dots[length]; i < ii; i++) {
+                var stop = $("stop");
+                $(stop, {
+                    offset: dots[i].offset ? dots[i].offset : !i ? "0%" : "100%",
+                    "stop-color": dots[i].color || "#fff"
+                });
+                el[appendChild](stop);
+            }
+            $(o, {
+                fill: "url(#" + el.id + ")",
+                opacity: 1,
+                "fill-opacity": 1
+            });
+            s.fill = E;
+            s.opacity = 1;
+            s.fillOpacity = 1;
+            return 1;
+        };
+        var updatePosition = function (o) {
+            var bbox = o.getBBox();
+            $(o.pattern, {patternTransform: R.format("translate({0},{1})", bbox.x, bbox.y)});
+        };
+        var setFillAndStroke = function (o, params) {
+            var dasharray = {
+                    "": [0],
+                    "none": [0],
+                    "-": [3, 1],
+                    ".": [1, 1],
+                    "-.": [3, 1, 1, 1],
+                    "-..": [3, 1, 1, 1, 1, 1],
+                    ". ": [1, 3],
+                    "- ": [4, 3],
+                    "--": [8, 3],
+                    "- .": [4, 3, 1, 3],
+                    "--.": [8, 3, 1, 3],
+                    "--..": [8, 3, 1, 3, 1, 3]
+                },
+                node = o.node,
+                attrs = o.attrs,
+                rot = o.rotate(),
+                addDashes = function (o, value) {
+                    value = dasharray[lowerCase.call(value)];
+                    if (value) {
+                        var width = o.attrs["stroke-width"] || "1",
+                            butt = {round: width, square: width, butt: 0}[o.attrs["stroke-linecap"] || params["stroke-linecap"]] || 0,
+                            dashes = [];
+                        var i = value[length];
+                        while (i--) {
+                            dashes[i] = value[i] * width + ((i % 2) ? 1 : -1) * butt;
+                        }
+                        $(node, {"stroke-dasharray": dashes[join](",")});
+                    }
+                };
+            params[has]("rotation") && (rot = params.rotation);
+            var rotxy = (rot + E)[split](separator);
+            if (!(rotxy.length - 1)) {
+                rotxy = null;
+            } else {
+                rotxy[1] = +rotxy[1];
+                rotxy[2] = +rotxy[2];
+            }
+            toFloat(rot) && o.rotate(0, true);
+            for (var att in params) {
+                if (params[has](att)) {
+                    if (!availableAttrs[has](att)) {
+                        continue;
+                    }
+                    var value = params[att];
+                    attrs[att] = value;
+                    switch (att) {
+                        case "blur":
+                            o.blur(value);
+                            break;
+                        case "rotation":
+                            o.rotate(value, true);
+                            break;
+                        // Hyperlink
+                        case "href":
+                        case "title":
+                        case "target":
+                            var pn = node.parentNode;
+                            if (lowerCase.call(pn.tagName) != "a") {
+                                var hl = $("a");
+                                pn.insertBefore(hl, node);
+                                hl[appendChild](node);
+                                pn = hl;
+                            }
+                            pn.setAttributeNS(o.paper.xlink, att, value);
+                            break;
+                        case "cursor":
+                            node.style.cursor = value;
+                            break;
+                        case "clip-rect":
+                            var rect = (value + E)[split](separator);
+                            if (rect[length] == 4) {
+                                o.clip && o.clip.parentNode.parentNode.removeChild(o.clip.parentNode);
+                                var el = $("clipPath"),
+                                    rc = $("rect");
+                                el.id = "r" + (R._id++)[toString](36);
+                                $(rc, {
+                                    x: rect[0],
+                                    y: rect[1],
+                                    width: rect[2],
+                                    height: rect[3]
+                                });
+                                el[appendChild](rc);
+                                o.paper.defs[appendChild](el);
+                                $(node, {"clip-path": "url(#" + el.id + ")"});
+                                o.clip = rc;
+                            }
+                            if (!value) {
+                                var clip = doc.getElementById(node.getAttribute("clip-path")[rp](/(^url\(#|\)$)/g, E));
+                                clip && clip.parentNode.removeChild(clip);
+                                $(node, {"clip-path": E});
+                                delete o.clip;
+                            }
+                        break;
+                        case "path":
+                            if (o.type == "path") {
+                                $(node, {d: value ? attrs.path = roundPath(pathToAbsolute(value)) : "M0,0"});
+                            }
+                            break;
+                        case "width":
+                            node[setAttribute](att, value);
+                            if (attrs.fx) {
+                                att = "x";
+                                value = attrs.x;
+                            } else {
+                                break;
+                            }
+                        case "x":
+                            if (attrs.fx) {
+                                value = -attrs.x - (attrs.width || 0);
+                            }
+                        case "rx":
+                            if (att == "rx" && o.type == "rect") {
+                                break;
+                            }
+                        case "cx":
+                            rotxy && (att == "x" || att == "cx") && (rotxy[1] += value - attrs[att]);
+                            node[setAttribute](att, round(value));
+                            o.pattern && updatePosition(o);
+                            break;
+                        case "height":
+                            node[setAttribute](att, value);
+                            if (attrs.fy) {
+                                att = "y";
+                                value = attrs.y;
+                            } else {
+                                break;
+                            }
+                        case "y":
+                            if (attrs.fy) {
+                                value = -attrs.y - (attrs.height || 0);
+                            }
+                        case "ry":
+                            if (att == "ry" && o.type == "rect") {
+                                break;
+                            }
+                        case "cy":
+                            rotxy && (att == "y" || att == "cy") && (rotxy[2] += value - attrs[att]);
+                            node[setAttribute](att, round(value));
+                            o.pattern && updatePosition(o);
+                            break;
+                        case "r":
+                            if (o.type == "rect") {
+                                $(node, {rx: value, ry: value});
+                            } else {
+                                node[setAttribute](att, value);
+                            }
+                            break;
+                        case "src":
+                            if (o.type == "image") {
+                                node.setAttributeNS(o.paper.xlink, "href", value);
+                            }
+                            break;
+                        case "stroke-width":
+                            node.style.strokeWidth = value;
+                            // Need following line for Firefox
+                            node[setAttribute](att, value);
+                            if (attrs["stroke-dasharray"]) {
+                                addDashes(o, attrs["stroke-dasharray"]);
+                            }
+                            break;
+                        case "stroke-dasharray":
+                            addDashes(o, value);
+                            break;
+                        case "translation":
+                            var xy = (value + E)[split](separator);
+                            xy[0] = +xy[0] || 0;
+                            xy[1] = +xy[1] || 0;
+                            if (rotxy) {
+                                rotxy[1] += xy[0];
+                                rotxy[2] += xy[1];
+                            }
+                            translate.call(o, xy[0], xy[1]);
+                            break;
+                        case "scale":
+                            xy = (value + E)[split](separator);
+                            o.scale(+xy[0] || 1, +xy[1] || +xy[0] || 1, isNaN(toFloat(xy[2])) ? null : +xy[2], isNaN(toFloat(xy[3])) ? null : +xy[3]);
+                            break;
+                        case "fill":
+                            var isURL = (value + E).match(ISURL);
+                            if (isURL) {
+                                el = $("pattern");
+                                var ig = $("image");
+                                el.id = "r" + (R._id++)[toString](36);
+                                $(el, {x: 0, y: 0, patternUnits: "userSpaceOnUse", height: 1, width: 1});
+                                $(ig, {x: 0, y: 0});
+                                ig.setAttributeNS(o.paper.xlink, "href", isURL[1]);
+                                el[appendChild](ig);
+ 
+                                var img = doc.createElement("img");
+                                img.style.cssText = "position:absolute;left:-9999em;top-9999em";
+                                img.onload = function () {
+                                    $(el, {width: this.offsetWidth, height: this.offsetHeight});
+                                    $(ig, {width: this.offsetWidth, height: this.offsetHeight});
+                                    doc.body.removeChild(this);
+                                    o.paper.safari();
+                                };
+                                doc.body[appendChild](img);
+                                img.src = isURL[1];
+                                o.paper.defs[appendChild](el);
+                                node.style.fill = "url(#" + el.id + ")";
+                                $(node, {fill: "url(#" + el.id + ")"});
+                                o.pattern = el;
+                                o.pattern && updatePosition(o);
+                                break;
+                            }
+                            if (!R.getRGB(value).error) {
+                                delete params.gradient;
+                                delete attrs.gradient;
+                                !R.is(attrs.opacity, "undefined") &&
+                                    R.is(params.opacity, "undefined") &&
+                                    $(node, {opacity: attrs.opacity});
+                                !R.is(attrs["fill-opacity"], "undefined") &&
+                                    R.is(params["fill-opacity"], "undefined") &&
+                                    $(node, {"fill-opacity": attrs["fill-opacity"]});
+                            } else if ((({circle: 1, ellipse: 1})[has](o.type) || (value + E).charAt() != "r") && addGradientFill(node, value, o.paper)) {
+                                attrs.gradient = value;
+                                attrs.fill = "none";
+                                break;
+                            }
+                        case "stroke":
+                            node[setAttribute](att, R.getRGB(value).hex);
+                            break;
+                        case "gradient":
+                            (({circle: 1, ellipse: 1})[has](o.type) || (value + E).charAt() != "r") && addGradientFill(node, value, o.paper);
+                            break;
+                        case "opacity":
+                        case "fill-opacity":
+                            if (attrs.gradient) {
+                                var gradient = doc.getElementById(node.getAttribute("fill")[rp](/^url\(#|\)$/g, E));
+                                if (gradient) {
+                                    var stops = gradient.getElementsByTagName("stop");
+                                    stops[stops[length] - 1][setAttribute]("stop-opacity", value);
+                                }
+                                break;
+                            }
+                        default:
+                            att == "font-size" && (value = toInt(value, 10) + "px");
+                            var cssrule = att[rp](/(\-.)/g, function (w) {
+                                return upperCase.call(w.substring(1));
+                            });
+                            node.style[cssrule] = value;
+                            // Need following line for Firefox
+                            node[setAttribute](att, value);
+                            break;
+                    }
+                }
+            }
+            
+            tuneText(o, params);
+            if (rotxy) {
+                o.rotate(rotxy.join(S));
+            } else {
+                toFloat(rot) && o.rotate(rot, true);
+            }
+        };
+        var leading = 1.2,
+        tuneText = function (el, params) {
+            if (el.type != "text" || !(params[has]("text") || params[has]("font") || params[has]("font-size") || params[has]("x") || params[has]("y"))) {
+                return;
+            }
+            var a = el.attrs,
+                node = el.node,
+                fontSize = node.firstChild ? toInt(doc.defaultView.getComputedStyle(node.firstChild, E).getPropertyValue("font-size"), 10) : 10;
+ 
+            if (params[has]("text")) {
+                a.text = params.text;
+                while (node.firstChild) {
+                    node.removeChild(node.firstChild);
+                }
+                var texts = (params.text + E)[split]("\n");
+                for (var i = 0, ii = texts[length]; i < ii; i++) if (texts[i]) {
+                    var tspan = $("tspan");
+                    i && $(tspan, {dy: fontSize * leading, x: a.x});
+                    tspan[appendChild](doc.createTextNode(texts[i]));
+                    node[appendChild](tspan);
+                }
+            } else {
+                texts = node.getElementsByTagName("tspan");
+                for (i = 0, ii = texts[length]; i < ii; i++) {
+                    i && $(texts[i], {dy: fontSize * leading, x: a.x});
+                }
+            }
+            $(node, {y: a.y});
+            var bb = el.getBBox(),
+                dif = a.y - (bb.y + bb.height / 2);
+            dif && isFinite(dif) && $(node, {y: a.y + dif});
+        },
+        Element = function (node, svg) {
+            var X = 0,
+                Y = 0;
+            this[0] = node;
+            this.id = R._oid++;
+            this.node = node;
+            node.raphael = this;
+            this.paper = svg;
+            this.attrs = this.attrs || {};
+            this.transformations = []; // rotate, translate, scale
+            this._ = {
+                tx: 0,
+                ty: 0,
+                rt: {deg: 0, cx: 0, cy: 0},
+                sx: 1,
+                sy: 1
+            };
+            !svg.bottom && (svg.bottom = this);
+            this.prev = svg.top;
+            svg.top && (svg.top.next = this);
+            svg.top = this;
+            this.next = null;
+        };
+        Element[proto].rotate = function (deg, cx, cy) {
+            if (this.removed) {
+                return this;
+            }
+            if (deg == null) {
+                if (this._.rt.cx) {
+                    return [this._.rt.deg, this._.rt.cx, this._.rt.cy][join](S);
+                }
+                return this._.rt.deg;
+            }
+            var bbox = this.getBBox();
+            deg = (deg + E)[split](separator);
+            if (deg[length] - 1) {
+                cx = toFloat(deg[1]);
+                cy = toFloat(deg[2]);
+            }
+            deg = toFloat(deg[0]);
+            if (cx != null) {
+                this._.rt.deg = deg;
+            } else {
+                this._.rt.deg += deg;
+            }
+            (cy == null) && (cx = null);
+            this._.rt.cx = cx;
+            this._.rt.cy = cy;
+            cx = cx == null ? bbox.x + bbox.width / 2 : cx;
+            cy = cy == null ? bbox.y + bbox.height / 2 : cy;
+            if (this._.rt.deg) {
+                this.transformations[0] = R.format("rotate({0} {1} {2})", this._.rt.deg, cx, cy);
+                this.clip && $(this.clip, {transform: R.format("rotate({0} {1} {2})", -this._.rt.deg, cx, cy)});
+            } else {
+                this.transformations[0] = E;
+                this.clip && $(this.clip, {transform: E});
+            }
+            $(this.node, {transform: this.transformations[join](S)});
+            return this;
+        };
+        Element[proto].hide = function () {
+            !this.removed && (this.node.style.display = "none");
+            return this;
+        };
+        Element[proto].show = function () {
+            !this.removed && (this.node.style.display = "");
+            return this;
+        };
+        Element[proto].remove = function () {
+            if (this.removed) {
+                return;
+            }
+            tear(this, this.paper);
+            this.node.parentNode.removeChild(this.node);
+            for (var i in this) {
+                delete this[i];
+            }
+            this.removed = true;
+        };
+        Element[proto].getBBox = function () {
+            if (this.removed) {
+                return this;
+            }
+            if (this.type == "path") {
+                return pathDimensions(this.attrs.path);
+            }
+            if (this.node.style.display == "none") {
+                this.show();
+                var hide = true;
+            }
+            var bbox = {};
+            try {
+                bbox = this.node.getBBox();
+            } catch(e) {
+                // Firefox 3.0.x plays badly here
+            } finally {
+                bbox = bbox || {};
+            }
+            if (this.type == "text") {
+                bbox = {x: bbox.x, y: Infinity, width: 0, height: 0};
+                for (var i = 0, ii = this.node.getNumberOfChars(); i < ii; i++) {
+                    var bb = this.node.getExtentOfChar(i);
+                    (bb.y < bbox.y) && (bbox.y = bb.y);
+                    (bb.y + bb.height - bbox.y > bbox.height) && (bbox.height = bb.y + bb.height - bbox.y);
+                    (bb.x + bb.width - bbox.x > bbox.width) && (bbox.width = bb.x + bb.width - bbox.x);
+                }
+            }
+            hide && this.hide();
+            return bbox;
+        };
+        Element[proto].attr = function (name, value) {
+            if (this.removed) {
+                return this;
+            }
+            if (name == null) {
+                var res = {};
+                for (var i in this.attrs) if (this.attrs[has](i)) {
+                    res[i] = this.attrs[i];
+                }
+                this._.rt.deg && (res.rotation = this.rotate());
+                (this._.sx != 1 || this._.sy != 1) && (res.scale = this.scale());
+                res.gradient && res.fill == "none" && (res.fill = res.gradient) && delete res.gradient;
+                return res;
+            }
+            if (value == null && R.is(name, "string")) {
+                if (name == "translation") {
+                    return translate.call(this);
+                }
+                if (name == "rotation") {
+                    return this.rotate();
+                }
+                if (name == "scale") {
+                    return this.scale();
+                }
+                if (name == "fill" && this.attrs.fill == "none" && this.attrs.gradient) {
+                    return this.attrs.gradient;
+                }
+                return this.attrs[name];
+            }
+            if (value == null && R.is(name, "array")) {
+                var values = {};
+                for (var j = 0, jj = name.length; j < jj; j++) {
+                    values[name[j]] = this.attr(name[j]);
+                }
+                return values;
+            }
+            if (value != null) {
+                var params = {};
+                params[name] = value;
+                setFillAndStroke(this, params);
+            } else if (name != null && R.is(name, "object")) {
+                setFillAndStroke(this, name);
+            }
+            return this;
+        };
+        Element[proto].toFront = function () {
+            if (this.removed) {
+                return this;
+            }
+            this.node.parentNode[appendChild](this.node);
+            var svg = this.paper;
+            svg.top != this && tofront(this, svg);
+            return this;
+        };
+        Element[proto].toBack = function () {
+            if (this.removed) {
+                return this;
+            }
+            if (this.node.parentNode.firstChild != this.node) {
+                this.node.parentNode.insertBefore(this.node, this.node.parentNode.firstChild);
+                toback(this, this.paper);
+                var svg = this.paper;
+            }
+            return this;
+        };
+        Element[proto].insertAfter = function (element) {
+            if (this.removed) {
+                return this;
+            }
+            var node = element.node;
+            if (node.nextSibling) {
+                node.parentNode.insertBefore(this.node, node.nextSibling);
+            } else {
+                node.parentNode[appendChild](this.node);
+            }
+            insertafter(this, element, this.paper);
+            return this;
+        };
+        Element[proto].insertBefore = function (element) {
+            if (this.removed) {
+                return this;
+            }
+            var node = element.node;
+            node.parentNode.insertBefore(this.node, node);
+            insertbefore(this, element, this.paper);
+            return this;
+        };
+        Element[proto].blur = function (size) {
+            // Experimental. No Safari support. Use it on your own risk.
+            var t = this;
+            if (+size !== 0) {
+                var fltr = $("filter"),
+                    blur = $("feGaussianBlur");
+                t.attrs.blur = size;
+                fltr.id = "r" + (R._id++)[toString](36);
+                $(blur, {stdDeviation: +size || 1.5});
+                fltr.appendChild(blur);
+                t.paper.defs.appendChild(fltr);
+                t._blur = fltr;
+                $(t.node, {filter: "url(#" + fltr.id + ")"});
+            } else {
+                if (t._blur) {
+                    t._blur.parentNode.removeChild(t._blur);
+                    delete t._blur;
+                    delete t.attrs.blur;
+                }
+                t.node.removeAttribute("filter");
+            }
+        };
+        var theCircle = function (svg, x, y, r) {
+            x = round(x);
+            y = round(y);
+            var el = $("circle");
+            svg.canvas && svg.canvas[appendChild](el);
+            var res = new Element(el, svg);
+            res.attrs = {cx: x, cy: y, r: r, fill: "none", stroke: "#000"};
+            res.type = "circle";
+            $(el, res.attrs);
+            return res;
+        };
+        var theRect = function (svg, x, y, w, h, r) {
+            x = round(x);
+            y = round(y);
+            var el = $("rect");
+            svg.canvas && svg.canvas[appendChild](el);
+            var res = new Element(el, svg);
+            res.attrs = {x: x, y: y, width: w, height: h, r: r || 0, rx: r || 0, ry: r || 0, fill: "none", stroke: "#000"};
+            res.type = "rect";
+            $(el, res.attrs);
+            return res;
+        };
+        var theEllipse = function (svg, x, y, rx, ry) {
+            x = round(x);
+            y = round(y);
+            var el = $("ellipse");
+            svg.canvas && svg.canvas[appendChild](el);
+            var res = new Element(el, svg);
+            res.attrs = {cx: x, cy: y, rx: rx, ry: ry, fill: "none", stroke: "#000"};
+            res.type = "ellipse";
+            $(el, res.attrs);
+            return res;
+        };
+        var theImage = function (svg, src, x, y, w, h) {
+            var el = $("image");
+            $(el, {x: x, y: y, width: w, height: h, preserveAspectRatio: "none"});
+            el.setAttributeNS(svg.xlink, "href", src);
+            svg.canvas && svg.canvas[appendChild](el);
+            var res = new Element(el, svg);
+            res.attrs = {x: x, y: y, width: w, height: h, src: src};
+            res.type = "image";
+            return res;
+        };
+        var theText = function (svg, x, y, text) {
+            var el = $("text");
+            $(el, {x: x, y: y, "text-anchor": "middle"});
+            svg.canvas && svg.canvas[appendChild](el);
+            var res = new Element(el, svg);
+            res.attrs = {x: x, y: y, "text-anchor": "middle", text: text, font: availableAttrs.font, stroke: "none", fill: "#000"};
+            res.type = "text";
+            setFillAndStroke(res, res.attrs);
+            return res;
+        };
+        var setSize = function (width, height) {
+            this.width = width || this.width;
+            this.height = height || this.height;
+            this.canvas[setAttribute]("width", this.width);
+            this.canvas[setAttribute]("height", this.height);
+            return this;
+        };
+        var create = function () {
+            var con = getContainer[apply](0, arguments),
+                container = con && con.container,
+                x = con.x,
+                y = con.y,
+                width = con.width,
+                height = con.height;
+            if (!container) {
+                throw new Error("SVG container not found.");
+            }
+            var cnvs = $("svg");
+            width = width || 512;
+            height = height || 342;
+            $(cnvs, {
+                xmlns: "http://www.w3.org/2000/svg",
+                version: 1.1,
+                width: width,
+                height: height
+            });
+            if (container == 1) {
+                cnvs.style.cssText = "position:absolute;left:" + x + "px;top:" + y + "px";
+                doc.body[appendChild](cnvs);
+            } else {
+                if (container.firstChild) {
+                    container.insertBefore(cnvs, container.firstChild);
+                } else {
+                    container[appendChild](cnvs);
+                }
+            }
+            container = new Paper;
+            container.width = width;
+            container.height = height;
+            container.canvas = cnvs;
+            plugins.call(container, container, R.fn);
+            container.clear();
+            return container;
+        };
+        Paper[proto].clear = function () {
+            var c = this.canvas;
+            while (c.firstChild) {
+                c.removeChild(c.firstChild);
+            }
+            this.bottom = this.top = null;
+            (this.desc = $("desc"))[appendChild](doc.createTextNode("Created with Rapha\xebl"));
+            c[appendChild](this.desc);
+            c[appendChild](this.defs = $("defs"));
+        };
+        Paper[proto].remove = function () {
+            this.canvas.parentNode && this.canvas.parentNode.removeChild(this.canvas);
+            for (var i in this) {
+                this[i] = removed(i);
+            }
+        };
+    }
+
+    // VML
+    if (R.vml) {
+        var map = {M: "m", L: "l", C: "c", Z: "x", m: "t", l: "r", c: "v", z: "x"},
+            bites = /([clmz]),?([^clmz]*)/gi,
+            val = /-?[^,\s-]+/g,
+            coordsize = 1e3 + S + 1e3,
+            zoom = 10,
+            path2vml = function (path) {
+                var total =  /[ahqstv]/ig,
+                    command = pathToAbsolute;
+                (path + E).match(total) && (command = path2curve);
+                total = /[clmz]/g;
+                if (command == pathToAbsolute && !(path + E).match(total)) {
+                    var res = (path + E)[rp](bites, function (all, command, args) {
+                        var vals = [],
+                            isMove = lowerCase.call(command) == "m",
+                            res = map[command];
+                        args[rp](val, function (value) {
+                            if (isMove && vals[length] == 2) {
+                                res += vals + map[command == "m" ? "l" : "L"];
+                                vals = [];
+                            }
+                            vals[push](round(value * zoom));
+                        });
+                        return res + vals;
+                    });
+                    return res;
+                }
+                var pa = command(path), p, r;
+                res = [];
+                for (var i = 0, ii = pa[length]; i < ii; i++) {
+                    p = pa[i];
+                    r = lowerCase.call(pa[i][0]);
+                    r == "z" && (r = "x");
+                    for (var j = 1, jj = p[length]; j < jj; j++) {
+                        r += round(p[j] * zoom) + (j != jj - 1 ? "," : E);
+                    }
+                    res[push](r);
+                }
+                return res[join](S);
+            };
+        
+        R[toString] = function () {
+            return  "Your browser doesn\u2019t support SVG. Falling down to VML.\nYou are running Rapha\xebl " + this.version;
+        };
+        thePath = function (pathString, vml) {
+            var g = createNode("group");
+            g.style.cssText = "position:absolute;left:0;top:0;width:" + vml.width + "px;height:" + vml.height + "px";
+            g.coordsize = vml.coordsize;
+            g.coordorigin = vml.coordorigin;
+            var el = createNode("shape"), ol = el.style;
+            ol.width = vml.width + "px";
+            ol.height = vml.height + "px";
+            el.coordsize = coordsize;
+            el.coordorigin = vml.coordorigin;
+            g[appendChild](el);
+            var p = new Element(el, g, vml),
+                attr = {fill: "none", stroke: "#000"};
+            pathString && (attr.path = pathString);
+            p.isAbsolute = true;
+            p.type = "path";
+            p.path = [];
+            p.Path = E;
+            setFillAndStroke(p, attr);
+            vml.canvas[appendChild](g);
+            return p;
+        };
+        setFillAndStroke = function (o, params) {
+            o.attrs = o.attrs || {};
+            var node = o.node,
+                a = o.attrs,
+                s = node.style,
+                xy,
+                res = o;
+            for (var par in params) if (params[has](par)) {
+                a[par] = params[par];
+            }
+            params.href && (node.href = params.href);
+            params.title && (node.title = params.title);
+            params.target && (node.target = params.target);
+            params.cursor && (s.cursor = params.cursor);
+            "blur" in params && o.blur(params.blur);
+            if (params.path && o.type == "path") {
+                a.path = params.path;
+                node.path = path2vml(a.path);
+            }
+            if (params.rotation != null) {
+                o.rotate(params.rotation, true);
+            }
+            if (params.translation) {
+                xy = (params.translation + E)[split](separator);
+                translate.call(o, xy[0], xy[1]);
+                if (o._.rt.cx != null) {
+                    o._.rt.cx +=+ xy[0];
+                    o._.rt.cy +=+ xy[1];
+                    o.setBox(o.attrs, xy[0], xy[1]);
+                }
+            }
+            if (params.scale) {
+                xy = (params.scale + E)[split](separator);
+                o.scale(+xy[0] || 1, +xy[1] || +xy[0] || 1, +xy[2] || null, +xy[3] || null);
+            }
+            if ("clip-rect" in params) {
+                var rect = (params["clip-rect"] + E)[split](separator);
+                if (rect[length] == 4) {
+                    rect[2] = +rect[2] + (+rect[0]);
+                    rect[3] = +rect[3] + (+rect[1]);
+                    var div = node.clipRect || doc.createElement("div"),
+                        dstyle = div.style,
+                        group = node.parentNode;
+                    dstyle.clip = R.format("rect({1}px {2}px {3}px {0}px)", rect);
+                    if (!node.clipRect) {
+                        dstyle.position = "absolute";
+                        dstyle.top = 0;
+                        dstyle.left = 0;
+                        dstyle.width = o.paper.width + "px";
+                        dstyle.height = o.paper.height + "px";
+                        group.parentNode.insertBefore(div, group);
+                        div[appendChild](group);
+                        node.clipRect = div;
+                    }
+                }
+                if (!params["clip-rect"]) {
+                    node.clipRect && (node.clipRect.style.clip = E);
+                }
+            }
+            if (o.type == "image" && params.src) {
+                node.src = params.src;
+            }
+            if (o.type == "image" && params.opacity) {
+                node.filterOpacity = " progid:DXImageTransform.Microsoft.Alpha(opacity=" + (params.opacity * 100) + ")";
+                s.filter = (node.filterMatrix || E) + (node.filterOpacity || E);
+            }
+            params.font && (s.font = params.font);
+            params["font-family"] && (s.fontFamily = '"' + params["font-family"][split](",")[0][rp](/^['"]+|['"]+$/g, E) + '"'); //'
+            params["font-size"] && (s.fontSize = params["font-size"]);
+            params["font-weight"] && (s.fontWeight = params["font-weight"]);
+            params["font-style"] && (s.fontStyle = params["font-style"]);
+            if (params.opacity != null || 
+                params["stroke-width"] != null ||
+                params.fill != null ||
+                params.stroke != null ||
+                params["stroke-width"] != null ||
+                params["stroke-opacity"] != null ||
+                params["fill-opacity"] != null ||
+                params["stroke-dasharray"] != null ||
+                params["stroke-miterlimit"] != null ||
+                params["stroke-linejoin"] != null ||
+                params["stroke-linecap"] != null) {
+                node = o.shape || node;
+                var fill = (node.getElementsByTagName("fill") && node.getElementsByTagName("fill")[0]),
+                    newfill = false;
+                !fill && (newfill = fill = createNode("fill"));
+                if ("fill-opacity" in params || "opacity" in params) {
+                    var opacity = ((+a["fill-opacity"] + 1 || 2) - 1) * ((+a.opacity + 1 || 2) - 1);
+                    opacity < 0 && (opacity = 0);
+                    opacity > 1 && (opacity = 1);
+                    fill.opacity = opacity;
+                }
+                params.fill && (fill.on = true);
+                if (fill.on == null || params.fill == "none") {
+                    fill.on = false;
+                }
+                if (fill.on && params.fill) {
+                    var isURL = params.fill.match(ISURL);
+                    if (isURL) {
+                        fill.src = isURL[1];
+                        fill.type = "tile";
+                    } else {
+                        fill.color = R.getRGB(params.fill).hex;
+                        fill.src = E;
+                        fill.type = "solid";
+                        if (R.getRGB(params.fill).error && (res.type in {circle: 1, ellipse: 1} || (params.fill + E).charAt() != "r") && addGradientFill(res, params.fill)) {
+                            a.fill = "none";
+                            a.gradient = params.fill;
+                        }
+                    }
+                }
+                newfill && node[appendChild](fill);
+                var stroke = (node.getElementsByTagName("stroke") && node.getElementsByTagName("stroke")[0]),
+                newstroke = false;
+                !stroke && (newstroke = stroke = createNode("stroke"));
+                if ((params.stroke && params.stroke != "none") ||
+                    params["stroke-width"] ||
+                    params["stroke-opacity"] != null ||
+                    params["stroke-dasharray"] ||
+                    params["stroke-miterlimit"] ||
+                    params["stroke-linejoin"] ||
+                    params["stroke-linecap"]) {
+                    stroke.on = true;
+                }
+                (params.stroke == "none" || stroke.on == null || params.stroke == 0 || params["stroke-width"] == 0) && (stroke.on = false);
+                stroke.on && params.stroke && (stroke.color = R.getRGB(params.stroke).hex);
+                opacity = ((+a["stroke-opacity"] + 1 || 2) - 1) * ((+a.opacity + 1 || 2) - 1);
+                var width = (toFloat(params["stroke-width"]) || 1) * .75;
+                opacity < 0 && (opacity = 0);
+                opacity > 1 && (opacity = 1);
+                params["stroke-width"] == null && (width = a["stroke-width"]);
+                params["stroke-width"] && (stroke.weight = width);
+                width && width < 1 && (opacity *= width) && (stroke.weight = 1);
+                stroke.opacity = opacity;
+                
+                params["stroke-linejoin"] && (stroke.joinstyle = params["stroke-linejoin"] || "miter");
+                stroke.miterlimit = params["stroke-miterlimit"] || 8;
+                params["stroke-linecap"] && (stroke.endcap = params["stroke-linecap"] == "butt" ? "flat" : params["stroke-linecap"] == "square" ? "square" : "round");
+                if (params["stroke-dasharray"]) {
+                    var dasharray = {
+                        "-": "shortdash",
+                        ".": "shortdot",
+                        "-.": "shortdashdot",
+                        "-..": "shortdashdotdot",
+                        ". ": "dot",
+                        "- ": "dash",
+                        "--": "longdash",
+                        "- .": "dashdot",
+                        "--.": "longdashdot",
+                        "--..": "longdashdotdot"
+                    };
+                    stroke.dashstyle = dasharray[has](params["stroke-dasharray"]) ? dasharray[params["stroke-dasharray"]] : E;
+                }
+                newstroke && node[appendChild](stroke);
+            }
+            if (res.type == "text") {
+                s = res.paper.span.style;
+                a.font && (s.font = a.font);
+                a["font-family"] && (s.fontFamily = a["font-family"]);
+                a["font-size"] && (s.fontSize = a["font-size"]);
+                a["font-weight"] && (s.fontWeight = a["font-weight"]);
+                a["font-style"] && (s.fontStyle = a["font-style"]);
+                res.node.string && (res.paper.span.innerHTML = (res.node.string + E)[rp](/</g, "&#60;")[rp](/&/g, "&#38;")[rp](/\n/g, "<br>"));
+                res.W = a.w = res.paper.span.offsetWidth;
+                res.H = a.h = res.paper.span.offsetHeight;
+                res.X = a.x;
+                res.Y = a.y + round(res.H / 2);
+ 
+                // text-anchor emulationm
+                switch (a["text-anchor"]) {
+                    case "start":
+                        res.node.style["v-text-align"] = "left";
+                        res.bbx = round(res.W / 2);
+                    break;
+                    case "end":
+                        res.node.style["v-text-align"] = "right";
+                        res.bbx = -round(res.W / 2);
+                    break;
+                    default:
+                        res.node.style["v-text-align"] = "center";
+                    break;
+                }
+            }
+        };
+        addGradientFill = function (o, gradient) {
+            o.attrs = o.attrs || {};
+            var attrs = o.attrs,
+                fill = o.node.getElementsByTagName("fill"),
+                type = "linear",
+                fxfy = ".5 .5";
+            o.attrs.gradient = gradient;
+            gradient = (gradient + E)[rp](radial_gradient, function (all, fx, fy) {
+                type = "radial";
+                if (fx && fy) {
+                    fx = toFloat(fx);
+                    fy = toFloat(fy);
+                    pow(fx - .5, 2) + pow(fy - .5, 2) > .25 && (fy = math.sqrt(.25 - pow(fx - .5, 2)) * ((fy > .5) * 2 - 1) + .5);
+                    fxfy = fx + S + fy;
+                }
+                return E;
+            });
+            gradient = gradient[split](/\s*\-\s*/);
+            if (type == "linear") {
+                var angle = gradient.shift();
+                angle = -toFloat(angle);
+                if (isNaN(angle)) {
+                    return null;
+                }
+            }
+            var dots = parseDots(gradient);
+            if (!dots) {
+                return null;
+            }
+            o = o.shape || o.node;
+            fill = fill[0] || createNode("fill");
+            if (dots[length]) {
+                fill.on = true;
+                fill.method = "none";
+                fill.type = (type == "radial") ? "gradientradial" : "gradient";
+                fill.color = dots[0].color;
+                fill.color2 = dots[dots[length] - 1].color;
+                var clrs = [];
+                for (var i = 0, ii = dots[length]; i < ii; i++) {
+                    dots[i].offset && clrs[push](dots[i].offset + S + dots[i].color);
+                }
+                fill.colors && (fill.colors.value = clrs[length] ? clrs[join](",") : "0% " + fill.color);
+                if (type == "radial") {
+                    fill.focus = "100%";
+                    fill.focussize = fxfy;
+                    fill.focusposition = fxfy;
+                } else {
+                    fill.angle = (270 - angle) % 360;
+                }
+            }
+            return 1;
+        };
+        Element = function (node, group, vml) {
+            var Rotation = 0,
+                RotX = 0,
+                RotY = 0,
+                Scale = 1;
+            this[0] = node;
+            this.id = R._oid++;
+            this.node = node;
+            node.raphael = this;
+            this.X = 0;
+            this.Y = 0;
+            this.attrs = {};
+            this.Group = group;
+            this.paper = vml;
+            this._ = {
+                tx: 0,
+                ty: 0,
+                rt: {deg:0},
+                sx: 1,
+                sy: 1
+            };
+            !vml.bottom && (vml.bottom = this);
+            this.prev = vml.top;
+            vml.top && (vml.top.next = this);
+            vml.top = this;
+            this.next = null;
+        };
+        Element[proto].rotate = function (deg, cx, cy) {
+            if (this.removed) {
+                return this;
+            }
+            if (deg == null) {
+                if (this._.rt.cx) {
+                    return [this._.rt.deg, this._.rt.cx, this._.rt.cy][join](S);
+                }
+                return this._.rt.deg;
+            }
+            deg = (deg + E)[split](separator);
+            if (deg[length] - 1) {
+                cx = toFloat(deg[1]);
+                cy = toFloat(deg[2]);
+            }
+            deg = toFloat(deg[0]);
+            if (cx != null) {
+                this._.rt.deg = deg;
+            } else {
+                this._.rt.deg += deg;
+            }
+            cy == null && (cx = null);
+            this._.rt.cx = cx;
+            this._.rt.cy = cy;
+            this.setBox(this.attrs, cx, cy);
+            this.Group.style.rotation = this._.rt.deg;
+            // gradient fix for rotation. TODO
+            // var fill = (this.shape || this.node).getElementsByTagName("fill");
+            // fill = fill[0] || {};
+            // var b = ((360 - this._.rt.deg) - 270) % 360;
+            // !R.is(fill.angle, "undefined") && (fill.angle = b);
+            return this;
+        };
+        Element[proto].setBox = function (params, cx, cy) {
+            if (this.removed) {
+                return this;
+            }
+            var gs = this.Group.style,
+                os = (this.shape && this.shape.style) || this.node.style;
+            params = params || {};
+            for (var i in params) if (params[has](i)) {
+                this.attrs[i] = params[i];
+            }
+            cx = cx || this._.rt.cx;
+            cy = cy || this._.rt.cy;
+            var attr = this.attrs,
+                x,
+                y,
+                w,
+                h;
+            switch (this.type) {
+                case "circle":
+                    x = attr.cx - attr.r;
+                    y = attr.cy - attr.r;
+                    w = h = attr.r * 2;
+                    break;
+                case "ellipse":
+                    x = attr.cx - attr.rx;
+                    y = attr.cy - attr.ry;
+                    w = attr.rx * 2;
+                    h = attr.ry * 2;
+                    break;
+                case "rect":
+                case "image":
+                    x = +attr.x;
+                    y = +attr.y;
+                    w = attr.width || 0;
+                    h = attr.height || 0;
+                    break;
+                case "text":
+                    this.textpath.v = ["m", round(attr.x), ", ", round(attr.y - 2), "l", round(attr.x) + 1, ", ", round(attr.y - 2)][join](E);
+                    x = attr.x - round(this.W / 2);
+                    y = attr.y - this.H / 2;
+                    w = this.W;
+                    h = this.H;
+                    break;
+                case "path":
+                    if (!this.attrs.path) {
+                        x = 0;
+                        y = 0;
+                        w = this.paper.width;
+                        h = this.paper.height;
+                    } else {
+                        var dim = pathDimensions(this.attrs.path);
+                        x = dim.x;
+                        y = dim.y;
+                        w = dim.width;
+                        h = dim.height;
+                    }
+                    break;
+                default:
+                    x = 0;
+                    y = 0;
+                    w = this.paper.width;
+                    h = this.paper.height;
+                    break;
+            }
+            cx = (cx == null) ? x + w / 2 : cx;
+            cy = (cy == null) ? y + h / 2 : cy;
+            var left = cx - this.paper.width / 2,
+                top = cy - this.paper.height / 2, t;
+            gs.left != (t = left + "px") && (gs.left = t);
+            gs.top != (t = top + "px") && (gs.top = t);
+            this.X = this.type == "path" ? -left : x;
+            this.Y = this.type == "path" ? -top : y;
+            this.W = w;
+            this.H = h;
+            if (this.type == "path") {
+                os.left != (t = -left * zoom + "px") && (os.left = t);
+                os.top != (t = -top * zoom + "px") && (os.top = t);
+            } else if (this.type == "text") {
+                os.left != (t = -left + "px") && (os.left = t);
+                os.top != (t = -top + "px") && (os.top = t);
+            } else {
+                gs.width != (t = this.paper.width + "px") && (gs.width = t);
+                gs.height != (t = this.paper.height + "px") && (gs.height = t);
+                os.left != (t = x - left + "px") && (os.left = t);
+                os.top != (t = y - top + "px") && (os.top = t);
+                os.width != (t = w + "px") && (os.width = t);
+                os.height != (t = h + "px") && (os.height = t);
+                var arcsize = (+params.r || 0) / mmin(w, h);
+                if (this.type == "rect" && this.arcsize.toFixed(4) != arcsize.toFixed(4) && (arcsize || this.arcsize)) {
+                    // We should replace element with the new one
+                    var o = createNode("roundrect"),
+                        a = {},
+                        ii = this.events && this.events[length];
+                    i = 0;
+                    o.arcsize = arcsize;
+                    o.raphael = this;
+                    this.Group[appendChild](o);
+                    this.Group.removeChild(this.node);
+                    this[0] = this.node = o;
+                    this.arcsize = arcsize;
+                    for (i in attr) {
+                        a[i] = attr[i];
+                    }
+                    delete a.scale;
+                    this.attr(a);
+                    if (this.events) for (; i < ii; i++) {
+                        this.events[i].unbind = addEvent(this.node, this.events[i].name, this.events[i].f, this);
+                    }
+                }
+            }
+        };
+        Element[proto].hide = function () {
+            !this.removed && (this.Group.style.display = "none");
+            return this;
+        };
+        Element[proto].show = function () {
+            !this.removed && (this.Group.style.display = "block");
+            return this;
+        };
+        Element[proto].getBBox = function () {
+            if (this.removed) {
+                return this;
+            }
+            if (this.type == "path") {
+                return pathDimensions(this.attrs.path);
+            }
+            return {
+                x: this.X + (this.bbx || 0),
+                y: this.Y,
+                width: this.W,
+                height: this.H
+            };
+        };
+        Element[proto].remove = function () {
+            if (this.removed) {
+                return;
+            }
+            tear(this, this.paper);
+            this.node.parentNode.removeChild(this.node);
+            this.Group.parentNode.removeChild(this.Group);
+            this.shape && this.shape.parentNode.removeChild(this.shape);
+            for (var i in this) {
+                delete this[i];
+            }
+            this.removed = true;
+        };
+        Element[proto].attr = function (name, value) {
+            if (this.removed) {
+                return this;
+            }
+            if (name == null) {
+                var res = {};
+                for (var i in this.attrs) if (this.attrs[has](i)) {
+                    res[i] = this.attrs[i];
+                }
+                this._.rt.deg && (res.rotation = this.rotate());
+                (this._.sx != 1 || this._.sy != 1) && (res.scale = this.scale());
+                res.gradient && res.fill == "none" && (res.fill = res.gradient) && delete res.gradient;
+                return res;
+            }
+            if (value == null && R.is(name, "string")) {
+                if (name == "translation") {
+                    return translate.call(this);
+                }
+                if (name == "rotation") {
+                    return this.rotate();
+                }
+                if (name == "scale") {
+                    return this.scale();
+                }
+                if (name == "fill" && this.attrs.fill == "none" && this.attrs.gradient) {
+                    return this.attrs.gradient;
+                }
+                return this.attrs[name];
+            }
+            if (this.attrs && value == null && R.is(name, "array")) {
+                var ii, values = {};
+                for (i = 0, ii = name[length]; i < ii; i++) {
+                    values[name[i]] = this.attr(name[i]);
+                }
+                return values;
+            }
+            var params;
+            if (value != null) {
+                params = {};
+                params[name] = value;
+            }
+            value == null && R.is(name, "object") && (params = name);
+            if (params) {
+                if (params.text && this.type == "text") {
+                    this.node.string = params.text;
+                }
+                setFillAndStroke(this, params);
+                if (params.gradient && (({circle: 1, ellipse: 1})[has](this.type) || (params.gradient + E).charAt() != "r")) {
+                    addGradientFill(this, params.gradient);
+                }
+                (this.type != "path" || this._.rt.deg) && this.setBox(this.attrs);
+            }
+            return this;
+        };
+        Element[proto].toFront = function () {
+            !this.removed && this.Group.parentNode[appendChild](this.Group);
+            this.paper.top != this && tofront(this, this.paper);
+            return this;
+        };
+        Element[proto].toBack = function () {
+            if (this.removed) {
+                return this;
+            }
+            if (this.Group.parentNode.firstChild != this.Group) {
+                this.Group.parentNode.insertBefore(this.Group, this.Group.parentNode.firstChild);
+                toback(this, this.paper);
+            }
+            return this;
+        };
+        Element[proto].insertAfter = function (element) {
+            if (this.removed) {
+                return this;
+            }
+            if (element.Group.nextSibling) {
+                element.Group.parentNode.insertBefore(this.Group, element.Group.nextSibling);
+            } else {
+                element.Group.parentNode[appendChild](this.Group);
+            }
+            insertafter(this, element, this.paper);
+            return this;
+        };
+        Element[proto].insertBefore = function (element) {
+            if (this.removed) {
+                return this;
+            }
+            element.Group.parentNode.insertBefore(this.Group, element.Group);
+            insertbefore(this, element, this.paper);
+            return this;
+        };
+        var blurregexp = / progid:\S+Blur\([^\)]+\)/g;
+        Element[proto].blur = function (size) {
+            var s = this.node.style,
+                f = s.filter;
+            f = f.replace(blurregexp, "");
+            if (+size !== 0) {
+                this.attrs.blur = size;
+                s.filter = f + " progid:DXImageTransform.Microsoft.Blur(pixelradius=" + (+size || 1.5) + ")";
+                s.margin = Raphael.format("-{0}px 0 0 -{0}px", Math.round(+size || 1.5));
+            } else {
+                s.filter = f;
+                s.margin = 0;
+                delete this.attrs.blur;
+            }
+        };
+ 
+        theCircle = function (vml, x, y, r) {
+            var g = createNode("group"),
+                o = createNode("oval"),
+                ol = o.style;
+            g.style.cssText = "position:absolute;left:0;top:0;width:" + vml.width + "px;height:" + vml.height + "px";
+            g.coordsize = coordsize;
+            g.coordorigin = vml.coordorigin;
+            g[appendChild](o);
+            var res = new Element(o, g, vml);
+            res.type = "circle";
+            setFillAndStroke(res, {stroke: "#000", fill: "none"});
+            res.attrs.cx = x;
+            res.attrs.cy = y;
+            res.attrs.r = r;
+            res.setBox({x: x - r, y: y - r, width: r * 2, height: r * 2});
+            vml.canvas[appendChild](g);
+            return res;
+        };
+        theRect = function (vml, x, y, w, h, r) {
+            var g = createNode("group"),
+                o = createNode("roundrect"),
+                arcsize = (+r || 0) / (mmin(w, h));
+            g.style.cssText = "position:absolute;left:0;top:0;width:" + vml.width + "px;height:" + vml.height + "px";
+            g.coordsize = coordsize;
+            g.coordorigin = vml.coordorigin;
+            g[appendChild](o);
+            o.arcsize = arcsize;
+            var res = new Element(o, g, vml);
+            res.type = "rect";
+            setFillAndStroke(res, {stroke: "#000"});
+            res.arcsize = arcsize;
+            res.setBox({x: x, y: y, width: w, height: h, r: r});
+            vml.canvas[appendChild](g);
+            return res;
+        };
+        theEllipse = function (vml, x, y, rx, ry) {
+            var g = createNode("group"),
+                o = createNode("oval"),
+                ol = o.style;
+            g.style.cssText = "position:absolute;left:0;top:0;width:" + vml.width + "px;height:" + vml.height + "px";
+            g.coordsize = coordsize;
+            g.coordorigin = vml.coordorigin;
+            g[appendChild](o);
+            var res = new Element(o, g, vml);
+            res.type = "ellipse";
+            setFillAndStroke(res, {stroke: "#000"});
+            res.attrs.cx = x;
+            res.attrs.cy = y;
+            res.attrs.rx = rx;
+            res.attrs.ry = ry;
+            res.setBox({x: x - rx, y: y - ry, width: rx * 2, height: ry * 2});
+            vml.canvas[appendChild](g);
+            return res;
+        };
+        theImage = function (vml, src, x, y, w, h) {
+            var g = createNode("group"),
+                o = createNode("image"),
+                ol = o.style;
+            g.style.cssText = "position:absolute;left:0;top:0;width:" + vml.width + "px;height:" + vml.height + "px";
+            g.coordsize = coordsize;
+            g.coordorigin = vml.coordorigin;
+            o.src = src;
+            g[appendChild](o);
+            var res = new Element(o, g, vml);
+            res.type = "image";
+            res.attrs.src = src;
+            res.attrs.x = x;
+            res.attrs.y = y;
+            res.attrs.w = w;
+            res.attrs.h = h;
+            res.setBox({x: x, y: y, width: w, height: h});
+            vml.canvas[appendChild](g);
+            return res;
+        };
+        theText = function (vml, x, y, text) {
+            var g = createNode("group"),
+                el = createNode("shape"),
+                ol = el.style,
+                path = createNode("path"),
+                ps = path.style,
+                o = createNode("textpath");
+            g.style.cssText = "position:absolute;left:0;top:0;width:" + vml.width + "px;height:" + vml.height + "px";
+            g.coordsize = coordsize;
+            g.coordorigin = vml.coordorigin;
+            path.v = R.format("m{0},{1}l{2},{1}", round(x * 10), round(y * 10), round(x * 10) + 1);
+            path.textpathok = true;
+            ol.width = vml.width;
+            ol.height = vml.height;
+            o.string = text + E;
+            o.on = true;
+            el[appendChild](o);
+            el[appendChild](path);
+            g[appendChild](el);
+            var res = new Element(o, g, vml);
+            res.shape = el;
+            res.textpath = path;
+            res.type = "text";
+            res.attrs.text = text;
+            res.attrs.x = x;
+            res.attrs.y = y;
+            res.attrs.w = 1;
+            res.attrs.h = 1;
+            setFillAndStroke(res, {font: availableAttrs.font, stroke: "none", fill: "#000"});
+            res.setBox();
+            vml.canvas[appendChild](g);
+            return res;
+        };
+        setSize = function (width, height) {
+            var cs = this.canvas.style;
+            width == +width && (width += "px");
+            height == +height && (height += "px");
+            cs.width = width;
+            cs.height = height;
+            cs.clip = "rect(0 " + width + " " + height + " 0)";
+            return this;
+        };
+        var createNode;
+        doc.createStyleSheet().addRule(".rvml", "behavior:url(#default#VML)");
+        try {
+            !doc.namespaces.rvml && doc.namespaces.add("rvml", "urn:schemas-microsoft-com:vml");
+            createNode = function (tagName) {
+                return doc.createElement('<rvml:' + tagName + ' class="rvml">');
+            };
+        } catch (e) {
+            createNode = function (tagName) {
+                return doc.createElement('<' + tagName + ' xmlns="urn:schemas-microsoft.com:vml" class="rvml">');
+            };
+        }
+        create = function () {
+            var con = getContainer[apply](0, arguments),
+                container = con.container,
+                height = con.height,
+                s,
+                width = con.width,
+                x = con.x,
+                y = con.y;
+            if (!container) {
+                throw new Error("VML container not found.");
+            }
+            var res = new Paper,
+                c = res.canvas = doc.createElement("div"),
+                cs = c.style;
+            width = width || 512;
+            height = height || 342;
+            width == +width && (width += "px");
+            height == +height && (height += "px");
+            res.width = 1e3;
+            res.height = 1e3;
+            res.coordsize = zoom * 1e3 + S + zoom * 1e3;
+            res.coordorigin = "0 0";
+            res.span = doc.createElement("span");
+            res.span.style.cssText = "position:absolute;left:-9999em;top:-9999em;padding:0;margin:0;line-height:1;display:inline;";
+            c[appendChild](res.span);
+            cs.cssText = R.format("width:{0};height:{1};position:absolute;clip:rect(0 {0} {1} 0);overflow:hidden", width, height);
+            if (container == 1) {
+                doc.body[appendChild](c);
+                cs.left = x + "px";
+                cs.top = y + "px";
+            } else {
+                container.style.width = width;
+                container.style.height = height;
+                if (container.firstChild) {
+                    container.insertBefore(c, container.firstChild);
+                } else {
+                    container[appendChild](c);
+                }
+            }
+            plugins.call(res, res, R.fn);
+            return res;
+        };
+        Paper[proto].clear = function () {
+            this.canvas.innerHTML = E;
+            this.span = doc.createElement("span");
+            this.span.style.cssText = "position:absolute;left:-9999em;top:-9999em;padding:0;margin:0;line-height:1;display:inline;";
+            this.canvas[appendChild](this.span);
+            this.bottom = this.top = null;
+        };
+        Paper[proto].remove = function () {
+            this.canvas.parentNode.removeChild(this.canvas);
+            for (var i in this) {
+                this[i] = removed(i);
+            }
+            return true;
+        };
+    }
+ 
+    // rest
+    // Safari or Chrome (WebKit) rendering bug workaround method
+    if ((/^Apple|^Google/).test(win.navigator.vendor) && !(win.navigator.userAgent.indexOf("Version/4.0") + 1)) {
+        Paper[proto].safari = function () {
+            var rect = this.rect(-99, -99, this.width + 99, this.height + 99);
+            win.setTimeout(function () {rect.remove();});
+        };
+    } else {
+        Paper[proto].safari = function () {};
+    }
+ 
+    // Events
+    var addEvent = (function () {
+        if (doc.addEventListener) {
+            return function (obj, type, fn, element) {
+                var f = function (e) {
+                    return fn.call(element, e);
+                };
+                obj.addEventListener(type, f, false);
+                return function () {
+                    obj.removeEventListener(type, f, false);
+                    return true;
+                };
+            };
+        } else if (doc.attachEvent) {
+            return function (obj, type, fn, element) {
+                var f = function (e) {
+                    return fn.call(element, e || win.event);
+                };
+                obj.attachEvent("on" + type, f);
+                var detacher = function () {
+                    obj.detachEvent("on" + type, f);
+                    return true;
+                };
+                return detacher;
+            };
+        }
+    })();
+    for (var i = events[length]; i--;) {
+        (function (eventName) {
+            Element[proto][eventName] = function (fn) {
+                if (R.is(fn, "function")) {
+                    this.events = this.events || [];
+                    this.events.push({name: eventName, f: fn, unbind: addEvent(this.shape || this.node, eventName, fn, this)});
+                }
+                return this;
+            };
+            Element[proto]["un" + eventName] = function (fn) {
+                var events = this.events,
+                    l = events[length];
+                while (l--) if (events[l].name == eventName && events[l].f == fn) {
+                    events[l].unbind();
+                    events.splice(l, 1);
+                    !events.length && delete this.events;
+                    return this;
+                }
+                return this;
+            };
+        })(events[i]);
+    }
+    Element[proto].hover = function (f_in, f_out) {
+        return this.mouseover(f_in).mouseout(f_out);
+    };
+    Element[proto].unhover = function (f_in, f_out) {
+        return this.unmouseover(f_in).unmouseout(f_out);
+    };
+    Paper[proto].circle = function (x, y, r) {
+        return theCircle(this, x || 0, y || 0, r || 0);
+    };
+    Paper[proto].rect = function (x, y, w, h, r) {
+        return theRect(this, x || 0, y || 0, w || 0, h || 0, r || 0);
+    };
+    Paper[proto].ellipse = function (x, y, rx, ry) {
+        return theEllipse(this, x || 0, y || 0, rx || 0, ry || 0);
+    };
+    Paper[proto].path = function (pathString) {
+        pathString && !R.is(pathString, "string") && !R.is(pathString[0], "array") && (pathString += E);
+        return thePath(R.format[apply](R, arguments), this);
+    };
+    Paper[proto].image = function (src, x, y, w, h) {
+        return theImage(this, src || "about:blank", x || 0, y || 0, w || 0, h || 0);
+    };
+    Paper[proto].text = function (x, y, text) {
+        return theText(this, x || 0, y || 0, text || E);
+    };
+    Paper[proto].set = function (itemsArray) {
+        arguments[length] > 1 && (itemsArray = Array[proto].splice.call(arguments, 0, arguments[length]));
+        return new Set(itemsArray);
+    };
+    Paper[proto].setSize = setSize;
+    Paper[proto].top = Paper[proto].bottom = null;
+    Paper[proto].raphael = R;
+    function x_y() {
+        return this.x + S + this.y;
+    }
+    Element[proto].scale = function (x, y, cx, cy) {
+        if (x == null && y == null) {
+            return {
+                x: this._.sx,
+                y: this._.sy,
+                toString: x_y
+            };
+        }
+        y = y || x;
+        !+y && (y = x);
+        var dx,
+            dy,
+            dcx,
+            dcy,
+            a = this.attrs;
+        if (x != 0) {
+            var bb = this.getBBox(),
+                rcx = bb.x + bb.width / 2,
+                rcy = bb.y + bb.height / 2,
+                kx = x / this._.sx,
+                ky = y / this._.sy;
+            cx = (+cx || cx == 0) ? cx : rcx;
+            cy = (+cy || cy == 0) ? cy : rcy;
+            var dirx = ~~(x / math.abs(x)),
+                diry = ~~(y / math.abs(y)),
+                s = this.node.style,
+                ncx = cx + (rcx - cx) * kx,
+                ncy = cy + (rcy - cy) * ky;
+            switch (this.type) {
+                case "rect":
+                case "image":
+                    var neww = a.width * dirx * kx,
+                        newh = a.height * diry * ky;
+                    this.attr({
+                        height: newh,
+                        r: a.r * mmin(dirx * kx, diry * ky),
+                        width: neww,
+                        x: ncx - neww / 2,
+                        y: ncy - newh / 2
+                    });
+                    break;
+                case "circle":
+                case "ellipse":
+                    this.attr({
+                        rx: a.rx * dirx * kx,
+                        ry: a.ry * diry * ky,
+                        r: a.r * mmin(dirx * kx, diry * ky),
+                        cx: ncx,
+                        cy: ncy
+                    });
+                    break;
+                case "path":
+                    var path = pathToRelative(a.path),
+                        skip = true;
+                    for (var i = 0, ii = path[length]; i < ii; i++) {
+                        var p = path[i],
+                            P0 = upperCase.call(p[0]);
+                        if (P0 == "M" && skip) {
+                            continue;
+                        } else {
+                            skip = false;
+                        }
+                        if (P0 == "A") {
+                            p[path[i][length] - 2] *= kx;
+                            p[path[i][length] - 1] *= ky;
+                            p[1] *= dirx * kx;
+                            p[2] *= diry * ky;
+                            p[5] = +!(dirx + diry ? !+p[5] : +p[5]);
+                        } else if (P0 == "H") {
+                            for (var j = 1, jj = p[length]; j < jj; j++) {
+                                p[j] *= kx;
+                            }
+                        } else if (P0 == "V") {
+                            for (j = 1, jj = p[length]; j < jj; j++) {
+                                p[j] *= ky;
+                            }
+                         } else {
+                            for (j = 1, jj = p[length]; j < jj; j++) {
+                                p[j] *= (j % 2) ? kx : ky;
+                            }
+                        }
+                    }
+                    var dim2 = pathDimensions(path);
+                    dx = ncx - dim2.x - dim2.width / 2;
+                    dy = ncy - dim2.y - dim2.height / 2;
+                    path[0][1] += dx;
+                    path[0][2] += dy;
+                    this.attr({path: path});
+                break;
+            }
+            if (this.type in {text: 1, image:1} && (dirx != 1 || diry != 1)) {
+                if (this.transformations) {
+                    this.transformations[2] = "scale("[concat](dirx, ",", diry, ")");
+                    this.node[setAttribute]("transform", this.transformations[join](S));
+                    dx = (dirx == -1) ? -a.x - (neww || 0) : a.x;
+                    dy = (diry == -1) ? -a.y - (newh || 0) : a.y;
+                    this.attr({x: dx, y: dy});
+                    a.fx = dirx - 1;
+                    a.fy = diry - 1;
+                } else {
+                    this.node.filterMatrix = " progid:DXImageTransform.Microsoft.Matrix(M11="[concat](dirx,
+                        ", M12=0, M21=0, M22=", diry,
+                        ", Dx=0, Dy=0, sizingmethod='auto expand', filtertype='bilinear')");
+                    s.filter = (this.node.filterMatrix || E) + (this.node.filterOpacity || E);
+                }
+            } else {
+                if (this.transformations) {
+                    this.transformations[2] = E;
+                    this.node[setAttribute]("transform", this.transformations[join](S));
+                    a.fx = 0;
+                    a.fy = 0;
+                } else {
+                    this.node.filterMatrix = E;
+                    s.filter = (this.node.filterMatrix || E) + (this.node.filterOpacity || E);
+                }
+            }
+            a.scale = [x, y, cx, cy][join](S);
+            this._.sx = x;
+            this._.sy = y;
+        }
+        return this;
+    };
+    Element[proto].clone = function () {
+        var attr = this.attr();
+        delete attr.scale;
+        delete attr.translation;
+        return this.paper[this.type]().attr(attr);
+    };
+    var getPointAtSegmentLength = cacher(function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, length) {
+        var len = 0,
+            old;
+        for (var i = 0; i < 1.001; i+=.001) {
+            var dot = R.findDotsAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, i);
+            i && (len += pow(pow(old.x - dot.x, 2) + pow(old.y - dot.y, 2), .5));
+            if (len >= length) {
+                return dot;
+            }
+            old = dot;
+        }
+    }),
+    getLengthFactory = function (istotal, subpath) {
+        return function (path, length, onlystart) {
+            path = path2curve(path);
+            var x, y, p, l, sp = "", subpaths = {}, point,
+                len = 0;
+            for (var i = 0, ii = path.length; i < ii; i++) {
+                p = path[i];
+                if (p[0] == "M") {
+                    x = +p[1];
+                    y = +p[2];
+                } else {
+                    l = segmentLength(x, y, p[1], p[2], p[3], p[4], p[5], p[6]);
+                    if (len + l > length) {
+                        if (subpath && !subpaths.start) {
+                            point = getPointAtSegmentLength(x, y, p[1], p[2], p[3], p[4], p[5], p[6], length - len);
+                            sp += ["C", point.start.x, point.start.y, point.m.x, point.m.y, point.x, point.y];
+                            if (onlystart) {return sp;}
+                            subpaths.start = sp;
+                            sp = ["M", point.x, point.y + "C", point.n.x, point.n.y, point.end.x, point.end.y, p[5], p[6]][join]();
+                            len += l;
+                            x = +p[5];
+                            y = +p[6];
+                            continue;
+                        }
+                        if (!istotal && !subpath) {
+                            point = getPointAtSegmentLength(x, y, p[1], p[2], p[3], p[4], p[5], p[6], length - len);
+                            return {x: point.x, y: point.y, alpha: point.alpha};
+                        }
+                    }
+                    len += l;
+                    x = +p[5];
+                    y = +p[6];
+                }
+                sp += p;
+            }
+            subpaths.end = sp;
+            point = istotal ? len : subpath ? subpaths : R.findDotsAtSegment(x, y, p[1], p[2], p[3], p[4], p[5], p[6], 1);
+            point.alpha && (point = {x: point.x, y: point.y, alpha: point.alpha});
+            return point;
+        };
+    },
+    segmentLength = cacher(function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) {
+        var old = {x: 0, y: 0},
+            len = 0;
+        for (var i = 0; i < 1.01; i+=.01) {
+            var dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, i);
+            i && (len += pow(pow(old.x - dot.x, 2) + pow(old.y - dot.y, 2), .5));
+            old = dot;
+        }
+        return len;
+    });
+    var getTotalLength = getLengthFactory(1),
+        getPointAtLength = getLengthFactory(),
+        getSubpathsAtLength = getLengthFactory(0, 1);
+    Element[proto].getTotalLength = function () {
+        if (this.type != "path") {return;}
+        return getTotalLength(this.attrs.path);
+    };
+    Element[proto].getPointAtLength = function (length) {
+        if (this.type != "path") {return;}
+        return getPointAtLength(this.attrs.path, length);
+    };
+    Element[proto].getSubpath = function (from, to) {
+        if (this.type != "path") {return;}
+        if (math.abs(this.getTotalLength() - to) < 1e-6) {
+            return getSubpathsAtLength(this.attrs.path, from).end;
+        }
+        var a = getSubpathsAtLength(this.attrs.path, to, 1);
+        return from ? getSubpathsAtLength(a, from).end : a;
+    };
+
+    // animation easing formulas
+    R.easing_formulas = {
+        linear: function (n) {
+            return n;
+        },
+        "<": function (n) {
+            return pow(n, 3);
+        },
+        ">": function (n) {
+            return pow(n - 1, 3) + 1;
+        },
+        "<>": function (n) {
+            n = n * 2;
+            if (n < 1) {
+                return pow(n, 3) / 2;
+            }
+            n -= 2;
+            return (pow(n, 3) + 2) / 2;
+        },
+        backIn: function (n) {
+            var s = 1.70158;
+            return n * n * ((s + 1) * n - s);
+        },
+        backOut: function (n) {
+            n = n - 1;
+            var s = 1.70158;
+            return n * n * ((s + 1) * n + s) + 1;
+        },
+        elastic: function (n) {
+            if (n == 0 || n == 1) {
+                return n;
+            }
+            var p = .3,
+                s = p / 4;
+            return pow(2, -10 * n) * math.sin((n - s) * (2 * math.PI) / p) + 1;
+        },
+        bounce: function (n) {
+            var s = 7.5625,
+                p = 2.75,
+                l;
+            if (n < (1 / p)) {
+                l = s * n * n;
+            } else {
+                if (n < (2 / p)) {
+                    n -= (1.5 / p);
+                    l = s * n * n + .75;
+                } else {
+                    if (n < (2.5 / p)) {
+                        n -= (2.25 / p);
+                        l = s * n * n + .9375;
+                    } else {
+                        n -= (2.625 / p);
+                        l = s * n * n + .984375;
+                    }
+                }
+            }
+            return l;
+        }
+    };
+ 
+    var animationElements = {length : 0},
+        animation = function () {
+            var Now = +new Date;
+            for (var l in animationElements) if (l != "length" && animationElements[has](l)) {
+                var e = animationElements[l];
+                if (e.stop || e.el.removed) {
+                    delete animationElements[l];
+                    animationElements[length]--;
+                    continue;
+                }
+                var time = Now - e.start,
+                    ms = e.ms,
+                    easing = e.easing,
+                    from = e.from,
+                    diff = e.diff,
+                    to = e.to,
+                    t = e.t,
+                    prev = e.prev || 0,
+                    that = e.el,
+                    callback = e.callback,
+                    set = {},
+                    now;
+                if (time < ms) {
+                    var pos = R.easing_formulas[easing] ? R.easing_formulas[easing](time / ms) : time / ms;
+                    for (var attr in from) if (from[has](attr)) {
+                        switch (availableAnimAttrs[attr]) {
+                            case "along":
+                                now = pos * ms * diff[attr];
+                                to.back && (now = to.len - now);
+                                var point = getPointAtLength(to[attr], now);
+                                that.translate(diff.sx - diff.x || 0, diff.sy - diff.y || 0);
+                                diff.x = point.x;
+                                diff.y = point.y;
+                                that.translate(point.x - diff.sx, point.y - diff.sy);
+                                to.rot && that.rotate(diff.r + point.alpha, point.x, point.y);
+                                break;
+                            case "number":
+                                now = +from[attr] + pos * ms * diff[attr];
+                                break;
+                            case "colour":
+                                now = "rgb(" + [
+                                    upto255(round(from[attr].r + pos * ms * diff[attr].r)),
+                                    upto255(round(from[attr].g + pos * ms * diff[attr].g)),
+                                    upto255(round(from[attr].b + pos * ms * diff[attr].b))
+                                ][join](",") + ")";
+                                break;
+                            case "path":
+                                now = [];
+                                for (var i = 0, ii = from[attr][length]; i < ii; i++) {
+                                    now[i] = [from[attr][i][0]];
+                                    for (var j = 1, jj = from[attr][i][length]; j < jj; j++) {
+                                        now[i][j] = +from[attr][i][j] + pos * ms * diff[attr][i][j];
+                                    }
+                                    now[i] = now[i][join](S);
+                                }
+                                now = now[join](S);
+                                break;
+                            case "csv":
+                                switch (attr) {
+                                    case "translation":
+                                        var x = diff[attr][0] * (time - prev),
+                                            y = diff[attr][1] * (time - prev);
+                                        t.x += x;
+                                        t.y += y;
+                                        now = x + S + y;
+                                    break;
+                                    case "rotation":
+                                        now = +from[attr][0] + pos * ms * diff[attr][0];
+                                        from[attr][1] && (now += "," + from[attr][1] + "," + from[attr][2]);
+                                    break;
+                                    case "scale":
+                                        now = [+from[attr][0] + pos * ms * diff[attr][0], +from[attr][1] + pos * ms * diff[attr][1], (2 in to[attr] ? to[attr][2] : E), (3 in to[attr] ? to[attr][3] : E)][join](S);
+                                    break;
+                                    case "clip-rect":
+                                        now = [];
+                                        i = 4;
+                                        while (i--) {
+                                            now[i] = +from[attr][i] + pos * ms * diff[attr][i];
+                                        }
+                                    break;
+                                }
+                                break;
+                        }
+                        set[attr] = now;
+                    }
+                    that.attr(set);
+                    that._run && that._run.call(that);
+                } else {
+                    if (to.along) {
+                        point = getPointAtLength(to.along, to.len * !to.back);
+                        that.translate(diff.sx - (diff.x || 0) + point.x - diff.sx, diff.sy - (diff.y || 0) + point.y - diff.sy);
+                        to.rot && that.rotate(diff.r + point.alpha, point.x, point.y);
+                    }
+                    (t.x || t.y) && that.translate(-t.x, -t.y);
+                    to.scale && (to.scale = to.scale + E);
+                    that.attr(to);
+                    delete animationElements[l];
+                    animationElements[length]--;
+                    that.in_animation = null;
+                    R.is(callback, "function") && callback.call(that);
+                }
+                e.prev = time;
+            }
+            R.svg && that && that.paper.safari();
+            animationElements[length] && win.setTimeout(animation);
+        },
+        upto255 = function (color) {
+            return color > 255 ? 255 : (color < 0 ? 0 : color);
+        },
+        translate = function (x, y) {
+            if (x == null) {
+                return {x: this._.tx, y: this._.ty, toString: x_y};
+            }
+            this._.tx += +x;
+            this._.ty += +y;
+            switch (this.type) {
+                case "circle":
+                case "ellipse":
+                    this.attr({cx: +x + this.attrs.cx, cy: +y + this.attrs.cy});
+                    break;
+                case "rect":
+                case "image":
+                case "text":
+                    this.attr({x: +x + this.attrs.x, y: +y + this.attrs.y});
+                    break;
+                case "path":
+                    var path = pathToRelative(this.attrs.path);
+                    path[0][1] += +x;
+                    path[0][2] += +y;
+                    this.attr({path: path});
+                break;
+            }
+            return this;
+        };
+    Element[proto].animateWith = function (element, params, ms, easing, callback) {
+        animationElements[element.id] && (params.start = animationElements[element.id].start);
+        return this.animate(params, ms, easing, callback);
+    };
+    Element[proto].animateAlong = along();
+    Element[proto].animateAlongBack = along(1);
+    function along(isBack) {
+        return function (path, ms, rotate, callback) {
+            var params = {back: isBack};
+            R.is(rotate, "function") ? (callback = rotate) : (params.rot = rotate);
+            path && path.constructor == Element && (path = path.attrs.path);
+            path && (params.along = path);
+            return this.animate(params, ms, callback);
+        };
+    }
+    Element[proto].onAnimation = function (f) {
+        this._run = f || 0;
+        return this;
+    };
+    Element[proto].animate = function (params, ms, easing, callback) {
+        if (R.is(easing, "function") || !easing) {
+            callback = easing || null;
+        }
+        var from = {},
+            to = {},
+            diff = {};
+        for (var attr in params) if (params[has](attr)) {
+            if (availableAnimAttrs[has](attr)) {
+                from[attr] = this.attr(attr);
+                (from[attr] == null) && (from[attr] = availableAttrs[attr]);
+                to[attr] = params[attr];
+                switch (availableAnimAttrs[attr]) {
+                    case "along":
+                        var len = getTotalLength(params[attr]),
+                            point = getPointAtLength(params[attr], len * !!params.back),
+                            bb = this.getBBox();
+                        diff[attr] = len / ms;
+                        diff.tx = bb.x;
+                        diff.ty = bb.y;
+                        diff.sx = point.x;
+                        diff.sy = point.y;
+                        to.rot = params.rot;
+                        to.back = params.back;
+                        to.len = len;
+                        params.rot && (diff.r = toFloat(this.rotate()) || 0);
+                        break;
+                    case "number":
+                        diff[attr] = (to[attr] - from[attr]) / ms;
+                        break;
+                    case "colour":
+                        from[attr] = R.getRGB(from[attr]);
+                        var toColour = R.getRGB(to[attr]);
+                        diff[attr] = {
+                            r: (toColour.r - from[attr].r) / ms,
+                            g: (toColour.g - from[attr].g) / ms,
+                            b: (toColour.b - from[attr].b) / ms
+                        };
+                        break;
+                    case "path":
+                        var pathes = path2curve(from[attr], to[attr]);
+                        from[attr] = pathes[0];
+                        var toPath = pathes[1];
+                        diff[attr] = [];
+                        for (var i = 0, ii = from[attr][length]; i < ii; i++) {
+                            diff[attr][i] = [0];
+                            for (var j = 1, jj = from[attr][i][length]; j < jj; j++) {
+                                diff[attr][i][j] = (toPath[i][j] - from[attr][i][j]) / ms;
+                            }
+                        }
+                        break;
+                    case "csv":
+                        var values = (params[attr] + E)[split](separator),
+                            from2 = (from[attr] + E)[split](separator);
+                        switch (attr) {
+                            case "translation":
+                                from[attr] = [0, 0];
+                                diff[attr] = [values[0] / ms, values[1] / ms];
+                            break;
+                            case "rotation":
+                                from[attr] = (from2[1] == values[1] && from2[2] == values[2]) ? from2 : [0, values[1], values[2]];
+                                diff[attr] = [(values[0] - from[attr][0]) / ms, 0, 0];
+                            break;
+                            case "scale":
+                                params[attr] = values;
+                                from[attr] = (from[attr] + E)[split](separator);
+                                diff[attr] = [(values[0] - from[attr][0]) / ms, (values[1] - from[attr][1]) / ms, 0, 0];
+                            break;
+                            case "clip-rect":
+                                from[attr] = (from[attr] + E)[split](separator);
+                                diff[attr] = [];
+                                i = 4;
+                                while (i--) {
+                                    diff[attr][i] = (values[i] - from[attr][i]) / ms;
+                                }
+                            break;
+                        }
+                        to[attr] = values;
+                }
+            }
+        }
+        this.stop();
+        this.in_animation = 1;
+        animationElements[this.id] = {
+            start: params.start || +new Date,
+            ms: ms,
+            easing: easing,
+            from: from,
+            diff: diff,
+            to: to,
+            el: this,
+            callback: callback,
+            t: {x: 0, y: 0}
+        };
+        ++animationElements[length] == 1 && animation();
+        return this;
+    };
+    Element[proto].stop = function () {
+        animationElements[this.id] && animationElements[length]--;
+        delete animationElements[this.id];
+        return this;
+    };
+    Element[proto].translate = function (x, y) {
+        return this.attr({translation: x + " " + y});
+    };
+    Element[proto][toString] = function () {
+        return "Rapha\xebl\u2019s object";
+    };
+    R.ae = animationElements;
+ 
+    // Set
+    var Set = function (items) {
+        this.items = [];
+        this[length] = 0;
+        if (items) {
+            for (var i = 0, ii = items[length]; i < ii; i++) {
+                if (items[i] && (items[i].constructor == Element || items[i].constructor == Set)) {
+                    this[this.items[length]] = this.items[this.items[length]] = items[i];
+                    this[length]++;
+                }
+            }
+        }
+    };
+    Set[proto][push] = function () {
+        var item,
+            len;
+        for (var i = 0, ii = arguments[length]; i < ii; i++) {
+            item = arguments[i];
+            if (item && (item.constructor == Element || item.constructor == Set)) {
+                len = this.items[length];
+                this[len] = this.items[len] = item;
+                this[length]++;
+            }
+        }
+        return this;
+    };
+    Set[proto].pop = function () {
+        delete this[this[length]--];
+        return this.items.pop();
+    };
+    for (var method in Element[proto]) if (Element[proto][has](method)) {
+        Set[proto][method] = (function (methodname) {
+            return function () {
+                for (var i = 0, ii = this.items[length]; i < ii; i++) {
+                    this.items[i][methodname][apply](this.items[i], arguments);
+                }
+                return this;
+            };
+        })(method);
+    }
+    Set[proto].attr = function (name, value) {
+        if (name && R.is(name, "array") && R.is(name[0], "object")) {
+            for (var j = 0, jj = name[length]; j < jj; j++) {
+                this.items[j].attr(name[j]);
+            }
+        } else {
+            for (var i = 0, ii = this.items[length]; i < ii; i++) {
+                this.items[i].attr(name, value);
+            }
+        }
+        return this;
+    };
+    Set[proto].animate = function (params, ms, easing, callback) {
+        (R.is(easing, "function") || !easing) && (callback = easing || null);
+        var len = this.items[length],
+            i = len,
+            set = this,
+            collector;
+        callback && (collector = function () {
+            !--len && callback.call(set);
+        });
+        this.items[--i].animate(params, ms, easing || collector, collector);
+        while (i--) {
+            this.items[i].animateWith(this.items[len - 1], params, ms, easing || collector, collector);
+        }
+        return this;
+    };
+    Set[proto].insertAfter = function (el) {
+        var i = this.items[length];
+        while (i--) {
+            this.items[i].insertAfter(el);
+        }
+        return this;
+    };
+    Set[proto].getBBox = function () {
+        var x = [],
+            y = [],
+            w = [],
+            h = [];
+        for (var i = this.items[length]; i--;) {
+            var box = this.items[i].getBBox();
+            x[push](box.x);
+            y[push](box.y);
+            w[push](box.x + box.width);
+            h[push](box.y + box.height);
+        }
+        x = mmin[apply](0, x);
+        y = mmin[apply](0, y);
+        return {
+            x: x,
+            y: y,
+            width: mmax[apply](0, w) - x,
+            height: mmax[apply](0, h) - y
+        };
+    };
+    Set[proto].clone = function (s) {
+        s = new Set;
+        for (var i = 0, ii = this.items[length]; i < ii; i++) {
+            s[push](this.items[i].clone());
+        }
+        return s;
+    };
+
+    R.registerFont = function (font) {
+        if (!font.face) {
+            return font;
+        }
+        this.fonts = this.fonts || {};
+        var fontcopy = {
+                w: font.w,
+                face: {},
+                glyphs: {}
+            },
+            family = font.face["font-family"];
+        for (var prop in font.face) if (font.face[has](prop)) {
+            fontcopy.face[prop] = font.face[prop];
+        }
+        if (this.fonts[family]) {
+            this.fonts[family][push](fontcopy);
+        } else {
+            this.fonts[family] = [fontcopy];
+        }
+        if (!font.svg) {
+            fontcopy.face["units-per-em"] = toInt(font.face["units-per-em"], 10);
+            for (var glyph in font.glyphs) if (font.glyphs[has](glyph)) {
+                var path = font.glyphs[glyph];
+                fontcopy.glyphs[glyph] = {
+                    w: path.w,
+                    k: {},
+                    d: path.d && "M" + path.d[rp](/[mlcxtrv]/g, function (command) {
+                            return {l: "L", c: "C", x: "z", t: "m", r: "l", v: "c"}[command] || "M";
+                        }) + "z"
+                };
+                if (path.k) {
+                    for (var k in path.k) if (path[has](k)) {
+                        fontcopy.glyphs[glyph].k[k] = path.k[k];
+                    }
+                }
+            }
+        }
+        return font;
+    };
+    Paper[proto].getFont = function (family, weight, style, stretch) {
+        stretch = stretch || "normal";
+        style = style || "normal";
+        weight = +weight || {normal: 400, bold: 700, lighter: 300, bolder: 800}[weight] || 400;
+        var font = R.fonts[family];
+        if (!font) {
+            var name = new RegExp("(^|\\s)" + family[rp](/[^\w\d\s+!~.:_-]/g, E) + "(\\s|$)", "i");
+            for (var fontName in R.fonts) if (R.fonts[has](fontName)) {
+                if (name.test(fontName)) {
+                    font = R.fonts[fontName];
+                    break;
+                }
+            }
+        }
+        var thefont;
+        if (font) {
+            for (var i = 0, ii = font[length]; i < ii; i++) {
+                thefont = font[i];
+                if (thefont.face["font-weight"] == weight && (thefont.face["font-style"] == style || !thefont.face["font-style"]) && thefont.face["font-stretch"] == stretch) {
+                    break;
+                }
+            }
+        }
+        return thefont;
+    };
+    Paper[proto].print = function (x, y, string, font, size, origin) {
+        origin = origin || "middle"; // baseline|middle
+        var out = this.set(),
+            letters = (string + E)[split](E),
+            shift = 0,
+            path = E,
+            scale;
+        R.is(font, "string") && (font = this.getFont(font));
+        if (font) {
+            scale = (size || 16) / font.face["units-per-em"];
+            var bb = font.face.bbox.split(separator),
+                top = +bb[0],
+                height = +bb[1] + (origin == "baseline" ? bb[3] - bb[1] + (+font.face.descent) : (bb[3] - bb[1]) / 2);
+            for (var i = 0, ii = letters[length]; i < ii; i++) {
+                var prev = i && font.glyphs[letters[i - 1]] || {},
+                    curr = font.glyphs[letters[i]];
+                shift += i ? (prev.w || font.w) + (prev.k && prev.k[letters[i]] || 0) : 0;
+                curr && curr.d && out[push](this.path(curr.d).attr({fill: "#000", stroke: "none", translation: [shift, 0]}));
+            }
+            out.scale(scale, scale, top, height).translate(x - top, y - height);
+        }
+        return out;
+    };
+
+    var formatrg = /\{(\d+)\}/g;
+    R.format = function (token, array) {
+        var args = R.is(array, "array") ? [0][concat](array) : arguments;
+        token && R.is(token, "string") && args[length] - 1 && (token = token[rp](formatrg, function (str, i) {
+            return args[++i] == null ? E : args[i];
+        }));
+        return token || E;
+    };
+    R.ninja = function () {
+        oldRaphael.was ? (Raphael = oldRaphael.is) : delete Raphael;
+        return R;
+    };
+    R.el = Element[proto];
+    return R;
+})();

File diff suppressed because it is too large
+ 7 - 0
src/contrib/loggraph/web/org/apache/zookeeper/graph/resources/yui-min.js


Some files were not shown because too many files changed in this diff