diff --git a/TODO b/TODO
index 3f6678e..6126c64 100644
--- a/TODO
+++ b/TODO
@@ -3,6 +3,7 @@ AVERAGE/HIGH PRIORITY:
======================
- Web interface
+- Function names (private functions with _ or __ ?)
- Bayesian network
- Modules for correlation coefficients
- Code profiling
diff --git a/correlation.c b/correlation.c
index 8552005..c28953c 100644
--- a/correlation.c
+++ b/correlation.c
@@ -73,7 +73,7 @@ _AI_correlation_table_cleanup ()
*/
PRIVATE void
-_AI_print_correlated_alerts ( AI_alert_correlation *corr, FILE *fp )
+_AI_correlated_alerts_to_dot ( AI_alert_correlation *corr, FILE *fp )
{
char src_addr1[INET_ADDRSTRLEN],
dst_addr1[INET_ADDRSTRLEN],
@@ -130,8 +130,65 @@ _AI_print_correlated_alerts ( AI_alert_correlation *corr, FILE *fp )
timestamp2,
corr->key.b->grouped_alerts_count
);
-} /* ----- end of function _AI_correlation_flow_to_file ----- */
+} /* ----- end of function _AI_correlated_alerts_to_dot ----- */
+/**
+ * \brief Recursively write the flow of correlated alerts to a .json file, ready for being rendered in the web interface
+ */
+
+PRIVATE void
+_AI_correlated_alerts_to_json ()
+{
+ unsigned int i = 0;
+ char json_file[1040] = { 0 };
+ FILE *fp;
+ AI_snort_alert *alert_iterator = NULL;
+
+ /* If there is no directory configured for the web interface, just exit */
+ if ( strlen ( config->webserv_dir ) == 0 )
+ return;
+
+ snprintf ( json_file, sizeof ( json_file ), "%s/correlation_graph.json", config->webserv_dir );
+
+ if ( !( fp = fopen ( json_file, "w" )))
+ {
+ AI_fatal_err ( "Unable to write on correlated_graph.json in htdocs directory", __FILE__, __LINE__ );
+ }
+
+ fprintf ( fp, "[\n" );
+
+ for ( alert_iterator = alerts; alert_iterator; alert_iterator = alert_iterator->next )
+ {
+ fprintf ( fp, "{\n"
+ "\t\"id\": %lu,\n"
+ "\t\"label\": \"%s\"",
+ alert_iterator->alert_id, alert_iterator->desc );
+
+ for ( i=0; i < alert_iterator->n_derived_alerts; i++ )
+ {
+ if ( i == 0 )
+ {
+ fprintf ( fp, ",\n\t\"connectedTo\": [\n" );
+ }
+
+ fprintf ( fp, "\t\t{ \"id\": %lu }%s\n",
+ alert_iterator->derived_alerts[i]->alert_id,
+ ((i < alert_iterator->n_derived_alerts - 1) ? "," : ""));
+
+ if ( i == alert_iterator->n_derived_alerts - 1 )
+ {
+ fprintf ( fp, "\t]" );
+ }
+ }
+
+ fprintf ( fp, "\n}%s\n",
+ (alert_iterator->next ? "," : ""));
+ }
+
+ fprintf ( fp, "]\n" );
+ fclose ( fp );
+ chmod ( json_file, 0644 );
+} /* ----- end of function _AI_correlated_alerts_to_json ----- */
/**
* \brief Get the name of the function called by a pre-condition or post-condition predicate
@@ -848,7 +905,7 @@ AI_alert_correlation_thread ( void *arg )
corr->key.a->derived_alerts[ corr->key.a->n_derived_alerts - 1 ] = corr->key.b;
corr->key.b->parent_alerts [ corr->key.b->n_parent_alerts - 1 ] = corr->key.a;
- _AI_print_correlated_alerts ( corr, fp );
+ _AI_correlated_alerts_to_dot ( corr, fp );
if ( config->outdbtype != outdb_none )
{
@@ -889,6 +946,16 @@ AI_alert_correlation_thread ( void *arg )
agclose ( g );
fclose ( fp );
#endif
+
+ /* If no database output is defined, then the alerts have no alert_id, so we cannot use the
+ * web interface for correlating them, as they have no unique identifier */
+ if ( config->outdbtype != outdb_none )
+ {
+ if ( strlen ( config->webserv_dir ) != 0 )
+ {
+ _AI_correlated_alerts_to_json ();
+ }
+ }
}
pthread_mutex_unlock ( &mutex );
diff --git a/htdocs/graph.json b/htdocs/graph.json
new file mode 100644
index 0000000..e2a8cc9
--- /dev/null
+++ b/htdocs/graph.json
@@ -0,0 +1,11 @@
+[{
+ "id": 1,
+ "label": "test1",
+ "connectedTo": [
+ { "id": 2 }
+ ]
+}, {
+ "id": 2,
+ "label": "test2"
+}]
+
diff --git a/htdocs/index.html b/htdocs/index.html
new file mode 100644
index 0000000..16e4303
--- /dev/null
+++ b/htdocs/index.html
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/htdocs/js/Curry-1.0.1.js b/htdocs/js/Curry-1.0.1.js
new file mode 100644
index 0000000..f5aa0f9
--- /dev/null
+++ b/htdocs/js/Curry-1.0.1.js
@@ -0,0 +1,29 @@
+/**
+ * Curry - Function currying
+ * Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
+ * Licensed under BSD (http://www.opensource.org/licenses/bsd-license.php)
+ * Date: 10/4/2008
+ *
+ * @author Ariel Flesler
+ * @version 1.0.1
+ */
+
+function curry( fn ){
+ return function(){
+ var args = curry.args(arguments),
+ master = arguments.callee,
+ self = this;
+
+ return args.length >= fn.length ? fn.apply(self,args) : function(){
+ return master.apply( self, args.concat(curry.args(arguments)) );
+ };
+ };
+};
+
+curry.args = function( args ){
+ return Array.prototype.slice.call(args);
+};
+
+Function.prototype.curry = function(){
+ return curry(this);
+};
\ No newline at end of file
diff --git a/htdocs/js/dracula_algorithms.js b/htdocs/js/dracula_algorithms.js
new file mode 100644
index 0000000..81979bf
--- /dev/null
+++ b/htdocs/js/dracula_algorithms.js
@@ -0,0 +1,414 @@
+/*
+ * Various algorithms and data structures, licensed under the MIT-license.
+ * (c) 2010 by Johann Philipp Strathausen
+ * http://strathausen.eu
+ *
+ */
+
+
+
+/*
+ Path-finding algorithm Bellman-Ford
+
+ finds the shortest paths from one node to all nodes
+ - runs in O( |E| · |V| ), where E = edges and V = vertices (nodes)
+ - can run on graphs with negative edge weights as long as they do not have
+ any negative weight cycles
+ */
+function bellman_ford(g, source) {
+
+ /* STEP 1: initialisation */
+ for(var n in g.nodes)
+ g.nodes[n].distance = Infinity;
+ /* predecessors are implicitly null */
+ source.distance = 0;
+ g.snapShot("Initiallisation: Set all distances are infinite and all predecessors are null.");
+
+ /* STEP 2: relax each edge (this is at the heart of Bellman-Ford) */
+ /* repeat this for the number of nodes minus one */
+ for(var i = 1; i < g.nodes.length; i++)
+ /* for each edge */
+ for(var e in g.edges) {
+ var edge = g.edges[e];
+ if(edge.source.distance + edge.weight < edge.target.distance) {
+ g.snapShot("Relax edge between "+edge.source.id+" and "+edge.target.id+".");
+ edge.target.distance = edge.source.distance + edge.weight;
+ edge.target.predecessor = edge.source;
+ }
+ }
+
+ g.snapShot("Ready.");
+
+ /* STEP 3: TODO Check for negative cycles */
+ /* For now we assume here that the graph does not contain any negative
+ weights cycles. (this is left as an excercise to the reader[tm]) */
+}
+
+
+
+/*
+ Path-finding algorithm Dijkstra
+
+ - worst-case running time is O( |E| + |V| · log |V| ) thus better than
+ Bellman-Ford, but cannot handle negative edge weights
+ */
+function dijkstra(g, source) {
+ /* initially, all distances are infinite and all predecessors are null */
+ for(var n in g.nodes)
+ g.nodes[n].distance = Infinity;
+ /* predecessors are implicitly null */
+ source.distance = 0;
+ var counter=0;
+ /* set of unoptimized nodes, sorted by their distance (but a Fibonacci heap
+ would be better) */
+ var q = new BinaryMinHeap(g.nodes, "distance");
+
+ var node;
+ /* get the node with the smallest distance */
+ /* as long as we have unoptimized nodes */
+
+ while(q.min()!=undefined) {
+ /* remove the latest */
+ node=q.extractMin();
+ node.optimized=true;
+
+ /* no nodes accessible from this one, should not happen */
+ if(node.distance == Infinity)
+ throw "Orphaned node!";
+
+ /* for each neighbour of node */
+ for(e in node.edges) {
+ if(node.edges[e].target.optimized)
+ continue;
+
+ /* look for an alternative route */
+ var alt = node.distance + node.edges[e].weight;
+
+ /* update distance and route if a better one has been found */
+ if (alt < node.edges[e].target.distance) {
+
+ /* update distance of neighbour */
+ node.edges[e].target.distance = alt;
+
+ /* update priority queue */
+ q.heapify();
+
+ /* update path */
+ node.edges[e].target.predecessor = node;
+ }
+ }
+ }
+}
+
+
+
+/* Runs at worst in O(|V|³) and at best in Omega(|V|³) :-)
+ complexity Sigma(|V|²) */
+/* This implementation is not yet ready for general use, but works with the
+ Dracula graph library. */
+function floyd_warshall(g, source) {
+
+ /* Step 1: initialising empty path matrix (second dimension is implicit) */
+ var path = [];
+ var next = [];
+ var n = g.nodes.length;
+
+ /* construct path matrix, initialize with Infinity */
+ for(j in g.nodes) {
+ path[j] = [];
+ next[j] = [];
+ for(i in g.nodes)
+ path[j][i] = j == i ? 0 : Infinity;
+ }
+
+ /* initialize path with edge weights */
+ for(e in g.edges)
+ path[g.edges[e].source.id][g.edges[e].target.id] = g.edges[e].weight;
+
+ /* Note: Usually, the initialisation is done by getting the edge weights
+ from a node matrix representation of the graph, not by iterating through
+ a list of edges as done here. */
+
+ /* Step 2: find best distances (the heart of Floyd-Warshall) */
+ for(k in g.nodes){
+ for(i in g.nodes) {
+ for(j in g.nodes)
+ if(path[i][j] > path[i][k] + path[k][j]) {
+ path[i][j] = path[i][k] + path[k][j];
+ /* Step 2.b: remember the path */
+ next[i][j] = k;
+ }
+ }
+ }
+ /* Step 3: Path reconstruction */
+ function getPath(i, j) {
+ if(path[i][j] == Infinity)
+ throw "There is no path.";
+ var intermediate = next[i][j];
+ if(intermediate == undefined)
+ return null;
+ else
+ return getPath(i, intermediate)
+ .concat([intermediate])
+ .concat(getPath(intermediate, j));
+ }
+// console&&console.log(path);
+// console&&console.log(next);
+
+ /* TODO use the knowledge */
+}
+
+
+
+/*
+ A simple binary min-heap serving as a priority queue
+ - takes an array as the input, with elements having a key property
+ - elements will look like this:
+ {
+ key: "... key property ...",
+ value: "... element content ..."
+ }
+ - provides insert(), min(), extractMin() and heapify()
+ - example usage (e.g. via the Firebug console):
+ var x = {foo:20,hui:"bla"};
+ var a = new BinaryMinHeap([x,{foo:3},{foo:10},{foo:20},{foo:30},{foo:6},{foo:1},{foo:3}],"foo");
+ console.log(a.extractMin());
+ console.log(a.extractMin());
+ x.ma=0;
+ a.heapify(); // call this when key updated
+ console.log(a.extractMin());
+ console.log(a.extractMin());
+ - can also be used on a simple array, like [9,7,8,5]
+ */
+function BinaryMinHeap(array, key) {
+
+ /* Binary tree stored in an array, no need for a complicated data structure */
+ var tree = [];
+
+ var key = key || 'key';
+
+ /* Calculate the index of the parent or a child */
+ var parent = function(index) { return Math.floor((index - 1)/2); };
+ var right = function(index) { return 2 * index + 2; };
+ var left = function(index) { return 2 * index + 1; };
+
+ /* Helper function to swap elements with their parent
+ as long as the parent is bigger */
+ function bubble_up(i) {
+ var p = parent(i);
+ while((p >= 0) && (tree[i][key] < tree[p][key])) {
+ /* swap with parent */
+ tree[i] = tree.splice(p, 1, tree[i])[0];
+ /* go up one level */
+ i = p;
+ p = parent(i);
+ }
+ }
+
+ /* Helper function to swap elements with the smaller of their children
+ as long as there is one */
+ function bubble_down(i) {
+ var l = left(i);
+ var r = right(i);
+
+ /* as long as there are smaller children */
+ while(tree[l] && (tree[i][key] > tree[l][key]) || tree[r] && (tree[i][key] > tree[r][key])) {
+
+ /* find smaller child */
+ var child = tree[l] ? tree[r] ? tree[l][key] > tree[r][key] ? r : l : l : l;
+
+ /* swap with smaller child with current element */
+ tree[i] = tree.splice(child, 1, tree[i])[0];
+
+ /* go up one level */
+ i = child;
+ l = left(i);
+ r = right(i);
+ }
+ }
+
+ /* Insert a new element with respect to the heap property
+ 1. Insert the element at the end
+ 2. Bubble it up until it is smaller than its parent */
+ this.insert = function(element) {
+
+ /* make sure there's a key property */
+ (element[key] == undefined) && (element = {key:element});
+
+ /* insert element at the end */
+ tree.push(element);
+
+ /* bubble up the element */
+ bubble_up(tree.length - 1);
+ }
+
+ /* Only show us the minimum */
+ this.min = function() {
+ return tree.length == 1 ? undefined : tree[0];
+ }
+
+ /* Return and remove the minimum
+ 1. Take the root as the minimum that we are looking for
+ 2. Move the last element to the root (thereby deleting the root)
+ 3. Compare the new root with both of its children, swap it with the
+ smaller child and then check again from there (bubble down)
+ */
+ this.extractMin = function() {
+ var result = this.min();
+
+ /* move the last element to the root or empty the tree completely */
+ /* bubble down the new root if necessary */
+ (tree.length == 1) && (tree = []) || (tree[0] = tree.pop()) && bubble_down(0);
+
+ return result;
+ }
+
+ /* currently unused, TODO implement */
+ this.changeKey = function(index, key) {
+ throw "function not implemented";
+ }
+
+ this.heapify = function() {
+ for(var start = Math.floor((tree.length - 2) / 2); start >= 0; start--) {
+ bubble_down(start);
+ }
+ }
+
+ //this.debug = function() {console&&console.log("----");for(i in tree){console&&console.log(tree[i].id,tree[i].distance);};}
+ /* insert the input elements one by one only when we don't have a key property (TODO can be done more elegant) */
+// if (key=="key")
+ for(i in (array || []))
+ this.insert(array[i]);
+// else {
+// this.tree = array; // TODO there's an error here, maybe the array needs to be cloned or copied, because all reference is lost after this assignment
+// this.heapify();
+// }
+}
+
+
+
+/*
+ Quick Sort:
+ 1. Select some random value from the array, the median.
+ 2. Divide the array in three smaller arrays according to the elements
+ being less, equal or greater than the median.
+ 3. Recursively sort the array containg the elements less than the
+ median and the one containing elements greater than the median.
+ 4. Concatenate the three arrays (less, equal and greater).
+ 5. One or no element is always sorted.
+ Note: This could be implemented more efficiently by using only one array.
+*/
+function quickSort(arr) {
+ /* recursion anchor: one element is always sorted */
+ if(arr.length <= 1) return arr;
+ /* randomly selecting some value */
+ var median = arr[Math.floor(Math.random() * arr.length)];
+ var arr1 = [], arr2 = [], arr3 = [];
+ for(var i in arr) {
+ arr[i] < median && arr1.push(arr[i]);
+ arr[i] == median && arr2.push(arr[i]);
+ arr[i] > median && arr3.push(arr[i]);
+ }
+ /* recursive sorting and assembling final result */
+ return quickSort(arr1).concat(arr2).concat(quickSort(arr3));
+}
+
+/*
+ Selection Sort:
+ 1. Select the minimum and remove it from the array
+ 2. Sort the rest recursively
+ 3. Return the minimum plus the sorted rest
+ 4. An array with only one element is already sorted
+*/
+function selectionSort(arr) {
+ /* recursion anchor: one element is always sorted */
+ if(arr.length == 1) return arr;
+ var minimum = Infinity;
+ var index;
+ for(var i in arr) {
+ if(arr[i] < minimum) {
+ minimum = arr[i];
+ index = i; /* remember the minimum index for later removal */
+ }
+ }
+ /* remove the minimum */
+ arr.splice(index, 1);
+ /* assemble result and sort recursively (could be easily done iteratively as well)*/
+ return [minimum].concat(selectionSort(arr));
+}
+
+/*
+ Merge Sort:
+ 1. Cut the array in half
+ 2. Sort each of them recursively
+ 3. Merge the two sorted arrays
+ 4. An array with only one element is already sorted
+
+*/
+function mergeSort(arr) {
+ /* merges two sorted arrays into one sorted array */
+ function merge(a, b) {
+ /* result set */
+ var c = [];
+ /* as long as there are elements in the arrays to be merged */
+ while(a.length > 0 || b.length > 0){
+ /* are there elements to be merged, if yes, compare them and merge */
+ var n = a.length > 0 && b.length > 0 ? a[0] < b[0] ? a.shift() : b.shift() : b.length > 0 ? b.shift() : a.length > 0 ? a.shift() : null;
+ /* always push the smaller one onto the result set */
+ n != null && c.push(n);
+ }
+ return c;
+ }
+ /* this mergeSort implementation cuts the array in half, wich should be fine with randomized arrays, but introduces the risk of a worst-case scenario */
+ median = Math.floor(arr.length / 2);
+ var part1 = arr.slice(0, median); /* for some reason it doesn't work if inserted directly in the return statement (tried so with firefox) */
+ var part2 = arr.slice(median - arr.length);
+ return arr.length <= 1 ? arr : merge(
+ mergeSort(part1), /* first half */
+ mergeSort(part2) /* second half */
+ );
+}
+
+/* Balanced Red-Black-Tree */
+function RedBlackTree(arr) {
+
+}
+
+function BTree(arr) {
+
+}
+
+function NaryTree(n, arr) {
+
+}
+
+
+/**
+ * Curry - Function currying
+ * Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
+ * Licensed under BSD (http://www.opensource.org/licenses/bsd-license.php)
+ * Date: 10/4/2008
+ *
+ * @author Ariel Flesler
+ * @version 1.0.1
+ */
+
+function curry( fn ){
+ return function(){
+ var args = curry.args(arguments),
+ master = arguments.callee,
+ self = this;
+
+ return args.length >= fn.length ? fn.apply(self,args) : function(){
+ return master.apply( self, args.concat(curry.args(arguments)) );
+ };
+ };
+};
+
+curry.args = function( args ){
+ return Array.prototype.slice.call(args);
+};
+
+Function.prototype.curry = function(){
+ return curry(this);
+};
diff --git a/htdocs/js/dracula_graffle.js b/htdocs/js/dracula_graffle.js
new file mode 100644
index 0000000..5165035
--- /dev/null
+++ b/htdocs/js/dracula_graffle.js
@@ -0,0 +1,105 @@
+/**
+ * Originally grabbed from the official RaphaelJS Documentation
+ * http://raphaeljs.com/graffle.html
+ * Adopted (arrows) and commented by Philipp Strathausen http://blog.ameisenbar.de
+ * Licenced under the MIT licence.
+ */
+
+/**
+ * Usage:
+ * connect two shapes
+ * parameters:
+ * source shape [or connection for redrawing],
+ * target shape,
+ * style with { fg : linecolor, bg : background color, directed: boolean }
+ * returns:
+ * connection { draw = function() }
+ */
+Raphael.fn.connection = function (obj1, obj2, style) {
+ var selfRef = this;
+ /* create and return new connection */
+ var edge = {/*
+ from : obj1,
+ to : obj2,
+ style : style,*/
+ draw : function() {
+ /* get bounding boxes of target and source */
+ var bb1 = obj1.getBBox();
+ var bb2 = obj2.getBBox();
+ var off1 = 0;
+ var off2 = 0;
+ /* coordinates for potential connection coordinates from/to the objects */
+ var p = [
+ {x: bb1.x + bb1.width / 2, y: bb1.y - off1}, /* NORTH 1 */
+ {x: bb1.x + bb1.width / 2, y: bb1.y + bb1.height + off1}, /* SOUTH 1 */
+ {x: bb1.x - off1, y: bb1.y + bb1.height / 2}, /* WEST 1 */
+ {x: bb1.x + bb1.width + off1, y: bb1.y + bb1.height / 2}, /* EAST 1 */
+ {x: bb2.x + bb2.width / 2, y: bb2.y - off2}, /* NORTH 2 */
+ {x: bb2.x + bb2.width / 2, y: bb2.y + bb2.height + off2}, /* SOUTH 2 */
+ {x: bb2.x - off2, y: bb2.y + bb2.height / 2}, /* WEST 2 */
+ {x: bb2.x + bb2.width + off2, y: bb2.y + bb2.height / 2} /* EAST 2 */
+ ];
+
+ /* distances between objects and according coordinates connection */
+ var d = {}, dis = [];
+
+ /*
+ * find out the best connection coordinates by trying all possible ways
+ */
+ /* loop the first object's connection coordinates */
+ for (var i = 0; i < 4; i++) {
+ /* loop the seond object's connection coordinates */
+ for (var j = 4; j < 8; j++) {
+ var dx = Math.abs(p[i].x - p[j].x),
+ dy = Math.abs(p[i].y - p[j].y);
+ if ((i == j - 4) || (((i != 3 && j != 6) || p[i].x < p[j].x) && ((i != 2 && j != 7) || p[i].x > p[j].x) && ((i != 0 && j != 5) || p[i].y > p[j].y) && ((i != 1 && j != 4) || p[i].y < p[j].y))) {
+ dis.push(dx + dy);
+ d[dis[dis.length - 1].toFixed(3)] = [i, j];
+ }
+ }
+ }
+ var res = dis.length == 0 ? [0, 4] : d[Math.min.apply(Math, dis).toFixed(3)];
+ /* bezier path */
+ var x1 = p[res[0]].x,
+ y1 = p[res[0]].y,
+ x4 = p[res[1]].x,
+ y4 = p[res[1]].y,
+ dx = Math.max(Math.abs(x1 - x4) / 2, 10),
+ dy = Math.max(Math.abs(y1 - y4) / 2, 10),
+ x2 = [x1, x1, x1 - dx, x1 + dx][res[0]].toFixed(3),
+ y2 = [y1 - dy, y1 + dy, y1, y1][res[0]].toFixed(3),
+ x3 = [0, 0, 0, 0, x4, x4, x4 - dx, x4 + dx][res[1]].toFixed(3),
+ y3 = [0, 0, 0, 0, y1 + dy, y1 - dy, y4, y4][res[1]].toFixed(3);
+ /* assemble path and arrow */
+ var path = ["M", x1.toFixed(3), y1.toFixed(3), "C", x2, y2, x3, y3, x4.toFixed(3), y4.toFixed(3)].join(",");
+ /* arrow */
+ if(style && style.directed) {
+ /* magnitude, length of the last path vector */
+ var mag = Math.sqrt((y4 - y3) * (y4 - y3) + (x4 - x3) * (x4 - x3));
+ /* vector normalisation to specified length */
+ var norm = function(x,l){return (-x*(l||5)/mag);};
+ /* calculate array coordinates (two lines orthogonal to the path vector) */
+ var arr = [
+ {x:(norm(x4-x3)+norm(y4-y3)+x4).toFixed(3), y:(norm(y4-y3)+norm(x4-x3)+y4).toFixed(3)},
+ {x:(norm(x4-x3)-norm(y4-y3)+x4).toFixed(3), y:(norm(y4-y3)-norm(x4-x3)+y4).toFixed(3)}
+ ];
+ path = path + ",M"+arr[0].x+","+arr[0].y+",L"+x4+","+y4+",L"+arr[1].x+","+arr[1].y;
+ }
+
+ /* applying path(s) */
+ edge.fg && edge.fg.attr({path:path})
+ || (edge.fg = selfRef.path(path).attr({stroke: style && style.stroke || "#000", fill: "none"}).toBack());
+ edge.bg && edge.bg.attr({path:path})
+ || style && style.fill && (edge.bg = style.fill.split && selfRef.path(path).attr({stroke: style.fill.split("|")[0], fill: "none", "stroke-width": style.fill.split("|")[1] || 3}).toBack());
+ /* setting label */
+ style && style.label
+ && (edge.label && edge.label.attr({x:(x1+x4)/2, y:(y1+y4)/2})
+ || (edge.label = selfRef.text((x1+x4)/2, (y1+y4)/2, style.label).attr({fill: "#000", "font-size":"14px"})));
+// && selfRef.text(x4, y4, style.label).attr({stroke: style && style.stroke || "#fff", "font-weight":"bold", "font-size":"20px"})
+// style && style.callback && style.callback(edge);
+ }
+ }
+ edge.draw();
+ return edge;
+};
+//Raphael.prototype.set.prototype.dodo=function(){console.log("works");};
diff --git a/htdocs/js/dracula_graph.js b/htdocs/js/dracula_graph.js
new file mode 100644
index 0000000..70a43ae
--- /dev/null
+++ b/htdocs/js/dracula_graph.js
@@ -0,0 +1,406 @@
+/*
+ * Dracula Graph Layout and Drawing Framework 0.0.3alpha
+ * (c) 2010 Philipp Strathausen , http://strathausen.eu
+ *
+ * based on the Graph JavaScript framework, version 0.0.1
+ * (c) 2006 Aslak Hellesoy
+ * (c) 2006 Dave Hoover
+ *
+ * Ported from Graph::Layouter::Spring in
+ * http://search.cpan.org/~pasky/Graph-Layderer-0.02/
+ * The algorithm is based on a spring-style layouter of a Java-based social
+ * network tracker PieSpy written by Paul Mutton Epaul@jibble.orgE.
+ *
+ * This code is freely distributable under the terms of an MIT-style license.
+ * For details, see the Graph web site: http://dev.buildpatternd.com/trac
+ *
+ * Links:
+ *
+ * Graph Dracula JavaScript Framework:
+ * http://graphdracula.net
+ *
+ * Demo of the original applet:
+ * http://redsquirrel.com/dave/work/webdep/
+ *
+ * Mirrored original source code at snipplr:
+ * http://snipplr.com/view/1950/graph-javascript-framework-version-001/
+ *
+ * Original usage example:
+ * http://ajaxian.com/archives/new-javascriptcanvas-graph-library
+ *
+/*--------------------------------------------------------------------------*/
+
+
+
+/*
+ * Graph
+ */
+var Graph = function() {
+ this.nodes = [];
+ this.nodelist = []; // nodes by index number, only used once TODO use only one node container
+ this.edges = [];
+ this.snapshots = []; // previous graph states
+};
+Graph.prototype = {
+ /*
+ * add a node
+ * @id the node's ID (string or number)
+ * @content (optional, dictionary) can contain any information that is
+ * being interpreted by the layout algorithm or the graph
+ * representation
+ */
+ addNode: function(id, content) {
+ /* testing if node is already existing in the graph */
+ if(this.nodes[id] == undefined) {
+ this.nodes[id] = new Graph.Node(id, content || {"id" : id}); /* nodes indexed by node id */
+ this.nodelist.push(this.nodes[id]); /* node list indexed by numbers */
+ }
+ return this.nodes[id];
+ },
+
+ // TODO rename style to content
+ addEdge: function(source, target, style) {
+ var s = this.addNode(source);
+ var t = this.addNode(target);
+ var edge = { source: s, target: t, style: style, weight: style&&style.weight||1 }; // TODO tidy up here
+ s.edges.push(edge);
+ this.edges.push(edge);
+ /* add an edge back if graph undirected */
+ if(!style || !style.directed) {
+ var backedge = { source: t, target: s, style: style, weight : style&&style.weight||1, backedge : edge }; // TODO tidy up here
+ this.edges.push(backedge);
+ t.edges.push(backedge);
+ }
+ },
+
+ /*
+ * Preserve a copy of the graph state (nodes, positions, ...)
+ * @comment a comment describing the state
+ * @about a list with objects to be marked as significant in this state (TODO)
+ */
+ snapShot: function(comment, about) {
+ // TODO get rid of the jQuery plugin dependence just for the deep copying
+ var graph = new Graph();
+ jQuery.extend(true, graph.nodes, this.nodes);
+ jQuery.extend(true, graph.nodelist, this.nodelist);
+ jQuery.extend(true, graph.edges, this.edges);
+ graph.snapShot = null;
+ this.snapshots.push({comment: comment, graph: graph});
+ }
+};
+
+/*
+ * Node
+ */
+Graph.Node = function(id, value){
+ value.id = id;
+ value.edges = [];
+ return value;
+};
+Graph.Node.prototype = {
+};
+
+/*
+ * Renderer base class
+ */
+Graph.Renderer = {};
+
+/*
+ * Renderer implementation using RaphaelJS
+ */
+Graph.Renderer.Raphael = function(element, graph, width, height) {
+ this.width = width||400;
+ this.height = height||400;
+ var selfRef = this;
+ this.r = Raphael(element, this.width, this.height);
+ this.radius = 40; /* max dimension of a node */
+ this.graph = graph;
+ this.mouse_in = false;
+
+ /* TODO default node rendering function */
+ if(!this.graph.render) {
+ this.graph.render = function() {
+ return;
+ }
+ }
+
+ /*
+ * Dragging
+ */
+ this.isDrag = false;
+ this.dragger = function (e) {
+ this.dx = e.clientX;
+ this.dy = e.clientY;
+ selfRef.isDrag = this;
+ this.set && this.set.animate({"fill-opacity": .1}, 200) && this.set.toFront();
+ e.preventDefault && e.preventDefault();
+ };
+
+ document.onmousemove = function (e) {
+ e = e || window.event;
+ if (selfRef.isDrag) {
+ var bBox = selfRef.isDrag.set.getBBox();
+ // TODO round the coordinates here (eg. for proper image representation)
+ var newX = e.clientX - selfRef.isDrag.dx + (bBox.x + bBox.width / 2);
+ var newY = e.clientY - selfRef.isDrag.dy + (bBox.y + bBox.height / 2);
+ /* prevent shapes from being dragged out of the canvas */
+ var clientX = e.clientX - (newX < 20 ? newX - 20 : newX > selfRef.width - 20 ? newX - selfRef.width + 20 : 0);
+ var clientY = e.clientY - (newY < 20 ? newY - 20 : newY > selfRef.height - 20 ? newY - selfRef.height + 20 : 0);
+ selfRef.isDrag.set.translate(clientX - selfRef.isDrag.dx, clientY - selfRef.isDrag.dy);
+ for (var i in selfRef.graph.edges) {
+ selfRef.graph.edges[i].connection && selfRef.graph.edges[i].connection.draw();
+ }
+ //selfRef.r.safari();
+ selfRef.isDrag.dx = clientX;
+ selfRef.isDrag.dy = clientY;
+ }
+ };
+ document.onmouseup = function () {
+ selfRef.isDrag && selfRef.isDrag.set.animate({"fill-opacity": .6}, 500);
+ selfRef.isDrag = false;
+ };
+};
+Graph.Renderer.Raphael.prototype = {
+ translate: function(point) {
+ return [
+ Math.round((point[0] - this.graph.layoutMinX) * this.factorX + this.radius),
+ Math.round((point[1] - this.graph.layoutMinY) * this.factorY + this.radius)
+ ];
+ },
+
+ rotate: function(point, length, angle) {
+ var dx = length * Math.cos(angle);
+ var dy = length * Math.sin(angle);
+ return [point[0]+dx, point[1]+dy];
+ },
+
+ draw: function() {
+ this.factorX = (this.width - 2 * this.radius) / (this.graph.layoutMaxX - this.graph.layoutMinX);
+ this.factorY = (this.height - 2 * this.radius) / (this.graph.layoutMaxY - this.graph.layoutMinY);
+ for (i in this.graph.nodes) {
+ this.drawNode(this.graph.nodes[i]);
+ }
+ for (var i = 0; i < this.graph.edges.length; i++) {
+ this.drawEdge(this.graph.edges[i]);
+ }
+ },
+
+ drawNode: function(node) {
+ var point = this.translate([node.layoutPosX, node.layoutPosY]);
+ node.point = point;
+
+ /* if node has already been drawn, move the nodes */
+ if(node.shape) {
+ var oBBox = node.shape.getBBox();
+ var opoint = [ oBBox.x + Math.round(oBBox.width / 2) , oBBox.y + Math.round(oBBox.height / 2) ];
+ node.shape.translate(point[0]-opoint[0], point[1]-opoint[1]);
+ this.r.safari();
+ return;
+ }/* else, draw new nodes */
+ var shape;
+ /* if a node renderer function is provided by the user, then use it */
+ if(node.render) {
+ shape = node.render(this.r, node);
+ /* or check for an ajax representation of the nodes */
+ } else if(node.shape) {
+ // TODO ajax representation
+ /* the default node drawing */
+ } else {
+ var color = Raphael.getColor();
+ shape = this.r.set().
+ push(this.r.ellipse(point[0], point[1], 30, 20).attr({fill: color, stroke: color, "stroke-width": 2})).
+ push(this.r.text(point[0], point[1] + 30, node.label || node.id));
+ }
+ shape.attr({"fill-opacity": .6});
+ /* reference to the node an element belongs to, needed for dragging all elements of a node */
+ shape.items.forEach(function(item){ item.set = shape; item.node.style.cursor = "move"; });
+ shape.mousedown(this.dragger);
+ node.shape = shape;
+ },
+ drawEdge: function(edge) {
+ /* if this edge already exists the other way around and is undirected */
+ if(edge.backedge)
+ return;
+ /* if edge already has been drawn, only refresh the edge */
+ edge.connection && edge.connection.draw();
+ if(!edge.connection) {
+ edge.style && edge.style.callback && edge.style.callback(edge);//TODO move this somewhere else
+ edge.connection = this.r.connection(edge.source.shape, edge.target.shape, edge.style);
+ }
+ }
+};
+Graph.Layout = {};
+Graph.Layout.Spring = function(graph) {
+ this.graph = graph;
+ this.iterations = 500;
+ this.maxRepulsiveForceDistance = 6;
+ this.k = 2;
+ this.c = 0.01;
+ this.maxVertexMovement = 0.5;
+ };
+Graph.Layout.Spring.prototype = {
+ layout: function() {
+ this.layoutPrepare();
+ for (var i = 0; i < this.iterations; i++) {
+ this.layoutIteration();
+ }
+ this.layoutCalcBounds();
+ },
+
+ layoutPrepare: function() {
+ for (i in this.graph.nodes) {
+ var node = this.graph.nodes[i];
+ node.layoutPosX = 0;
+ node.layoutPosY = 0;
+ node.layoutForceX = 0;
+ node.layoutForceY = 0;
+ }
+
+ },
+
+ layoutCalcBounds: function() {
+ var minx = Infinity, maxx = -Infinity, miny = Infinity, maxy = -Infinity;
+
+ for (i in this.graph.nodes) {
+ var x = this.graph.nodes[i].layoutPosX;
+ var y = this.graph.nodes[i].layoutPosY;
+
+ if(x > maxx) maxx = x;
+ if(x < minx) minx = x;
+ if(y > maxy) maxy = y;
+ if(y < miny) miny = y;
+ }
+
+ this.graph.layoutMinX = minx;
+ this.graph.layoutMaxX = maxx;
+ this.graph.layoutMinY = miny;
+ this.graph.layoutMaxY = maxy;
+ },
+
+ layoutIteration: function() {
+ // Forces on nodes due to node-node repulsions
+ for (var i = 0; i < this.graph.nodelist.length; i++) {
+ var node1 = this.graph.nodelist[i];
+ for (var j = i + 1; j < this.graph.nodelist.length; j++) {
+ var node2 = this.graph.nodelist[j];
+ this.layoutRepulsive(node1, node2);
+ }
+ }
+ // Forces on nodes due to edge attractions
+ for (var i = 0; i < this.graph.edges.length; i++) {
+ var edge = this.graph.edges[i];
+ this.layoutAttractive(edge);
+ }
+
+ // Move by the given force
+ for (i in this.graph.nodes) {
+ var node = this.graph.nodes[i];
+ var xmove = this.c * node.layoutForceX;
+ var ymove = this.c * node.layoutForceY;
+
+ var max = this.maxVertexMovement;
+ if(xmove > max) xmove = max;
+ if(xmove < -max) xmove = -max;
+ if(ymove > max) ymove = max;
+ if(ymove < -max) ymove = -max;
+
+ node.layoutPosX += xmove;
+ node.layoutPosY += ymove;
+ node.layoutForceX = 0;
+ node.layoutForceY = 0;
+ }
+ },
+
+ layoutRepulsive: function(node1, node2) {
+ var dx = node2.layoutPosX - node1.layoutPosX;
+ var dy = node2.layoutPosY - node1.layoutPosY;
+ var d2 = dx * dx + dy * dy;
+ if(d2 < 0.01) {
+ dx = 0.1 * Math.random() + 0.1;
+ dy = 0.1 * Math.random() + 0.1;
+ var d2 = dx * dx + dy * dy;
+ }
+ var d = Math.sqrt(d2);
+ if(d < this.maxRepulsiveForceDistance) {
+ var repulsiveForce = this.k * this.k / d;
+ node2.layoutForceX += repulsiveForce * dx / d;
+ node2.layoutForceY += repulsiveForce * dy / d;
+ node1.layoutForceX -= repulsiveForce * dx / d;
+ node1.layoutForceY -= repulsiveForce * dy / d;
+ }
+ },
+
+ layoutAttractive: function(edge) {
+ var node1 = edge.source;
+ var node2 = edge.target;
+
+ var dx = node2.layoutPosX - node1.layoutPosX;
+ var dy = node2.layoutPosY - node1.layoutPosY;
+ var d2 = dx * dx + dy * dy;
+ if(d2 < 0.01) {
+ dx = 0.1 * Math.random() + 0.1;
+ dy = 0.1 * Math.random() + 0.1;
+ var d2 = dx * dx + dy * dy;
+ }
+ var d = Math.sqrt(d2);
+ if(d > this.maxRepulsiveForceDistance) {
+ d = this.maxRepulsiveForceDistance;
+ d2 = d * d;
+ }
+ var attractiveForce = (d2 - this.k * this.k) / this.k;
+ if(edge.attraction == undefined) edge.attraction = 1;
+ attractiveForce *= Math.log(edge.attraction) * 0.5 + 1;
+
+ node2.layoutForceX -= attractiveForce * dx / d;
+ node2.layoutForceY -= attractiveForce * dy / d;
+ node1.layoutForceX += attractiveForce * dx / d;
+ node1.layoutForceY += attractiveForce * dy / d;
+ }
+};
+
+/*
+ * usefull JavaScript extensions,
+ */
+//Array.prototype.forEach = function(f) {
+// var l = this.length;
+// for( var i = 0; i < l; i++) f(this[i]);
+//};
+
+function log(a) {console.log&&console.log(a);}
+
+// this will eventually mess up array usage with for-in-loops
+//Array.prototype.onEach = function(f, arg) {
+// var l = this.length;
+// for( var i = 0; i < l; i++) this[i][f]( arg );
+//}
+
+/*
+ * Raphael Tooltip Plugin
+ * - attaches an element as a tooltip to another element
+ *
+ * Usage example, adding a rectangle as a tooltip to a circle:
+ *
+ * paper.circle(100,100,10).tooltip(paper.rect(0,0,20,30));
+ *
+ * If you want to use more shapes, you'll have to put them into a set.
+ *
+ */
+Raphael.el.tooltip = function (tp) {
+ this.tp = tp;
+ this.tp.o = {x: 0, y: 0};
+ this.tp.hide();
+ this.hover(
+ function(event){
+ this.mousemove(function(event){
+ this.tp.translate(event.clientX -
+ this.tp.o.x,event.clientY - this.tp.o.y);
+ this.tp.o = {x: event.clientX, y: event.clientY};
+ });
+ this.tp.show().toFront();
+ },
+ function(event){
+ this.tp.hide();
+ this.unmousemove();
+ });
+ return this;
+};
diff --git a/htdocs/js/raphael-min.js b/htdocs/js/raphael-min.js
new file mode 100644
index 0000000..8718b5b
--- /dev/null
+++ b/htdocs/js/raphael-min.js
@@ -0,0 +1,7 @@
+/*
+ * Raphael 1.3.1 - JavaScript Vector Library
+ *
+ * Copyright (c) 2008 - 2009 Dmitry Baranovskiy (http://raphaeljs.com)
+ * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
+ */
+Raphael=(function(){var a=/[, ]+/,aO=/^(circle|rect|path|ellipse|text|image)$/,L=document,au=window,l={was:"Raphael" in au,is:au.Raphael},an=function(){if(an.is(arguments[0],"array")){var d=arguments[0],e=w[aW](an,d.splice(0,3+an.is(d[0],al))),S=e.set();for(var R=0,a0=d[m];R
";if(ag.childNodes[m]!=2){return null;}}an.svg=!(an.vml=an.type=="VML");aT[aY]=an[aY];an._id=0;an._oid=0;an.fn={};an.is=function(e,d){d=aZ.call(d);return((d=="object"||d=="undefined")&&typeof e==d)||(e==null&&d=="null")||aZ.call(aw.call(e).slice(8,-1))==d;};an.setWindow=function(d){au=d;L=au.document;};var aD=function(e){if(an.vml){var d=/^\s+|\s+$/g;aD=aj(function(R){var S;R=(R+at)[aP](d,at);try{var a0=new ActiveXObject("htmlfile");a0.write("");a0.close();S=a0.body;}catch(a2){S=createPopup().document.body;}var i=S.createTextRange();try{S.style.color=R;var a1=i.queryCommandValue("ForeColor");a1=((a1&255)<<16)|(a1&65280)|((a1&16711680)>>>16);return"#"+("000000"+a1[aA](16)).slice(-6);}catch(a2){return"none";}});}else{var E=L.createElement("i");E.title="Rapha\xebl Colour Picker";E.style.display="none";L.body[aL](E);aD=aj(function(i){E.style.color=i;return L.defaultView.getComputedStyle(E,at).getPropertyValue("color");});}return aD(e);};an.hsb2rgb=aj(function(a3,a1,a7){if(an.is(a3,"object")&&"h" in a3&&"s" in a3&&"b" in a3){a7=a3.b;a1=a3.s;a3=a3.h;}var R,S,a8;if(a7==0){return{r:0,g:0,b:0,hex:"#000"};}if(a3>1||a1>1||a7>1){a3/=255;a1/=255;a7/=255;}var a0=~~(a3*6),a4=(a3*6)-a0,E=a7*(1-a1),e=a7*(1-(a1*a4)),a9=a7*(1-(a1*(1-a4)));R=[a7,e,E,E,a9,a7,a7][a0];S=[a9,a7,a7,e,E,E,a9][a0];a8=[E,E,a9,a7,a7,e,E][a0];R*=255;S*=255;a8*=255;var a5={r:R,g:S,b:a8},d=(~~R)[aA](16),a2=(~~S)[aA](16),a6=(~~a8)[aA](16);d=d[aP](aU,"0");a2=a2[aP](aU,"0");a6=a6[aP](aU,"0");a5.hex="#"+d+a2+a6;return a5;},an);an.rgb2hsb=aj(function(d,e,a1){if(an.is(d,"object")&&"r" in d&&"g" in d&&"b" in d){a1=d.b;e=d.g;d=d.r;}if(an.is(d,"string")){var a3=an.getRGB(d);d=a3.r;e=a3.g;a1=a3.b;}if(d>1||e>1||a1>1){d/=255;e/=255;a1/=255;}var a0=g(d,e,a1),i=aI(d,e,a1),R,E,S=a0;if(i==a0){return{h:0,s:0,b:a0};}else{var a2=(a0-i);E=a2/a0;if(d==a0){R=(e-a1)/a2;}else{if(e==a0){R=2+((a1-d)/a2);}else{R=4+((d-e)/a2);}}R/=6;R<0&&R++;R>1&&R--;}return{h:R,s:E,b:S};},an);var aE=/,?([achlmqrstvxz]),?/gi;an._path2string=function(){return this.join(",")[aP](aE,"$1");};function aj(E,e,d){function i(){var R=Array[aY].slice.call(arguments,0),a0=R[az]("\u25ba"),S=i.cache=i.cache||{},a1=i.count=i.count||[];if(S[Q](a0)){return d?d(S[a0]):S[a0];}a1[m]>=1000&&delete S[a1.shift()];a1[f](a0);S[a0]=E[aW](e,R);return d?d(S[a0]):S[a0];}return i;}an.getRGB=aj(function(d){if(!d||!!((d=d+at).indexOf("-")+1)){return{r:-1,g:-1,b:-1,hex:"none",error:1};}if(d=="none"){return{r:-1,g:-1,b:-1,hex:"none"};}!(({hs:1,rg:1})[Q](d.substring(0,2))||d.charAt()=="#")&&(d=aD(d));var S,i,E,a2,a3,a0=d.match(x);if(a0){if(a0[2]){a2=G(a0[2].substring(5),16);E=G(a0[2].substring(3,5),16);i=G(a0[2].substring(1,3),16);}if(a0[3]){a2=G((a3=a0[3].charAt(3))+a3,16);E=G((a3=a0[3].charAt(2))+a3,16);i=G((a3=a0[3].charAt(1))+a3,16);}if(a0[4]){a0=a0[4][z](/\s*,\s*/);i=W(a0[0]);E=W(a0[1]);a2=W(a0[2]);}if(a0[5]){a0=a0[5][z](/\s*,\s*/);i=W(a0[0])*2.55;E=W(a0[1])*2.55;a2=W(a0[2])*2.55;}if(a0[6]){a0=a0[6][z](/\s*,\s*/);i=W(a0[0]);E=W(a0[1]);a2=W(a0[2]);return an.hsb2rgb(i,E,a2);}if(a0[7]){a0=a0[7][z](/\s*,\s*/);i=W(a0[0])*2.55;E=W(a0[1])*2.55;a2=W(a0[2])*2.55;return an.hsb2rgb(i,E,a2);}a0={r:i,g:E,b:a2};var e=(~~i)[aA](16),R=(~~E)[aA](16),a1=(~~a2)[aA](16);e=e[aP](aU,"0");R=R[aP](aU,"0");a1=a1[aP](aU,"0");a0.hex="#"+e+R+a1;return a0;}return{r:-1,g:-1,b:-1,hex:"none",error:1};},an);an.getColor=function(e){var i=this.getColor.start=this.getColor.start||{h:0,s:1,b:e||0.75},d=this.hsb2rgb(i.h,i.s,i.b);i.h+=0.075;if(i.h>1){i.h=0;i.s-=0.2;i.s<=0&&(this.getColor.start={h:0,s:1,b:i.b});}return d.hex;};an.getColor.reset=function(){delete this.start;};an.parsePathString=aj(function(d){if(!d){return null;}var i={a:7,c:6,h:1,l:2,m:2,q:4,s:4,t:2,v:1,z:0},e=[];if(an.is(d,"array")&&an.is(d[0],"array")){e=av(d);}if(!e[m]){(d+at)[aP](/([achlmqstvz])[\s,]*((-?\d*\.?\d*(?:e[-+]?\d+)?\s*,?\s*)+)/ig,function(R,E,a1){var a0=[],S=aZ.call(E);a1[aP](/(-?\d*\.?\d*(?:e[-+]?\d+)?)\s*,?\s*/ig,function(a3,a2){a2&&a0[f](+a2);});while(a0[m]>=i[S]){e[f]([E][aS](a0.splice(0,i[S])));if(!i[S]){break;}}});}e[aA]=an._path2string;return e;});an.findDotsAtSegment=function(e,d,be,bc,a0,R,a2,a1,a8){var a6=1-a8,a5=aM(a6,3)*e+aM(a6,2)*3*a8*be+a6*3*a8*a8*a0+aM(a8,3)*a2,a3=aM(a6,3)*d+aM(a6,2)*3*a8*bc+a6*3*a8*a8*R+aM(a8,3)*a1,ba=e+2*a8*(be-e)+a8*a8*(a0-2*be+e),a9=d+2*a8*(bc-d)+a8*a8*(R-2*bc+d),bd=be+2*a8*(a0-be)+a8*a8*(a2-2*a0+be),bb=bc+2*a8*(R-bc)+a8*a8*(a1-2*R+bc),a7=(1-a8)*e+a8*be,a4=(1-a8)*d+a8*bc,E=(1-a8)*a0+a8*a2,i=(1-a8)*R+a8*a1,S=(90-ab.atan((ba-bd)/(a9-bb))*180/ab.PI);(ba>bd||a91){bi=ab.sqrt(by)*bi;bg=ab.sqrt(by)*bg;}var E=bi*bi,br=bg*bg,bt=(a4==S?-1:1)*ab.sqrt(ab.abs((E*br-E*bn*bn-br*bo*bo)/(E*bn*bn+br*bo*bo))),bd=bt*bi*bn/bg+(a9+a8)/2,bc=bt*-bg*bo/bi+(bE+bD)/2,a3=ab.asin(((bE-bc)/bg).toFixed(7)),a2=ab.asin(((bD-bc)/bg).toFixed(7));a3=a9a2){a3=a3-R*2;}if(!S&&a2>a3){a2=a2-R*2;}}else{a3=bb[0];a2=bb[1];bd=bb[2];bc=bb[3];}var a7=a2-a3;if(ab.abs(a7)>bf){var be=a2,bh=a8,a5=bD;a2=a3+bf*(S&&a2>a3?1:-1);a8=bd+bi*ab.cos(a2);bD=bc+bg*ab.sin(a2);bm=K(a8,bD,bi,bg,ba,0,S,bh,a5,[a2,be,bd,bc]);}a7=a2-a3;var a1=ab.cos(a3),bC=ab.sin(a3),a0=ab.cos(a2),bB=ab.sin(a2),bp=ab.tan(a7/4),bs=4/3*bi*bp,bq=4/3*bg*bp,bz=[a9,bE],bx=[a9+bs*bC,bE-bq*a1],bw=[a8+bs*bB,bD-bq*a0],bu=[a8,bD];bx[0]=2*bz[0]-bx[0];bx[1]=2*bz[1]-bx[1];if(bb){return[bx,bw,bu][aS](bm);}else{bm=[bx,bw,bu][aS](bm)[az]()[z](",");var bk=[];for(var bv=0,bl=bm[m];bv1000000000000&&(a0=0.5);ab.abs(S)>1000000000000&&(S=0.5);if(a0>0&&a0<1){e=M(i,d,R,E,a9,a8,a5,a2,a0);a6[f](e.x);a3[f](e.y);}if(S>0&&S<1){e=M(i,d,R,E,a9,a8,a5,a2,S);a6[f](e.x);a3[f](e.y);}a7=(a8-2*E+d)-(a2-2*a8+E);a4=2*(E-d)-2*(a8-E);a1=d-E;a0=(-a4+ab.sqrt(a4*a4-4*a7*a1))/2/a7;S=(-a4-ab.sqrt(a4*a4-4*a7*a1))/2/a7;ab.abs(a0)>1000000000000&&(a0=0.5);ab.abs(S)>1000000000000&&(S=0.5);if(a0>0&&a0<1){e=M(i,d,R,E,a9,a8,a5,a2,a0);a6[f](e.x);a3[f](e.y);}if(S>0&&S<1){e=M(i,d,R,E,a9,a8,a5,a2,S);a6[f](e.x);a3[f](e.y);}return{min:{x:aI[aW](0,a6),y:aI[aW](0,a3)},max:{x:g[aW](0,a6),y:g[aW](0,a3)}};}),H=aj(function(a9,a4){var R=r(a9),a5=a4&&r(a4),a6={x:0,y:0,bx:0,by:0,X:0,Y:0,qx:null,qy:null},d={x:0,y:0,bx:0,by:0,X:0,Y:0,qx:null,qy:null},a0=function(ba,bb){var i,bc;if(!ba){return["C",bb.x,bb.y,bb.x,bb.y,bb.x,bb.y];}!(ba[0] in {T:1,Q:1})&&(bb.qx=bb.qy=null);switch(ba[0]){case"M":bb.X=ba[1];bb.Y=ba[2];break;case"A":ba=["C"][aS](K[aW](0,[bb.x,bb.y][aS](ba.slice(1))));break;case"S":i=bb.x+(bb.x-(bb.bx||bb.x));bc=bb.y+(bb.y-(bb.by||bb.y));ba=["C",i,bc][aS](ba.slice(1));break;case"T":bb.qx=bb.x+(bb.x-(bb.qx||bb.x));bb.qy=bb.y+(bb.y-(bb.qy||bb.y));ba=["C"][aS](aK(bb.x,bb.y,bb.qx,bb.qy,ba[1],ba[2]));break;case"Q":bb.qx=ba[1];bb.qy=ba[2];ba=["C"][aS](aK(bb.x,bb.y,ba[1],ba[2],ba[3],ba[4]));break;case"L":ba=["C"][aS](aX(bb.x,bb.y,ba[1],ba[2]));break;case"H":ba=["C"][aS](aX(bb.x,bb.y,ba[1],bb.y));break;case"V":ba=["C"][aS](aX(bb.x,bb.y,bb.x,ba[1]));break;case"Z":ba=["C"][aS](aX(bb.x,bb.y,bb.X,bb.Y));break;}return ba;},e=function(ba,bb){if(ba[bb][m]>7){ba[bb].shift();var bc=ba[bb];while(bc[m]){ba.splice(bb++,0,["C"][aS](bc.splice(0,6)));}ba.splice(bb,1);a7=g(R[m],a5&&a5[m]||0);}},E=function(be,bd,bb,ba,bc){if(be&&bd&&be[bc][0]=="M"&&bd[bc][0]!="M"){bd.splice(bc,0,["M",ba.x,ba.y]);bb.bx=0;bb.by=0;bb.x=be[bc][1];bb.y=be[bc][2];a7=g(R[m],a5&&a5[m]||0);}};for(var a2=0,a7=g(R[m],a5&&a5[m]||0);a23){return{container:1,x:arguments[0],y:arguments[1],width:arguments[2],height:arguments[3]};}}},aG=function(d,i){var e=this;for(var E in i){if(i[Q](E)&&!(E in d)){switch(typeof i[E]){case"function":(function(R){d[E]=d===e?R:function(){return R[aW](e,arguments);};})(i[E]);break;case"object":d[E]=d[E]||{};aG.call(this,d[E],i[E]);break;default:d[E]=i[E];break;}}}},ak=function(d,e){d==e.top&&(e.top=d.prev);d==e.bottom&&(e.bottom=d.next);d.next&&(d.next.prev=d.prev);d.prev&&(d.prev.next=d.next);},Y=function(d,e){if(e.top===d){return;}ak(d,e);d.next=null;d.prev=e.top;e.top.next=d;e.top=d;},k=function(d,e){if(e.bottom===d){return;}ak(d,e);d.next=e.bottom;d.prev=null;e.bottom.prev=d;e.bottom=d;},A=function(e,d,i){ak(e,i);d==i.top&&(i.top=e);d.next&&(d.next.prev=e);e.next=d.next;e.prev=d;d.next=e;},aq=function(e,d,i){ak(e,i);d==i.bottom&&(i.bottom=e);d.prev&&(d.prev.next=e);e.prev=d.prev;d.prev=e;e.next=d;},s=function(d){return function(){throw new Error("Rapha\xebl: you are calling to method \u201c"+d+"\u201d of removed object");};},ar=/^r(?:\(([^,]+?)\s*,\s*([^\)]+?)\))?/;if(an.svg){aT[aY].svgns="http://www.w3.org/2000/svg";aT[aY].xlink="http://www.w3.org/1999/xlink";var O=function(d){return +d+(~~d===d)*0.5;},V=function(S){for(var e=0,E=S[m];e0.5)*2-1);aM(a1-0.5,2)+aM(S-0.5,2)>0.25&&(S=ab.sqrt(0.25-aM(a1-0.5,2))*ba+0.5)&&S!=0.5&&(S=S.toFixed(5)-0.00001*ba);}return at;});a7=a7[z](/\s*\-\s*/);if(a4=="linear"){var a0=a7.shift();a0=-W(a0);if(isNaN(a0)){return null;}var R=[0,0,ab.cos(a0*ab.PI/180),ab.sin(a0*ab.PI/180)],a6=1/(g(ab.abs(R[2]),ab.abs(R[3]))||1);R[2]*=a6;R[3]*=a6;if(R[2]<0){R[0]=-R[2];R[2]=0;}if(R[3]<0){R[1]=-R[3];R[3]=0;}}var a3=p(a7);if(!a3){return null;}var e=aJ(a4+"Gradient");e.id="r"+(an._id++)[aA](36);aJ(e,a4=="radial"?{fx:a1,fy:S}:{x1:R[0],y1:R[1],x2:R[2],y2:R[3]});d.defs[aL](e);for(var a2=0,a8=a3[m];a2a1.height)&&(a1.height=a0.y+a0.height-a1.y);(a0.x+a0.width-a1.x>a1.width)&&(a1.width=a0.x+a0.width-a1.x);}}E&&this.hide();return a1;};ax[aY].attr=function(){if(this.removed){return this;}if(arguments[m]==0){var R={};for(var E in this.attrs){if(this.attrs[Q](E)){R[E]=this.attrs[E];}}this._.rt.deg&&(R.rotation=this.rotate());(this._.sx!=1||this._.sy!=1)&&(R.scale=this.scale());R.gradient&&R.fill=="none"&&(R.fill=R.gradient)&&delete R.gradient;return R;}if(arguments[m]==1&&an.is(arguments[0],"string")){if(arguments[0]=="translation"){return t.call(this);}if(arguments[0]=="rotation"){return this.rotate();}if(arguments[0]=="scale"){return this.scale();}if(arguments[0]=="fill"&&this.attrs.fill=="none"&&this.attrs.gradient){return this.attrs.gradient;}return this.attrs[arguments[0]];}if(arguments[m]==1&&an.is(arguments[0],"array")){var d={};for(var e in arguments[0]){if(arguments[0][Q](e)){d[arguments[0][e]]=this.attrs[arguments[0][e]];}}return d;}if(arguments[m]==2){var S={};S[arguments[0]]=arguments[1];aa(this,S);}else{if(arguments[m]==1&&an.is(arguments[0],"object")){aa(this,arguments[0]);}}return this;};ax[aY].toFront=function(){if(this.removed){return this;}this.node.parentNode[aL](this.node);var d=this.paper;d.top!=this&&Y(this,d);return this;};ax[aY].toBack=function(){if(this.removed){return this;}if(this.node.parentNode.firstChild!=this.node){this.node.parentNode.insertBefore(this.node,this.node.parentNode.firstChild);k(this,this.paper);var d=this.paper;}return this;};ax[aY].insertAfter=function(d){if(this.removed){return this;}var e=d.node;if(e.nextSibling){e.parentNode.insertBefore(this.node,e.nextSibling);}else{e.parentNode[aL](this.node);}A(this,d,this.paper);return this;};ax[aY].insertBefore=function(d){if(this.removed){return this;}var e=d.node;e.parentNode.insertBefore(this.node,e);aq(this,d,this.paper);return this;};var P=function(e,d,S,R){d=O(d);S=O(S);var E=aJ("circle");e.canvas&&e.canvas[aL](E);var i=new ax(E,e);i.attrs={cx:d,cy:S,r:R,fill:"none",stroke:"#000"};i.type="circle";aJ(E,i.attrs);return i;};var aF=function(i,d,a1,e,S,a0){d=O(d);a1=O(a1);var R=aJ("rect");i.canvas&&i.canvas[aL](R);var E=new ax(R,i);E.attrs={x:d,y:a1,width:e,height:S,r:a0||0,rx:a0||0,ry:a0||0,fill:"none",stroke:"#000"};E.type="rect";aJ(R,E.attrs);return E;};var ai=function(e,d,a0,S,R){d=O(d);a0=O(a0);var E=aJ("ellipse");e.canvas&&e.canvas[aL](E);var i=new ax(E,e);i.attrs={cx:d,cy:a0,rx:S,ry:R,fill:"none",stroke:"#000"};i.type="ellipse";aJ(E,i.attrs);return i;};var o=function(i,a0,d,a1,e,S){var R=aJ("image");aJ(R,{x:d,y:a1,width:e,height:S,preserveAspectRatio:"none"});R.setAttributeNS(i.xlink,"href",a0);i.canvas&&i.canvas[aL](R);var E=new ax(R,i);E.attrs={x:d,y:a1,width:e,height:S,src:a0};E.type="image";return E;};var X=function(e,d,S,R){var E=aJ("text");aJ(E,{x:d,y:S,"text-anchor":"middle"});e.canvas&&e.canvas[aL](E);var i=new ax(E,e);i.attrs={x:d,y:S,"text-anchor":"middle",text:R,font:j.font,stroke:"none",fill:"#000"};i.type="text";aa(i,i.attrs);return i;};var aV=function(e,d){this.width=e||this.width;this.height=d||this.height;this.canvas[v]("width",this.width);this.canvas[v]("height",this.height);return this;};var w=function(){var E=ao[aW](null,arguments),i=E&&E.container,e=E.x,a0=E.y,R=E.width,d=E.height;if(!i){throw new Error("SVG container not found.");}var S=aJ("svg");R=R||512;d=d||342;aJ(S,{xmlns:"http://www.w3.org/2000/svg",version:1.1,width:R,height:d});if(i==1){S.style.cssText="position:absolute;left:"+e+"px;top:"+a0+"px";L.body[aL](S);}else{if(i.firstChild){i.insertBefore(S,i.firstChild);}else{i[aL](S);}}i=new aT;i.width=R;i.height=d;i.canvas=S;aG.call(i,i,an.fn);i.clear();return i;};aT[aY].clear=function(){var d=this.canvas;while(d.firstChild){d.removeChild(d.firstChild);}this.bottom=this.top=null;(this.desc=aJ("desc"))[aL](L.createTextNode("Created with Rapha\xebl"));d[aL](this.desc);d[aL](this.defs=aJ("defs"));};aT[aY].remove=function(){this.canvas.parentNode&&this.canvas.parentNode.removeChild(this.canvas);for(var d in this){this[d]=s(d);}};}if(an.vml){var aH=function(a8){var a5=/[ahqstv]/ig,a0=r;(a8+at).match(a5)&&(a0=H);a5=/[clmz]/g;if(a0==r&&!(a8+at).match(a5)){var e={M:"m",L:"l",C:"c",Z:"x",m:"t",l:"r",c:"v",z:"x"},R=/([clmz]),?([^clmz]*)/gi,S=/-?[^,\s-]+/g;var a4=(a8+at)[aP](R,function(a9,bb,i){var ba=[];i[aP](S,function(bc){ba[f](O(bc));});return e[bb]+ba;});return a4;}var a6=a0(a8),E,a4=[],d;for(var a2=0,a7=a6[m];a21&&(e=1);a7.opacity=e;}a8.fill&&(a7.on=true);if(a7.on==null||a8.fill=="none"){a7.on=false;}if(a7.on&&a8.fill){var i=a8.fill.match(c);if(i){a7.src=i[1];a7.type="tile";}else{a7.color=an.getRGB(a8.fill).hex;a7.src=at;a7.type="solid";if(an.getRGB(a8.fill).error&&(bd.type in {circle:1,ellipse:1}||(a8.fill+at).charAt()!="r")&&b(bd,a8.fill)){a9.fill="none";a9.gradient=a8.fill;}}}ba&&a6[aL](a7);var R=(a6.getElementsByTagName("stroke")&&a6.getElementsByTagName("stroke")[0]),bb=false;!R&&(bb=R=ah("stroke"));if((a8.stroke&&a8.stroke!="none")||a8["stroke-width"]||a8["stroke-opacity"]!=null||a8["stroke-dasharray"]||a8["stroke-miterlimit"]||a8["stroke-linejoin"]||a8["stroke-linecap"]){R.on=true;}(a8.stroke=="none"||R.on==null||a8.stroke==0||a8["stroke-width"]==0)&&(R.on=false);R.on&&a8.stroke&&(R.color=an.getRGB(a8.stroke).hex);var e=((+a9["stroke-opacity"]+1||2)-1)*((+a9.opacity+1||2)-1),a4=(W(a8["stroke-width"])||1)*0.75;e<0&&(e=0);e>1&&(e=1);a8["stroke-width"]==null&&(a4=a9["stroke-width"]);a8["stroke-width"]&&(R.weight=a4);a4&&a4<1&&(e*=a4)&&(R.weight=1);R.opacity=e;a8["stroke-linejoin"]&&(R.joinstyle=a8["stroke-linejoin"]||"miter");R.miterlimit=a8["stroke-miterlimit"]||8;a8["stroke-linecap"]&&(R.endcap=a8["stroke-linecap"]=="butt"?"flat":a8["stroke-linecap"]=="square"?"square":"round");if(a8["stroke-dasharray"]){var a5={"-":"shortdash",".":"shortdot","-.":"shortdashdot","-..":"shortdashdotdot",". ":"dot","- ":"dash","--":"longdash","- .":"dashdot","--.":"longdashdot","--..":"longdashdotdot"};R.dashstyle=a5[Q](a8["stroke-dasharray"])?a5[a8["stroke-dasharray"]]:at;}bb&&a6[aL](R);}if(bd.type=="text"){var a0=bd.paper.span.style;a9.font&&(a0.font=a9.font);a9["font-family"]&&(a0.fontFamily=a9["font-family"]);a9["font-size"]&&(a0.fontSize=a9["font-size"]);a9["font-weight"]&&(a0.fontWeight=a9["font-weight"]);a9["font-style"]&&(a0.fontStyle=a9["font-style"]);bd.node.string&&(bd.paper.span.innerHTML=(bd.node.string+at)[aP](/"));bd.W=a9.w=bd.paper.span.offsetWidth;bd.H=a9.h=bd.paper.span.offsetHeight;bd.X=a9.x;bd.Y=a9.y+O(bd.H/2);switch(a9["text-anchor"]){case"start":bd.node.style["v-text-align"]="left";bd.bbx=O(bd.W/2);break;case"end":bd.node.style["v-text-align"]="right";bd.bbx=-O(bd.W/2);break;default:bd.node.style["v-text-align"]="center";break;}}};var b=function(d,a1){d.attrs=d.attrs||{};var a2=d.attrs,a4=d.node.getElementsByTagName("fill"),S="linear",a0=".5 .5";d.attrs.gradient=a1;a1=(a1+at)[aP](ar,function(a6,a7,i){S="radial";if(a7&&i){a7=W(a7);i=W(i);aM(a7-0.5,2)+aM(i-0.5,2)>0.25&&(i=ab.sqrt(0.25-aM(a7-0.5,2))*((i>0.5)*2-1)+0.5);a0=a7+am+i;}return at;});a1=a1[z](/\s*\-\s*/);if(S=="linear"){var e=a1.shift();e=-W(e);if(isNaN(e)){return null;}}var R=p(a1);if(!R){return null;}d=d.shape||d.node;a4=a4[0]||ah("fill");if(R[m]){a4.on=true;a4.method="none";a4.type=(S=="radial")?"gradientradial":"gradient";a4.color=R[0].color;a4.color2=R[R[m]-1].color;var a5=[];for(var E=0,a3=R[m];E');};}catch(af){ah=function(d){return L.createElement("<"+d+' xmlns="urn:schemas-microsoft.com:vml" class="rvml">');};}var w=function(){var i=ao[aW](null,arguments),d=i.container,a2=i.height,a3,e=i.width,a1=i.x,a0=i.y;if(!d){throw new Error("VML container not found.");}var R=new aT,S=R.canvas=L.createElement("div"),E=S.style;e=e||512;a2=a2||342;e==+e&&(e+="px");a2==+a2&&(a2+="px");R.width=1000;R.height=1000;R.coordsize="1000 1000";R.coordorigin="0 0";R.span=L.createElement("span");R.span.style.cssText="position:absolute;left:-9999em;top:-9999em;padding:0;margin:0;line-height:1;display:inline;";S[aL](R.span);E.cssText=an.format("width:{0};height:{1};position:absolute;clip:rect(0 {0} {1} 0);overflow:hidden",e,a2);if(d==1){L.body[aL](S);E.left=a1+"px";E.top=a0+"px";}else{d.style.width=e;d.style.height=a2;if(d.firstChild){d.insertBefore(S,d.firstChild);}else{d[aL](S);}}aG.call(R,R,an.fn);return R;};aT[aY].clear=function(){this.canvas.innerHTML=at;this.span=L.createElement("span");this.span.style.cssText="position:absolute;left:-9999em;top:-9999em;padding:0;margin:0;line-height:1;display:inline;";this.canvas[aL](this.span);this.bottom=this.top=null;};aT[aY].remove=function(){this.canvas.parentNode.removeChild(this.canvas);for(var d in this){this[d]=s(d);}};}if((/^Apple|^Google/).test(navigator.vendor)&&!(navigator.userAgent.indexOf("Version/4.0")+1)){aT[aY].safari=function(){var d=this.rect(-99,-99,this.width+99,this.height+99);setTimeout(function(){d.remove();});};}else{aT[aY].safari=function(){};}var ae=(function(){if(L.addEventListener){return function(R,i,e,d){var E=function(S){return e.call(d,S);};R.addEventListener(i,E,false);return function(){R.removeEventListener(i,E,false);return true;};};}else{if(L.attachEvent){return function(S,E,i,e){var R=function(a0){return i.call(e,a0||au.event);};S.attachEvent("on"+E,R);var d=function(){S.detachEvent("on"+E,R);return true;};return d;};}}})();for(var ac=F[m];ac--;){(function(d){ax[aY][d]=function(e){if(an.is(e,"function")){this.events=this.events||[];this.events.push({name:d,f:e,unbind:ae(this.shape||this.node,d,e,this)});}return this;};ax[aY]["un"+d]=function(E){var i=this.events,e=i[m];while(e--){if(i[e].name==d&&i[e].f==E){i[e].unbind();i.splice(e,1);!i.length&&delete this.events;return this;}}return this;};})(F[ac]);}ax[aY].hover=function(e,d){return this.mouseover(e).mouseout(d);};ax[aY].unhover=function(e,d){return this.unmouseover(e).unmouseout(d);};aT[aY].circle=function(d,i,e){return P(this,d||0,i||0,e||0);};aT[aY].rect=function(d,R,e,i,E){return aF(this,d||0,R||0,e||0,i||0,E||0);};aT[aY].ellipse=function(d,E,i,e){return ai(this,d||0,E||0,i||0,e||0);};aT[aY].path=function(d){d&&!an.is(d,"string")&&!an.is(d[0],"array")&&(d+=at);return q(an.format[aW](an,arguments),this);};aT[aY].image=function(E,d,R,e,i){return o(this,E||"about:blank",d||0,R||0,e||0,i||0);};aT[aY].text=function(d,i,e){return X(this,d||0,i||0,e||at);};aT[aY].set=function(d){arguments[m]>1&&(d=Array[aY].splice.call(arguments,0,arguments[m]));return new T(d);};aT[aY].setSize=aV;aT[aY].top=aT[aY].bottom=null;aT[aY].raphael=an;function u(){return this.x+am+this.y;}ax[aY].scale=function(a6,a5,E,e){if(a6==null&&a5==null){return{x:this._.sx,y:this._.sy,toString:u};}a5=a5||a6;!+a5&&(a5=a6);var ba,a8,a9,a7,bm=this.attrs;if(a6!=0){var a4=this.getBBox(),a1=a4.x+a4.width/2,R=a4.y+a4.height/2,bl=a6/this._.sx,bk=a5/this._.sy;E=(+E||E==0)?E:a1;e=(+e||e==0)?e:R;var a3=~~(a6/ab.abs(a6)),a0=~~(a5/ab.abs(a5)),be=this.node.style,bo=E+(a1-E)*bl,bn=e+(R-e)*bk;switch(this.type){case"rect":case"image":var a2=bm.width*a3*bl,bd=bm.height*a0*bk;this.attr({height:bd,r:bm.r*aI(a3*bl,a0*bk),width:a2,x:bo-a2/2,y:bn-bd/2});break;case"circle":case"ellipse":this.attr({rx:bm.rx*a3*bl,ry:bm.ry*a0*bk,r:bm.r*aI(a3*bl,a0*bk),cx:bo,cy:bn});break;case"path":var bg=ad(bm.path),bh=true;for(var bj=0,bc=bg[m];bjS){if(e&&!a8.start){a6=an.findDotsAtSegment(a5,a4,E[1],E[2],E[3],E[4],E[5],E[6],(S-a3)/a1);R+=["C",a6.start.x,a6.start.y,a6.m.x,a6.m.y,a6.x,a6.y];if(a0){return R;}a8.start=R;R=["M",a6.x,a6.y+"C",a6.n.x,a6.n.y,a6.end.x,a6.end.y,E[5],E[6]][az]();a3+=a1;a5=+E[5];a4=+E[6];continue;}if(!d&&!e){a6=an.findDotsAtSegment(a5,a4,E[1],E[2],E[3],E[4],E[5],E[6],(S-a3)/a1);return{x:a6.x,y:a6.y,alpha:a6.alpha};}}a3+=a1;a5=+E[5];a4=+E[6];}R+=E;}a8.end=R;a6=d?a3:e?a8:an.findDotsAtSegment(a5,a4,E[1],E[2],E[3],E[4],E[5],E[6],1);a6.alpha&&(a6={x:a6.x,y:a6.y,alpha:a6.alpha});return a6;};},n=aj(function(E,d,a0,S,a6,a5,a4,a3){var R={x:0,y:0},a2=0;for(var a1=0;a1<1.01;a1+=0.01){var e=M(E,d,a0,S,a6,a5,a4,a3,a1);a1&&(a2+=ab.sqrt(aM(R.x-e.x,2)+aM(R.y-e.y,2)));R=e;}return a2;});var ap=aB(1),C=aB(),J=aB(0,1);ax[aY].getTotalLength=function(){if(this.type!="path"){return;}return ap(this.attrs.path);};ax[aY].getPointAtLength=function(d){if(this.type!="path"){return;}return C(this.attrs.path,d);};ax[aY].getSubpath=function(i,e){if(this.type!="path"){return;}if(ab.abs(this.getTotalLength()-e)<0.000001){return J(this.attrs.path,i).end;}var d=J(this.attrs.path,e,1);return i?J(d,i).end:d;};an.easing_formulas={linear:function(d){return d;},"<":function(d){return aM(d,3);},">":function(d){return aM(d-1,3)+1;},"<>":function(d){d=d*2;if(d<1){return aM(d,3)/2;}d-=2;return(aM(d,3)+2)/2;},backIn:function(e){var d=1.70158;return e*e*((d+1)*e-d);},backOut:function(e){e=e-1;var d=1.70158;return e*e*((d+1)*e+d)+1;},elastic:function(i){if(i==0||i==1){return i;}var e=0.3,d=e/4;return aM(2,-10*i)*ab.sin((i-d)*(2*ab.PI)/e)+1;},bounce:function(E){var e=7.5625,i=2.75,d;if(E<(1/i)){d=e*E*E;}else{if(E<(2/i)){E-=(1.5/i);d=e*E*E+0.75;}else{if(E<(2.5/i)){E-=(2.25/i);d=e*E*E+0.9375;}else{E-=(2.625/i);d=e*E*E+0.984375;}}}return d;}};var I={length:0},aR=function(){var a2=+new Date;for(var be in I){if(be!="length"&&I[Q](be)){var bj=I[be];if(bj.stop){delete I[be];I[m]--;continue;}var a0=a2-bj.start,bb=bj.ms,ba=bj.easing,bf=bj.from,a7=bj.diff,E=bj.to,a6=bj.t,a9=bj.prev||0,a1=bj.el,R=bj.callback,a8={},d;if(a0255?255:(d<0?0:d);},t=function(d,i){if(d==null){return{x:this._.tx,y:this._.ty,toString:u};}this._.tx+=+d;this._.ty+=+i;switch(this.type){case"circle":case"ellipse":this.attr({cx:+d+this.attrs.cx,cy:+i+this.attrs.cy});break;case"rect":case"image":case"text":this.attr({x:+d+this.attrs.x,y:+i+this.attrs.y});break;case"path":var e=ad(this.attrs.path);e[0][1]+=+d;e[0][2]+=+i;this.attr({path:e});break;}return this;};ax[aY].animateWith=function(e,i,d,R,E){I[e.id]&&(i.start=I[e.id].start);return this.animate(i,d,R,E);};ax[aY].animateAlong=ay();ax[aY].animateAlongBack=ay(1);function ay(d){return function(E,i,e,S){var R={back:d};an.is(e,"function")?(S=e):(R.rot=e);E&&E.constructor==ax&&(E=E.attrs.path);E&&(R.along=E);return this.animate(R,i,S);};}ax[aY].onAnimation=function(d){this._run=d||0;return this;};ax[aY].animate=function(be,a5,a4,E){if(an.is(a4,"function")||!a4){E=a4||null;}var a9={},e={},a2={};for(var a6 in be){if(be[Q](a6)){if(Z[Q](a6)){a9[a6]=this.attr(a6);(a9[a6]==null)&&(a9[a6]=j[a6]);e[a6]=be[a6];switch(Z[a6]){case"along":var bc=ap(be[a6]),a7=C(be[a6],bc*!!be.back),R=this.getBBox();a2[a6]=bc/a5;a2.tx=R.x;a2.ty=R.y;a2.sx=a7.x;a2.sy=a7.y;e.rot=be.rot;e.back=be.back;e.len=bc;be.rot&&(a2.r=W(this.rotate())||0);break;case"number":a2[a6]=(e[a6]-a9[a6])/a5;break;case"colour":a9[a6]=an.getRGB(a9[a6]);var a8=an.getRGB(e[a6]);a2[a6]={r:(a8.r-a9[a6].r)/a5,g:(a8.g-a9[a6].g)/a5,b:(a8.b-a9[a6].b)/a5};break;case"path":var S=H(a9[a6],e[a6]);a9[a6]=S[0];var a3=S[1];a2[a6]=[];for(var bb=0,a1=a9[a6][m];bb
+//
+// Math.seedrandom('yipee'); Sets Math.random to a function that is
+// initialized using the given explicit seed.
+//
+// Math.seedrandom(); Sets Math.random to a function that is
+// seeded using the current time, dom state,
+// and other accumulated local entropy.
+// The generated seed string is returned.
+//
+// Math.seedrandom('yowza', true);
+// Seeds using the given explicit seed mixed
+// together with accumulated entropy.
+//
+//
+// Seeds using physical random bits downloaded
+// from random.org.
+//
+// Examples:
+//
+// Math.seedrandom("hello"); // Use "hello" as the seed.
+// document.write(Math.random()); // Always 0.5463663768140734
+// document.write(Math.random()); // Always 0.43973793770592234
+// var rng1 = Math.random; // Remember the current prng.
+//
+// var autoseed = Math.seedrandom(); // New prng with an automatic seed.
+// document.write(Math.random()); // Pretty much unpredictable.
+//
+// Math.random = rng1; // Continue "hello" prng sequence.
+// document.write(Math.random()); // Always 0.554769432473455
+//
+// Math.seedrandom(autoseed); // Restart at the previous seed.
+// document.write(Math.random()); // Repeat the 'unpredictable' value.
+//
+// Notes:
+//
+// Each time seedrandom('arg') is called, entropy from the passed seed
+// is accumulated in a pool to help generate future seeds for the
+// zero-argument form of Math.seedrandom, so entropy can be injected over
+// time by calling seedrandom with explicit data repeatedly.
+//
+// On speed - This javascript implementation of Math.random() is about
+// 3-10x slower than the built-in Math.random() because it is not native
+// code, but this is typically fast enough anyway. Seeding is more expensive,
+// especially if you use auto-seeding. Some details (timings on Chrome 4):
+//
+// Our Math.random() - avg less than 0.002 milliseconds per call
+// seedrandom('explicit') - avg less than 0.5 milliseconds per call
+// seedrandom('explicit', true) - avg less than 2 milliseconds per call
+// seedrandom() - avg about 38 milliseconds per call
+//
+// LICENSE (BSD):
+//
+// Copyright 2010 David Bau, all rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+//
+// 3. Neither the name of this module nor the names of its contributors may
+// be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+//
+/**
+ * All code is in an anonymous closure to keep the global namespace clean.
+ *
+ * @param {number=} overflow
+ * @param {number=} startdenom
+ */
+(function (pool, math, width, chunks, significance, overflow, startdenom) {
+
+
+//
+// seedrandom()
+// This is the seedrandom function described above.
+//
+math['seedrandom'] = function seedrandom(seed, use_entropy) {
+ var key = [];
+ var arc4;
+
+ // Flatten the seed string or build one from local entropy if needed.
+ seed = mixkey(flatten(
+ use_entropy ? [seed, pool] :
+ arguments.length ? seed :
+ [new Date().getTime(), pool, window], 3), key);
+
+ // Use the seed to initialize an ARC4 generator.
+ arc4 = new ARC4(key);
+
+ // Mix the randomness into accumulated entropy.
+ mixkey(arc4.S, pool);
+
+ // Override Math.random
+
+ // This function returns a random double in [0, 1) that contains
+ // randomness in every bit of the mantissa of the IEEE 754 value.
+
+ math['random'] = function random() { // Closure to return a random double:
+ var n = arc4.g(chunks); // Start with a numerator n < 2 ^ 48
+ var d = startdenom; // and denominator d = 2 ^ 48.
+ var x = 0; // and no 'extra last byte'.
+ while (n < significance) { // Fill up all significant digits by
+ n = (n + x) * width; // shifting numerator and
+ d *= width; // denominator and generating a
+ x = arc4.g(1); // new least-significant-byte.
+ }
+ while (n >= overflow) { // To avoid rounding up, before adding
+ n /= 2; // last byte, shift everything
+ d /= 2; // right using integer math until
+ x >>>= 1; // we have exactly the desired bits.
+ }
+ return (n + x) / d; // Form the number within [0, 1).
+ };
+
+ // Return the seed that was used
+ return seed;
+};
+
+//
+// ARC4
+//
+// An ARC4 implementation. The constructor takes a key in the form of
+// an array of at most (width) integers that should be 0 <= x < (width).
+//
+// The g(count) method returns a pseudorandom integer that concatenates
+// the next (count) outputs from ARC4. Its return value is a number x
+// that is in the range 0 <= x < (width ^ count).
+//
+/** @constructor */
+function ARC4(key) {
+ var t, u, me = this, keylen = key.length;
+ var i = 0, j = me.i = me.j = me.m = 0;
+ me.S = [];
+ me.c = [];
+
+ // The empty key [] is treated as [0].
+ if (!keylen) { key = [keylen++]; }
+
+ // Set up S using the standard key scheduling algorithm.
+ while (i < width) { me.S[i] = i++; }
+ for (i = 0; i < width; i++) {
+ t = me.S[i];
+ j = lowbits(j + t + key[i % keylen]);
+ u = me.S[j];
+ me.S[i] = u;
+ me.S[j] = t;
+ }
+
+ // The "g" method returns the next (count) outputs as one number.
+ me.g = function getnext(count) {
+ var s = me.S;
+ var i = lowbits(me.i + 1); var t = s[i];
+ var j = lowbits(me.j + t); var u = s[j];
+ s[i] = u;
+ s[j] = t;
+ var r = s[lowbits(t + u)];
+ while (--count) {
+ i = lowbits(i + 1); t = s[i];
+ j = lowbits(j + t); u = s[j];
+ s[i] = u;
+ s[j] = t;
+ r = r * width + s[lowbits(t + u)];
+ }
+ me.i = i;
+ me.j = j;
+ return r;
+ };
+ // For robust unpredictability discard an initial batch of values.
+ // See http://www.rsa.com/rsalabs/node.asp?id=2009
+ me.g(width);
+}
+
+//
+// flatten()
+// Converts an object tree to nested arrays of strings.
+//
+/** @param {Object=} result
+ * @param {string=} prop */
+function flatten(obj, depth, result, prop) {
+ result = [];
+ if (depth && typeof(obj) == 'object') {
+ for (prop in obj) {
+ if (prop.indexOf('S') < 5) { // Avoid FF3 bug (local/sessionStorage)
+ try { result.push(flatten(obj[prop], depth - 1)); } catch (e) {}
+ }
+ }
+ }
+ return result.length ? result : '' + obj;
+}
+
+//
+// mixkey()
+// Mixes a string seed into a key that is an array of integers, and
+// returns a shortened string seed that is equivalent to the result key.
+//
+/** @param {number=} smear
+ * @param {number=} j */
+function mixkey(seed, key, smear, j) {
+ seed += ''; // Ensure the seed is a string
+ smear = 0;
+ for (j = 0; j < seed.length; j++) {
+ key[lowbits(j)] =
+ lowbits((smear ^= key[lowbits(j)] * 19) + seed.charCodeAt(j));
+ }
+ seed = '';
+ for (j in key) { seed += String.fromCharCode(key[j]); }
+ return seed;
+}
+
+//
+// lowbits()
+// A quick "n mod width" for width a power of 2.
+//
+function lowbits(n) { return n & (width - 1); }
+
+//
+// The following constants are related to IEEE 754 limits.
+//
+startdenom = math.pow(width, chunks);
+significance = math.pow(2, significance);
+overflow = significance * 2;
+
+//
+// When seedrandom.js is loaded, we immediately mix a few bits
+// from the built-in RNG into the entropy pool. Because we do
+// not want to intefere with determinstic PRNG state later,
+// seedrandom will not call math.random on its own again after
+// initialization.
+//
+mixkey(math.random(), pool);
+
+// End anonymous scope, and pass initial values.
+})(
+ [], // pool: entropy pool starts empty
+ Math, // math: package containing random, pow, and seedrandom
+ 256, // width: each RC4 output is 0 <= x < 256
+ 6, // chunks: at least six RC4 outputs for each double
+ 52 // significance: there are 52 significant digits in a double
+);
diff --git a/htdocs/test.cgi b/htdocs/test.cgi
new file mode 100755
index 0000000..706d0cb
--- /dev/null
+++ b/htdocs/test.cgi
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+echo "Content-Type: text/html"
+echo
+echo "