Move bmx6 and luci-app-bmx6 packages into root directory
[feed/routing.git] / luci-app-bmx6 / files / www / luci-static / resources / bmx6 / js / dracula_graph.js
1 /*
2 * Dracula Graph Layout and Drawing Framework 0.0.3alpha
3 * (c) 2010 Philipp Strathausen <strathausen@gmail.com>, http://strathausen.eu
4 * Contributions by Jake Stothard <stothardj@gmail.com>.
5 *
6 * based on the Graph JavaScript framework, version 0.0.1
7 * (c) 2006 Aslak Hellesoy <aslak.hellesoy@gmail.com>
8 * (c) 2006 Dave Hoover <dave.hoover@gmail.com>
9 *
10 * Ported from Graph::Layouter::Spring in
11 * http://search.cpan.org/~pasky/Graph-Layderer-0.02/
12 * The algorithm is based on a spring-style layouter of a Java-based social
13 * network tracker PieSpy written by Paul Mutton <paul@jibble.org>.
14 *
15 * This code is freely distributable under the MIT license. Commercial use is
16 * hereby granted without any cost or restriction.
17 *
18 * Links:
19 *
20 * Graph Dracula JavaScript Framework:
21 * http://graphdracula.net
22 *
23 /*--------------------------------------------------------------------------*/
24
25 /*
26 * Edge Factory
27 */
28 var AbstractEdge = function() {
29 }
30 AbstractEdge.prototype = {
31 hide: function() {
32 this.connection.fg.hide();
33 this.connection.bg && this.bg.connection.hide();
34 }
35 };
36 var EdgeFactory = function() {
37 this.template = new AbstractEdge();
38 this.template.style = new Object();
39 this.template.style.directed = false;
40 this.template.weight = 1;
41 };
42 EdgeFactory.prototype = {
43 build: function(source, target) {
44 var e = jQuery.extend(true, {}, this.template);
45 e.source = source;
46 e.target = target;
47 return e;
48 }
49 };
50
51 /*
52 * Graph
53 */
54 var Graph = function() {
55 this.nodes = {};
56 this.edges = [];
57 this.snapshots = []; // previous graph states TODO to be implemented
58 this.edgeFactory = new EdgeFactory();
59 };
60 Graph.prototype = {
61 /*
62 * add a node
63 * @id the node's ID (string or number)
64 * @content (optional, dictionary) can contain any information that is
65 * being interpreted by the layout algorithm or the graph
66 * representation
67 */
68 addNode: function(id, content) {
69 /* testing if node is already existing in the graph */
70 if(this.nodes[id] == undefined) {
71 this.nodes[id] = new Graph.Node(id, content);
72 }
73 return this.nodes[id];
74 },
75
76 addEdge: function(source, target, style) {
77 var s = this.addNode(source);
78 var t = this.addNode(target);
79 var edge = this.edgeFactory.build(s, t);
80 jQuery.extend(edge.style,style);
81 s.edges.push(edge);
82 this.edges.push(edge);
83 // NOTE: Even directed edges are added to both nodes.
84 t.edges.push(edge);
85 },
86
87 /* TODO to be implemented
88 * Preserve a copy of the graph state (nodes, positions, ...)
89 * @comment a comment describing the state
90 */
91 snapShot: function(comment) {
92 /* FIXME
93 var graph = new Graph();
94 graph.nodes = jQuery.extend(true, {}, this.nodes);
95 graph.edges = jQuery.extend(true, {}, this.edges);
96 this.snapshots.push({comment: comment, graph: graph});
97 */
98 },
99 removeNode: function(id) {
100 delete this.nodes[id];
101 for(var i = 0; i < this.edges.length; i++) {
102 if (this.edges[i].source.id == id || this.edges[i].target.id == id) {
103 this.edges.splice(i, 1);
104 i--;
105 }
106 }
107 }
108 };
109
110 /*
111 * Node
112 */
113 Graph.Node = function(id, node){
114 node = node || {};
115 node.id = id;
116 node.edges = [];
117 node.hide = function() {
118 this.hidden = true;
119 this.shape && this.shape.hide(); /* FIXME this is representation specific code and should be elsewhere */
120 for(i in this.edges)
121 (this.edges[i].source.id == id || this.edges[i].target == id) && this.edges[i].hide && this.edges[i].hide();
122 };
123 node.show = function() {
124 this.hidden = false;
125 this.shape && this.shape.show();
126 for(i in this.edges)
127 (this.edges[i].source.id == id || this.edges[i].target == id) && this.edges[i].show && this.edges[i].show();
128 };
129 return node;
130 };
131 Graph.Node.prototype = {
132 };
133
134 /*
135 * Renderer base class
136 */
137 Graph.Renderer = {};
138
139 /*
140 * Renderer implementation using RaphaelJS
141 */
142 Graph.Renderer.Raphael = function(element, graph, width, height) {
143 this.width = width || 400;
144 this.height = height || 400;
145 var selfRef = this;
146 this.r = Raphael(element, this.width, this.height);
147 this.radius = 40; /* max dimension of a node */
148 this.graph = graph;
149 this.mouse_in = false;
150
151 /* TODO default node rendering function */
152 if(!this.graph.render) {
153 this.graph.render = function() {
154 return;
155 }
156 }
157
158 /*
159 * Dragging
160 */
161 this.isDrag = false;
162 this.dragger = function (e) {
163 this.dx = e.clientX;
164 this.dy = e.clientY;
165 selfRef.isDrag = this;
166 this.set && this.set.animate({"fill-opacity": .1}, 200) && this.set.toFront();
167 e.preventDefault && e.preventDefault();
168 };
169
170 var d = document.getElementById(element);
171 d.onmousemove = function (e) {
172 e = e || window.event;
173 if (selfRef.isDrag) {
174 var bBox = selfRef.isDrag.set.getBBox();
175 // TODO round the coordinates here (eg. for proper image representation)
176 var newX = e.clientX - selfRef.isDrag.dx + (bBox.x + bBox.width / 2);
177 var newY = e.clientY - selfRef.isDrag.dy + (bBox.y + bBox.height / 2);
178 /* prevent shapes from being dragged out of the canvas */
179 var clientX = e.clientX - (newX < 20 ? newX - 20 : newX > selfRef.width - 20 ? newX - selfRef.width + 20 : 0);
180 var clientY = e.clientY - (newY < 20 ? newY - 20 : newY > selfRef.height - 20 ? newY - selfRef.height + 20 : 0);
181 selfRef.isDrag.set.translate(clientX - Math.round(selfRef.isDrag.dx), clientY - Math.round(selfRef.isDrag.dy));
182 // console.log(clientX - Math.round(selfRef.isDrag.dx), clientY - Math.round(selfRef.isDrag.dy));
183 for (var i in selfRef.graph.edges) {
184 selfRef.graph.edges[i].connection && selfRef.graph.edges[i].connection.draw();
185 }
186 //selfRef.r.safari();
187 selfRef.isDrag.dx = clientX;
188 selfRef.isDrag.dy = clientY;
189 }
190 };
191 d.onmouseup = function () {
192 selfRef.isDrag && selfRef.isDrag.set.animate({"fill-opacity": .6}, 500);
193 selfRef.isDrag = false;
194 };
195 this.draw();
196 };
197 Graph.Renderer.Raphael.prototype = {
198 translate: function(point) {
199 return [
200 (point[0] - this.graph.layoutMinX) * this.factorX + this.radius,
201 (point[1] - this.graph.layoutMinY) * this.factorY + this.radius
202 ];
203 },
204
205 rotate: function(point, length, angle) {
206 var dx = length * Math.cos(angle);
207 var dy = length * Math.sin(angle);
208 return [point[0]+dx, point[1]+dy];
209 },
210
211 draw: function() {
212 this.factorX = (this.width - 2 * this.radius) / (this.graph.layoutMaxX - this.graph.layoutMinX);
213 this.factorY = (this.height - 2 * this.radius) / (this.graph.layoutMaxY - this.graph.layoutMinY);
214 for (i in this.graph.nodes) {
215 this.drawNode(this.graph.nodes[i]);
216 }
217 for (var i = 0; i < this.graph.edges.length; i++) {
218 this.drawEdge(this.graph.edges[i]);
219 }
220 },
221
222 drawNode: function(node) {
223 var point = this.translate([node.layoutPosX, node.layoutPosY]);
224 node.point = point;
225
226 /* if node has already been drawn, move the nodes */
227 if(node.shape) {
228 var oBBox = node.shape.getBBox();
229 var opoint = { x: oBBox.x + oBBox.width / 2, y: oBBox.y + oBBox.height / 2};
230 node.shape.translate(Math.round(point[0] - opoint.x), Math.round(point[1] - opoint.y));
231 this.r.safari();
232 return node;
233 }/* else, draw new nodes */
234
235 var shape;
236
237 /* if a node renderer function is provided by the user, then use it
238 or the default render function instead */
239 if(!node.render) {
240 node.render = function(r, node) {
241 /* the default node drawing */
242 var color = Raphael.getColor();
243 var ellipse = r.ellipse(0, 0, 30, 20).attr({fill: color, stroke: color, "stroke-width": 2});
244 /* set DOM node ID */
245 ellipse.node.id = node.label || node.id;
246 shape = r.set().
247 push(ellipse).
248 push(r.text(0, 30, node.label || node.id));
249 return shape;
250 }
251 }
252 /* or check for an ajax representation of the nodes */
253 if(node.shapes) {
254 // TODO ajax representation evaluation
255 }
256
257 shape = node.render(this.r, node).hide();
258
259 shape.attr({"fill-opacity": .6});
260 /* re-reference to the node an element belongs to, needed for dragging all elements of a node */
261 shape.items.forEach(function(item){ item.set = shape; item.node.style.cursor = "move"; });
262 shape.mousedown(this.dragger);
263
264 var box = shape.getBBox();
265 shape.translate(Math.round(point[0]-(box.x+box.width/2)),Math.round(point[1]-(box.y+box.height/2)))
266 //console.log(box,point);
267 node.hidden || shape.show();
268 node.shape = shape;
269 },
270 drawEdge: function(edge) {
271 /* if this edge already exists the other way around and is undirected */
272 if(edge.backedge)
273 return;
274 if(edge.source.hidden || edge.target.hidden) {
275 edge.connection && edge.connection.fg.hide() | edge.connection.bg && edge.connection.bg.hide();
276 return;
277 }
278 /* if edge already has been drawn, only refresh the edge */
279 if(!edge.connection) {
280 edge.style && edge.style.callback && edge.style.callback(edge); // TODO move this somewhere else
281 edge.connection = this.r.connection(edge.source.shape, edge.target.shape, edge.style);
282 return;
283 }
284 //FIXME showing doesn't work well
285 edge.connection.fg.show();
286 edge.connection.bg && edge.connection.bg.show();
287 edge.connection.draw();
288 }
289 };
290 Graph.Layout = {};
291 Graph.Layout.Spring = function(graph) {
292 this.graph = graph;
293 this.iterations = 500;
294 this.maxRepulsiveForceDistance = 6;
295 this.k = 2;
296 this.c = 0.01;
297 this.maxVertexMovement = 0.5;
298 this.layout();
299 };
300 Graph.Layout.Spring.prototype = {
301 layout: function() {
302 this.layoutPrepare();
303 for (var i = 0; i < this.iterations; i++) {
304 this.layoutIteration();
305 }
306 this.layoutCalcBounds();
307 },
308
309 layoutPrepare: function() {
310 for (i in this.graph.nodes) {
311 var node = this.graph.nodes[i];
312 node.layoutPosX = 0;
313 node.layoutPosY = 0;
314 node.layoutForceX = 0;
315 node.layoutForceY = 0;
316 }
317
318 },
319
320 layoutCalcBounds: function() {
321 var minx = Infinity, maxx = -Infinity, miny = Infinity, maxy = -Infinity;
322
323 for (i in this.graph.nodes) {
324 var x = this.graph.nodes[i].layoutPosX;
325 var y = this.graph.nodes[i].layoutPosY;
326
327 if(x > maxx) maxx = x;
328 if(x < minx) minx = x;
329 if(y > maxy) maxy = y;
330 if(y < miny) miny = y;
331 }
332
333 this.graph.layoutMinX = minx;
334 this.graph.layoutMaxX = maxx;
335 this.graph.layoutMinY = miny;
336 this.graph.layoutMaxY = maxy;
337 },
338
339 layoutIteration: function() {
340 // Forces on nodes due to node-node repulsions
341
342 var prev = new Array();
343 for(var c in this.graph.nodes) {
344 var node1 = this.graph.nodes[c];
345 for (var d in prev) {
346 var node2 = this.graph.nodes[prev[d]];
347 this.layoutRepulsive(node1, node2);
348
349 }
350 prev.push(c);
351 }
352
353 // Forces on nodes due to edge attractions
354 for (var i = 0; i < this.graph.edges.length; i++) {
355 var edge = this.graph.edges[i];
356 this.layoutAttractive(edge);
357 }
358
359 // Move by the given force
360 for (i in this.graph.nodes) {
361 var node = this.graph.nodes[i];
362 var xmove = this.c * node.layoutForceX;
363 var ymove = this.c * node.layoutForceY;
364
365 var max = this.maxVertexMovement;
366 if(xmove > max) xmove = max;
367 if(xmove < -max) xmove = -max;
368 if(ymove > max) ymove = max;
369 if(ymove < -max) ymove = -max;
370
371 node.layoutPosX += xmove;
372 node.layoutPosY += ymove;
373 node.layoutForceX = 0;
374 node.layoutForceY = 0;
375 }
376 },
377
378 layoutRepulsive: function(node1, node2) {
379 if (typeof node1 == 'undefined' || typeof node2 == 'undefined')
380 return;
381 var dx = node2.layoutPosX - node1.layoutPosX;
382 var dy = node2.layoutPosY - node1.layoutPosY;
383 var d2 = dx * dx + dy * dy;
384 if(d2 < 0.01) {
385 dx = 0.1 * Math.random() + 0.1;
386 dy = 0.1 * Math.random() + 0.1;
387 var d2 = dx * dx + dy * dy;
388 }
389 var d = Math.sqrt(d2);
390 if(d < this.maxRepulsiveForceDistance) {
391 var repulsiveForce = this.k * this.k / d;
392 node2.layoutForceX += repulsiveForce * dx / d;
393 node2.layoutForceY += repulsiveForce * dy / d;
394 node1.layoutForceX -= repulsiveForce * dx / d;
395 node1.layoutForceY -= repulsiveForce * dy / d;
396 }
397 },
398
399 layoutAttractive: function(edge) {
400 var node1 = edge.source;
401 var node2 = edge.target;
402
403 var dx = node2.layoutPosX - node1.layoutPosX;
404 var dy = node2.layoutPosY - node1.layoutPosY;
405 var d2 = dx * dx + dy * dy;
406 if(d2 < 0.01) {
407 dx = 0.1 * Math.random() + 0.1;
408 dy = 0.1 * Math.random() + 0.1;
409 var d2 = dx * dx + dy * dy;
410 }
411 var d = Math.sqrt(d2);
412 if(d > this.maxRepulsiveForceDistance) {
413 d = this.maxRepulsiveForceDistance;
414 d2 = d * d;
415 }
416 var attractiveForce = (d2 - this.k * this.k) / this.k;
417 if(edge.attraction == undefined) edge.attraction = 1;
418 attractiveForce *= Math.log(edge.attraction) * 0.5 + 1;
419
420 node2.layoutForceX -= attractiveForce * dx / d;
421 node2.layoutForceY -= attractiveForce * dy / d;
422 node1.layoutForceX += attractiveForce * dx / d;
423 node1.layoutForceY += attractiveForce * dy / d;
424 }
425 };
426
427 Graph.Layout.Ordered = function(graph, order) {
428 this.graph = graph;
429 this.order = order;
430 this.layout();
431 };
432 Graph.Layout.Ordered.prototype = {
433 layout: function() {
434 this.layoutPrepare();
435 this.layoutCalcBounds();
436 },
437
438 layoutPrepare: function(order) {
439 for (i in this.graph.nodes) {
440 var node = this.graph.nodes[i];
441 node.layoutPosX = 0;
442 node.layoutPosY = 0;
443 }
444 var counter = 0;
445 for (i in this.order) {
446 var node = this.order[i];
447 node.layoutPosX = counter;
448 node.layoutPosY = Math.random();
449 counter++;
450 }
451 },
452
453 layoutCalcBounds: function() {
454 var minx = Infinity, maxx = -Infinity, miny = Infinity, maxy = -Infinity;
455
456 for (i in this.graph.nodes) {
457 var x = this.graph.nodes[i].layoutPosX;
458 var y = this.graph.nodes[i].layoutPosY;
459
460 if(x > maxx) maxx = x;
461 if(x < minx) minx = x;
462 if(y > maxy) maxy = y;
463 if(y < miny) miny = y;
464 }
465
466 this.graph.layoutMinX = minx;
467 this.graph.layoutMaxX = maxx;
468
469 this.graph.layoutMinY = miny;
470 this.graph.layoutMaxY = maxy;
471 }
472 };
473
474 /*
475 * usefull JavaScript extensions,
476 */
477
478 function log(a) {console.log&&console.log(a);}
479
480 /*
481 * Raphael Tooltip Plugin
482 * - attaches an element as a tooltip to another element
483 *
484 * Usage example, adding a rectangle as a tooltip to a circle:
485 *
486 * paper.circle(100,100,10).tooltip(paper.rect(0,0,20,30));
487 *
488 * If you want to use more shapes, you'll have to put them into a set.
489 *
490 */
491 Raphael.el.tooltip = function (tp) {
492 this.tp = tp;
493 this.tp.o = {x: 0, y: 0};
494 this.tp.hide();
495 this.hover(
496 function(event){
497 this.mousemove(function(event){
498 this.tp.translate(event.clientX -
499 this.tp.o.x,event.clientY - this.tp.o.y);
500 this.tp.o = {x: event.clientX, y: event.clientY};
501 });
502 this.tp.show().toFront();
503 },
504 function(event){
505 this.tp.hide();
506 this.unmousemove();
507 });
508 return this;
509 };
510
511 /* For IE */
512 if (!Array.prototype.forEach)
513 {
514 Array.prototype.forEach = function(fun /*, thisp*/)
515 {
516 var len = this.length;
517 if (typeof fun != "function")
518 throw new TypeError();
519
520 var thisp = arguments[1];
521 for (var i = 0; i < len; i++)
522 {
523 if (i in this)
524 fun.call(thisp, this[i], i, this);
525 }
526 };
527 }