diff options
| author | Xavier Morel <xmo@openerp.com> | 2012-03-05 12:08:13 +0100 |
|---|---|---|
| committer | Xavier Morel <xmo@openerp.com> | 2012-03-05 12:08:13 +0100 |
| commit | 06abe5aa3d7a79159b79e5d321cabca7e4f90f1a (patch) | |
| tree | c8df4bbfcc6b1c45f1026fc85950903725c8ffcc /addons/web_process/static/lib/dracula/dracula_graph.coffee | |
| parent | fc887bfb2f5e5985a68590017e4eac9202e3428a (diff) | |
[FIX] re-add Dracula removed in rev xmo@openerp.com-20120305103353-w8r6fk9cv8wjklts: turns out Process needs it
bzr revid: xmo@openerp.com-20120305110813-b00fhngx0e5p0efo
Diffstat (limited to 'addons/web_process/static/lib/dracula/dracula_graph.coffee')
| -rw-r--r-- | addons/web_process/static/lib/dracula/dracula_graph.coffee | 524 |
1 files changed, 524 insertions, 0 deletions
diff --git a/addons/web_process/static/lib/dracula/dracula_graph.coffee b/addons/web_process/static/lib/dracula/dracula_graph.coffee new file mode 100644 index 00000000000..bb91945098d --- /dev/null +++ b/addons/web_process/static/lib/dracula/dracula_graph.coffee @@ -0,0 +1,524 @@ +### + * Dracula Graph Layout and Drawing Framework 0.0.3alpha + * (c) 2010 Philipp Strathausen <strathausen@gmail.com>, http://strathausen.eu + * + * Contributions by: + * Branched by Jake Stothard <stothardj@gmail.com>. + * + * based on the Graph JavaScript framework, version 0.0.1 + * (c) 2006 Aslak Hellesoy <aslak.hellesoy@gmail.com> + * (c) 2006 Dave Hoover <dave.hoover@gmail.com> + * + * 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 E<lt>paul@jibble.orgE<gt>. + * + * 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 + * +### + + +### + Edge Factory +### +AbstractEdge = -> + +AbstractEdge.prototype = + hide: -> + @connection.fg.hide() + @connection.bg && @bg.connection.hide() + +EdgeFactory = -> + @template = new AbstractEdge() + @template.style = new Object() + @template.style.directed = false + @template.weight = 1 + +EdgeFactory.prototype = + build: (source, target) -> + e = jQuery.extend true, {}, @template + e.source = source + e.target = target + e + +### + Graph +### +Graph = -> + @nodes = {} + @edges = [] + @snapshots = [] # previous graph states TODO to be implemented + @edgeFactory = new EdgeFactory() + +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: (id, content) -> + # testing if node is already existing in the graph + if @nodes[id] == undefined + @nodes[id] = new Graph.Node id, content + @nodes[id] + + addEdge: (source, target, style) -> + s = @addNode source + t = @addNode target + var edge = @edgeFactory.build s, t + jQuery.extend edge.style, style + s.edges.push edge + @edges.push edge + # NOTE: Even directed edges are added to both nodes. + t.edges.push edge + + # TODO to be implemented + # Preserve a copy of the graph state (nodes, positions, ...) + # @comment a comment describing the state + snapShot: (comment) -> + ###/* FIXME + var graph = new Graph() + graph.nodes = jQuery.extend(true, {}, @nodes) + graph.edges = jQuery.extend(true, {}, @edges) + @snapshots.push({comment: comment, graph: graph}) + */ + ### + + removeNode: (id) -> + delete @nodes[id] + for i = 0; i < @edges.length; i++ + if @edges[i].source.id == id || @edges[i].target.id == id + @edges.splice(i, 1) + i-- + +/* + * Node + */ +Graph.Node = (id, node) -> + node = node || {} + node.id = id + node.edges = [] + node.hide = -> + @hidden = true + @shape && @shape.hide() # FIXME this is representation specific code and should be elsewhere */ + for(i in @edges) + (@edges[i].source.id == id || @edges[i].target == id) && @edges[i].hide && @edges[i].hide() + + node.show = -> + @hidden = false + @shape && @shape.show() + for(i in @edges) + (@edges[i].source.id == id || @edges[i].target == id) && @edges[i].show && @edges[i].show() + + node + +Graph.Node.prototype = { } + +### + Renderer base class +### +Graph.Renderer = { } + +### + Renderer implementation using RaphaelJS +### +Graph.Renderer.Raphael = (element, graph, width, height) -> + @width = width || 400 + @height = height || 400 + var selfRef = this + @r = Raphael element, @width, @height + @radius = 40 # max dimension of a node + @graph = graph + @mouse_in = false + + # TODO default node rendering + if(!@graph.render) { + @graph.render = -> + return + } + } + + /* + * Dragging + */ + @isDrag = false + @dragger = (e) -> + @dx = e.clientX + @dy = e.clientY + selfRef.isDrag = this + @set && @set.animate "fill-opacity": .1, 200 && @set.toFront() + e.preventDefault && e.preventDefault() + + document.onmousemove = (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 - Math.round(selfRef.isDrag.dx), clientY - Math.round(selfRef.isDrag.dy)) + // console.log(clientX - Math.round(selfRef.isDrag.dx), clientY - Math.round(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 = -> + selfRef.isDrag && selfRef.isDrag.set.animate({"fill-opacity": .6}, 500) + selfRef.isDrag = false + } + @draw() +} +Graph.Renderer.Raphael.prototype = { + translate: (point) { + return [ + (point[0] - @graph.layoutMinX) * @factorX + @radius, + (point[1] - @graph.layoutMinY) * @factorY + @radius + ] + }, + + rotate: (point, length, angle) { + var dx = length * Math.cos(angle) + var dy = length * Math.sin(angle) + return [point[0]+dx, point[1]+dy] + }, + + draw: -> + @factorX = (@width - 2 * @radius) / (@graph.layoutMaxX - @graph.layoutMinX) + @factorY = (@height - 2 * @radius) / (@graph.layoutMaxY - @graph.layoutMinY) + for (i in @graph.nodes) { + @drawNode(@graph.nodes[i]) + } + for (var i = 0; i < @graph.edges.length; i++) { + @drawEdge(@graph.edges[i]) + } + }, + + drawNode: (node) { + var point = @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 = { x: oBBox.x + oBBox.width / 2, y: oBBox.y + oBBox.height / 2} + node.shape.translate(Math.round(point[0] - opoint.x), Math.round(point[1] - opoint.y)) + @r.safari() + return node + }/* else, draw new nodes */ + + var shape + + /* if a node renderer is provided by the user, then use it + or the default render instead */ + if(!node.render) { + node.render = (r, node) { + /* the default node drawing */ + var color = Raphael.getColor() + var ellipse = r.ellipse(0, 0, 30, 20).attr({fill: color, stroke: color, "stroke-width": 2}) + /* set DOM node ID */ + ellipse.node.id = node.label || node.id + shape = r.set(). + push(ellipse). + push(r.text(0, 30, node.label || node.id)) + return shape + } + } + /* or check for an ajax representation of the nodes */ + if(node.shapes) { + // TODO ajax representation evaluation + } + + shape = node.render(@r, node).hide() + + shape.attr({"fill-opacity": .6}) + /* re-reference to the node an element belongs to, needed for dragging all elements of a node */ + shape.items.forEach((item){ item.set = shape; item.node.style.cursor = "move"; }) + shape.mousedown(@dragger) + + var box = shape.getBBox() + shape.translate(Math.round(point[0]-(box.x+box.width/2)),Math.round(point[1]-(box.y+box.height/2))) + //console.log(box,point) + node.hidden || shape.show() + node.shape = shape + }, + drawEdge: (edge) { + /* if this edge already exists the other way around and is undirected */ + if(edge.backedge) + return + if(edge.source.hidden || edge.target.hidden) { + edge.connection && edge.connection.fg.hide() | edge.connection.bg && edge.connection.bg.hide() + return + } + /* if edge already has been drawn, only refresh the edge */ + if(!edge.connection) { + edge.style && edge.style.callback && edge.style.callback(edge); // TODO move this somewhere else + edge.connection = @r.connection(edge.source.shape, edge.target.shape, edge.style) + return + } + //FIXME showing doesn't work well + edge.connection.fg.show() + edge.connection.bg && edge.connection.bg.show() + edge.connection.draw() + } +} +Graph.Layout = {} +Graph.Layout.Spring = (graph) { + @graph = graph + @iterations = 500 + @maxRepulsiveForceDistance = 6 + @k = 2 + @c = 0.01 + @maxVertexMovement = 0.5 + @layout() +} +Graph.Layout.Spring.prototype = { + layout: -> + @layoutPrepare() + for (var i = 0; i < @iterations; i++) { + @layoutIteration() + } + @layoutCalcBounds() + }, + + layoutPrepare: -> + for (i in @graph.nodes) { + var node = @graph.nodes[i] + node.layoutPosX = 0 + node.layoutPosY = 0 + node.layoutForceX = 0 + node.layoutForceY = 0 + } + + }, + + layoutCalcBounds: -> + var minx = Infinity, maxx = -Infinity, miny = Infinity, maxy = -Infinity + + for (i in @graph.nodes) { + var x = @graph.nodes[i].layoutPosX + var y = @graph.nodes[i].layoutPosY + + if(x > maxx) maxx = x + if(x < minx) minx = x + if(y > maxy) maxy = y + if(y < miny) miny = y + } + + @graph.layoutMinX = minx + @graph.layoutMaxX = maxx + @graph.layoutMinY = miny + @graph.layoutMaxY = maxy + }, + + layoutIteration: -> + // Forces on nodes due to node-node repulsions + + var prev = new Array() + for(var c in @graph.nodes) { + var node1 = @graph.nodes[c] + for (var d in prev) { + var node2 = @graph.nodes[prev[d]] + @layoutRepulsive(node1, node2) + + } + prev.push(c) + } + + // Forces on nodes due to edge attractions + for (var i = 0; i < @graph.edges.length; i++) { + var edge = @graph.edges[i] + @layoutAttractive(edge); + } + + // Move by the given force + for (i in @graph.nodes) { + var node = @graph.nodes[i] + var xmove = @c * node.layoutForceX + var ymove = @c * node.layoutForceY + + var max = @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: (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 < @maxRepulsiveForceDistance) { + var repulsiveForce = @k * @k / d + node2.layoutForceX += repulsiveForce * dx / d + node2.layoutForceY += repulsiveForce * dy / d + node1.layoutForceX -= repulsiveForce * dx / d + node1.layoutForceY -= repulsiveForce * dy / d + } + }, + + layoutAttractive: (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 > @maxRepulsiveForceDistance) { + d = @maxRepulsiveForceDistance + d2 = d * d + } + var attractiveForce = (d2 - @k * @k) / @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 + } +} + +Graph.Layout.Ordered = (graph, order) { + @graph = graph + @order = order + @layout() +} +Graph.Layout.Ordered.prototype = { + layout: -> + @layoutPrepare() + @layoutCalcBounds() + }, + + layoutPrepare: (order) { + for (i in @graph.nodes) { + var node = @graph.nodes[i] + node.layoutPosX = 0 + node.layoutPosY = 0 + } + var counter = 0 + for (i in @order) { + var node = @order[i] + node.layoutPosX = counter + node.layoutPosY = Math.random() + counter++ + } + }, + + layoutCalcBounds: -> + var minx = Infinity, maxx = -Infinity, miny = Infinity, maxy = -Infinity + + for (i in @graph.nodes) { + var x = @graph.nodes[i].layoutPosX + var y = @graph.nodes[i].layoutPosY + + if(x > maxx) maxx = x + if(x < minx) minx = x + if(y > maxy) maxy = y + if(y < miny) miny = y + } + + @graph.layoutMinX = minx + @graph.layoutMaxX = maxx + + @graph.layoutMinY = miny + @graph.layoutMaxY = maxy + } +} + +/* + * usefull JavaScript extensions, + */ + + log(a) {console.log&&console.log(a);} + +/* + * 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 = (tp) { + @tp = tp + @tp.o = {x: 0, y: 0} + @tp.hide() + @hover( + (event){ + @mousemove((event){ + @tp.translate(event.clientX - + @tp.o.x,event.clientY - @tp.o.y) + @tp.o = {x: event.clientX, y: event.clientY} + }) + @tp.show().toFront() + }, + (event){ + @tp.hide() + @unmousemove() + }) + return this +} + +/* For IE */ +if (!Array.prototype.forEach) +{ + Array.prototype.forEach = (fun /*, thisp*/) + { + var len = @length + if (typeof fun != "") + throw new TypeError() + + var thisp = arguments[1] + for (var i = 0; i < len; i++) + { + if (i in this) + fun.call(thisp, this[i], i, this) + } + } +} |
