diff --git a/addons/base/static/src/base.html b/addons/base/static/src/base.html index bd717d32f34..619178f4cff 100644 --- a/addons/base/static/src/base.html +++ b/addons/base/static/src/base.html @@ -23,6 +23,12 @@ + + + + + + diff --git a/addons/base/static/src/js/base.js b/addons/base/static/src/js/base.js index 675476879cf..3a5bd2a2915 100644 --- a/addons/base/static/src/js/base.js +++ b/addons/base/static/src/js/base.js @@ -128,6 +128,7 @@ openerp.base = function(instance) { openerp.base.form(instance); openerp.base.calendar(instance); openerp.base.gantt(instance); + openerp.base.diagram(instance); }; // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/base/static/src/xml/base.xml b/addons/base/static/src/xml/base.xml index 129198f0ea4..9f7ddc49a7b 100644 --- a/addons/base/static/src/xml/base.xml +++ b/addons/base/static/src/xml/base.xml @@ -205,8 +205,7 @@ - + @@ -232,6 +231,10 @@ + +

+
+

diff --git a/addons/base_diagram/__init__.py b/addons/base_diagram/__init__.py new file mode 100644 index 00000000000..355939a4922 --- /dev/null +++ b/addons/base_diagram/__init__.py @@ -0,0 +1 @@ +import controllers \ No newline at end of file diff --git a/addons/base_diagram/__openerp__.py b/addons/base_diagram/__openerp__.py new file mode 100644 index 00000000000..fb2a849f630 --- /dev/null +++ b/addons/base_diagram/__openerp__.py @@ -0,0 +1,6 @@ +{ + "name" : "OpenERP Web base Diagram", + "version" : "2.0", + "depends" : [], + 'active': True, +} diff --git a/addons/base_diagram/controllers/__init__.py b/addons/base_diagram/controllers/__init__.py new file mode 100644 index 00000000000..039d9715fab --- /dev/null +++ b/addons/base_diagram/controllers/__init__.py @@ -0,0 +1 @@ +import main \ No newline at end of file diff --git a/addons/base_diagram/controllers/main.py b/addons/base_diagram/controllers/main.py new file mode 100644 index 00000000000..2f76ead7117 --- /dev/null +++ b/addons/base_diagram/controllers/main.py @@ -0,0 +1,127 @@ +from base.controllers.main import View +import openerpweb + +class DiagramView(View): + _cp_path = "/base_diagram/diagram" + + @openerpweb.jsonrequest + def load(self, req, model, view_id): + print req + print dir(req) + fields_view = self.fields_view_get(req, model, view_id, 'diagram') + return {'fields_view': fields_view} + + @openerpweb.jsonrequest + def get_diagram_info(self, req, **kw): + id = kw['id'] + model = kw['model'] + node = kw['node'] + connector = kw['connector'] + src_node = kw['src_node'] + des_node = kw['des_node'] + + visible_node_fields = kw.get('visible_node_fields',[]) + invisible_node_fields = kw.get('invisible_node_fields',[]) + node_fields_string = kw.get('node_fields_string',[]) + connector_fields = kw.get('connector_fields',[]) + connector_fields_string = kw.get('connector_fields_string',[]) + + bgcolors = {} + shapes = {} + bgcolor = kw.get('bgcolor','') + shape = kw.get('shape','') + + if bgcolor: + for color_spec in bgcolor.split(';'): + if color_spec: + colour, color_state = color_spec.split(':') + bgcolors[colour] = color_state + + if shape: + for shape_spec in shape.split(';'): + if shape_spec: + shape_colour, shape_color_state = shape_spec.split(':') + shapes[shape_colour] = shape_color_state + + ir_view = req.session.model('ir.ui.view') + graphs = ir_view.graph_get(id, model, node, connector, src_node, des_node, False, + (140, 180), req.session.context) + + nodes = graphs['nodes'] + transitions = graphs['transitions'] + isolate_nodes = {} + + for node in graphs['blank_nodes']: + isolate_nodes[node['id']] = node + else: + y = map(lambda t: t['y'],filter(lambda x: x['y'] if x['x']==20 else None, nodes.values())) + y_max = (y and max(y)) or 120 + + connectors = {} + list_tr = [] + + for tr in transitions: + list_tr.append(tr) + connectors.setdefault(tr, { + 'id': tr, + 's_id': transitions[tr][0], + 'd_id': transitions[tr][1] + }) + connector_tr = req.session.model(connector) + connector_ids = connector_tr.search([('id', 'in', list_tr)], 0, 0, 0, req.session.context) + + data_connectors =connector_tr.read(connector_ids, connector_fields, req.session.context) + + + for tr in data_connectors: + t = connectors.get(str(tr['id'])) + t.update({ + 'source': tr[src_node][1], + 'destination': tr[des_node][1], + 'options': {} + }) + + for i, fld in enumerate(connector_fields): + t['options'][connector_fields_string[i]] = tr[fld] + + fields = req.session.model('ir.model.fields') + field_ids = fields.search([('model', '=', model), ('relation', '=', node)], 0, 0, 0, req.session.context) + field_data = fields.read(field_ids, ['relation_field'], req.session.context) + + node_act = req.session.model(node) + search_acts = node_act.search([(field_data[0]['relation_field'], '=', id)], 0, 0, 0, req.session.context) + data_acts = node_act.read(search_acts, invisible_node_fields + visible_node_fields, req.session.context) + + for act in data_acts: + n = nodes.get(str(act['id'])) + if not n: + n = isolate_nodes.get(act['id'], {}) + y_max += 140 + n.update({'x': 20, 'y': y_max}) + nodes[act['id']] = n + + n.update( + id=act['id'], + color='white', + shape='ellipse', + options={} + ) + for color, expr in bgcolors.items(): + if eval(expr, act): + n['color'] = color + + for shape, expr in shapes.items(): + if eval(expr, act): + n['shape'] = shape + + for i, fld in enumerate(visible_node_fields): + n['options'][node_fields_string[i]] = act[fld] + + #to relate m2o field of transition to corresponding o2m in activity + in_transition_field_id = fields.search([('relation', '=', connector), ('relation_field', '=', des_node), ('model', '=', node)], 0, 0, 0, req.session.context) + in_transition_field = fields.read(in_transition_field_id[0], ['name'], req.session.context)['name'] + + out_transition_field_id = fields.search([('relation', '=', connector), ('relation_field', '=', src_node), ('model', '=', node)], 0, 0, 0, req.session.context) + out_transition_field = fields.read(out_transition_field_id[0], ['name'], req.session.context)['name'] + + return dict(nodes=nodes, conn=connectors, in_transition_field=in_transition_field, out_transition_field=out_transition_field) diff --git a/addons/base_diagram/static/lib/js/Curry-1.0.1.js b/addons/base_diagram/static/lib/js/Curry-1.0.1.js new file mode 100644 index 00000000000..f5aa0f993a6 --- /dev/null +++ b/addons/base_diagram/static/lib/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/addons/base_diagram/static/lib/js/dracula_algorithms.js b/addons/base_diagram/static/lib/js/dracula_algorithms.js new file mode 100644 index 00000000000..0fbb0857fc8 --- /dev/null +++ b/addons/base_diagram/static/lib/js/dracula_algorithms.js @@ -0,0 +1,599 @@ +/* + * Various algorithms and data structures, licensed under the MIT-license. + * (c) 2010 by Johann Philipp Strathausen + * http://strathausen.eu + * + */ + + + +/* + Bellman-Ford + + Path-finding algorithm, finds the shortest paths from one node to all nodes. + + + Complexity + + O( |E| · |V| ), where E = edges and V = vertices (nodes) + + + Constraints + + 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; + + step("Initially, 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) { + step("Relax edge between " + edge.source.id + " and " + edge.target.id + "."); + edge.target.distance = edge.source.distance + edge.weight; + edge.target.predecessor = edge.source; + } + //Added by Jake Stothard (Needs to be tested) + if(!edge.style.directed) { + if(edge.target.distance + edge.weight < edge.source.distance) { + g.snapShot("Relax edge between "+edge.target.id+" and "+edge.source.id+"."); + edge.source.distance = edge.target.distance + edge.weight; + edge.source.predecessor = edge.target; + } + } + } + step("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 for sparse graphs (with less edges), 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 */ + + g.snapShot("Initially, all distances are infinite and all predecessors are null."); + + source.distance = 0; + /* set of unoptimized nodes, sorted by their distance (but a Fibonacci heap + would be better) */ + var q = new BinaryMinHeap(g.nodes, "distance"); + + /* pointer to the node in focus */ + var node; + + /* get the node with the smallest distance + as long as we have unoptimized nodes. q.min() can have O(log n). */ + 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) { + var other = (node == node.edges[e].target) ? node.edges[e].source : node.edges[e].target; + + if(other.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 < other.distance) { + + /* update distance of neighbour */ + other.distance = alt; + + /* update priority queue */ + q.heapify(); + + /* update path */ + other.predecessor = node; + g.snapShot("Enhancing node.") + } + } + } +} + + +/* All-Pairs-Shortest-Paths */ +/* 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, get shortest path */ + 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)); + } + + /* TODO use the knowledge, e.g. mark path in graph */ +} + +/* + Ford-Fulkerson + + Max-Flow-Min-Cut Algorithm finding the maximum flow through a directed + graph from source to sink. + + + Complexity + + O(E * max(f)), max(f) being the maximum flow + + + Description + + As long as there is an open path through the residual graph, send the + minimum of the residual capacities on the path. + + + Constraints + + The algorithm works only if all weights are integers. Otherwise it is + possible that the Ford–Fulkerson algorithm will not converge to the maximum + value. + + + Input + + g - Graph object + s - Source ID + t - Target (sink) ID + + + Output + + Maximum flow from Source s to Target t + + */ +/* + Edmonds-Karp + + Max-Flow-Min-Cut Algorithm finding the maximum flow through a directed + graph from source to sink. An implementation of the Ford-Fulkerson + algorithm. + + + Complexity + + O(|V|*|E|²) + + + Input + + g - Graph object (with node and edge lists, capacity is a property of edge) + s - source ID + t - sink ID + + */ +function edmonds_karp(g, s, t) { + +} + +/* + 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 or Chromium 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.foo = 0; // update key + a.heapify(); // call this always after having a 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); + } + } + + /* insert the input elements one by one only when we don't have a key property (TODO can be done more elegant) */ + for(i in (array || [])) + this.insert(array[i]); +} + + + +/* + 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. + TODO: This could be implemented more efficiently by using only one array object and several pointers. +*/ +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 considered 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) { + +} + +/** + * Knuth-Morris-Pratt string matching algorithm - finds a pattern in a text. + * FIXME: Doesn't work correctly yet. + */ +function kmp(p, t) { + + /** + * PREFIX, OVERLAP or FALIURE function for KMP. Computes how many iterations + * the algorithm can skip after a mismatch. + * + * @input p - pattern (string) + * @result array of skippable iterations + */ + function prefix(p) { + /* pi contains the computed skip marks */ + var pi = [0], k = 0; + for(q = 1; q < p.length; q++) { + while(k > 0 && (p.charAt(k) != p.charAt(q))) + k = pi[k-1]; + + (p.charAt(k) == p.charAt(q)) && k++; + + pi[q] = k; + } + return pi; + } + + /* The actual KMP algorithm starts here. */ + + var pi = prefix(p), q = 0, result = []; + + for(var i = 0; i < t.length; i++) { + /* jump forward as long as the character doesn't match */ + while((q > 0) && (p.charAt(q) != t.charAt(i))) + q = pi[q]; + + (p.charAt(q) == t.charAt(i)) && q++; + + (q == p.length) && result.push(i - p.length) && (q = pi[q]); + } + + return result; +} + +/* step for algorithm visualisation */ +function step(comment, funct) { + //wait for input + //display comment (before or after waiting) +// next.wait(); + /* execute callback function */ + funct(); +} + +/** + * 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); +}; + +/** + * Topological Sort + * + * Sort a directed graph based on incoming edges + * + * Coded by Jake Stothard + */ +function topological_sort(g) { + //Mark nodes as "deleted" instead of actually deleting them + //That way we don't have to copy g + + for(i in g.nodes) + g.nodes[i].deleted = false; + + var ret = topological_sort_helper(g); + + //Cleanup: Remove the deleted property + for(i in g.nodes) + delete g.nodes[i].deleted + + return ret; +} +function topological_sort_helper(g) { + //Find node with no incoming edges + var node; + for(i in g.nodes) { + if(g.nodes[i].deleted) + continue; //Bad style, meh + + var incoming = false; + for(j in g.nodes[i].edges) { + if(g.nodes[i].edges[j].target == g.nodes[i] + && g.nodes[i].edges[j].source.deleted == false) { + incoming = true; + break; + } + } + if(!incoming) { + node = g.nodes[i]; + break; + } + } + + // Either unsortable or done. Either way, GTFO + if(node == undefined) + return []; + + //"Delete" node from g + node.deleted = true; + + var tail = topological_sort_helper(g); + + tail.unshift(node); + + return tail; +} diff --git a/addons/base_diagram/static/lib/js/dracula_graffle.js b/addons/base_diagram/static/lib/js/dracula_graffle.js new file mode 100644 index 00000000000..9d4fd2099e8 --- /dev/null +++ b/addons/base_diagram/static/lib/js/dracula_graffle.js @@ -0,0 +1,106 @@ +/** + * 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; + } + /* function to be used for moving existent path(s), e.g. animate() or attr() */ + var move = "attr"; + /* applying path(s) */ + edge.fg && edge.fg[move]({path:path}) + || (edge.fg = selfRef.path(path).attr({stroke: style && style.stroke || "#000", fill: "none"}).toBack()); + edge.bg && edge.bg[move]({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": style["font-size"] || "12px"}))); +// && 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/addons/base_diagram/static/lib/js/dracula_graph.coffee b/addons/base_diagram/static/lib/js/dracula_graph.coffee new file mode 100644 index 00000000000..bb91945098d --- /dev/null +++ b/addons/base_diagram/static/lib/js/dracula_graph.coffee @@ -0,0 +1,524 @@ +### + * Dracula Graph Layout and Drawing Framework 0.0.3alpha + * (c) 2010 Philipp Strathausen , http://strathausen.eu + * + * Contributions by: + * Branched by Jake Stothard . + * + * 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 + * +### + + +### + 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) + } + } +} diff --git a/addons/base_diagram/static/lib/js/dracula_graph.js b/addons/base_diagram/static/lib/js/dracula_graph.js new file mode 100644 index 00000000000..f7069c5d2e5 --- /dev/null +++ b/addons/base_diagram/static/lib/js/dracula_graph.js @@ -0,0 +1,525 @@ +/* + * Dracula Graph Layout and Drawing Framework 0.0.3alpha + * (c) 2010 Philipp Strathausen , http://strathausen.eu + * Contributions by Jake Stothard . + * + * 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 . + * + * This code is freely distributable under the MIT license. Commercial use is + * hereby granted without any cost or restriction. + * + * Links: + * + * Graph Dracula JavaScript Framework: + * http://graphdracula.net + * + /*--------------------------------------------------------------------------*/ + +/* + * Edge Factory + */ +var AbstractEdge = function() { +} +AbstractEdge.prototype = { + hide: function() { + this.connection.fg.hide(); + this.connection.bg && this.bg.connection.hide(); + } +}; +var EdgeFactory = function() { + this.template = new AbstractEdge(); + this.template.style = new Object(); + this.template.style.directed = false; + this.template.weight = 1; +}; +EdgeFactory.prototype = { + build: function(source, target) { + var e = jQuery.extend(true, {}, this.template); + e.source = source; + e.target = target; + return e; + } +}; + +/* + * Graph + */ +var Graph = function() { + this.nodes = {}; + this.edges = []; + this.snapshots = []; // previous graph states TODO to be implemented + this.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: 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); + } + return this.nodes[id]; + }, + + addEdge: function(source, target, style) { + var s = this.addNode(source); + var t = this.addNode(target); + var edge = this.edgeFactory.build(s, t); + jQuery.extend(edge.style,style); + s.edges.push(edge); + this.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: function(comment) { + /* FIXME + var graph = new Graph(); + graph.nodes = jQuery.extend(true, {}, this.nodes); + graph.edges = jQuery.extend(true, {}, this.edges); + this.snapshots.push({comment: comment, graph: graph}); + */ + }, + removeNode: function(id) { + delete this.nodes[id]; + for(var i = 0; i < this.edges.length; i++) { + if (this.edges[i].source.id == id || this.edges[i].target.id == id) { + this.edges.splice(i, 1); + i--; + } + } + } +}; + +/* + * Node + */ +Graph.Node = function(id, node){ + node = node || {}; + node.id = id; + node.edges = []; + node.hide = function() { + this.hidden = true; + this.shape && this.shape.hide(); /* FIXME this is representation specific code and should be elsewhere */ + for(i in this.edges) + (this.edges[i].source.id == id || this.edges[i].target == id) && this.edges[i].hide && this.edges[i].hide(); + }; + node.show = function() { + this.hidden = false; + this.shape && this.shape.show(); + for(i in this.edges) + (this.edges[i].source.id == id || this.edges[i].target == id) && this.edges[i].show && this.edges[i].show(); + }; + return node; +}; +Graph.Node.prototype = { +}; + +/* + * Renderer base class + */ +Graph.Renderer = {}; + +/* + * Renderer implementation using RaphaelJS + */ +Graph.Renderer.Raphael = function(element, graph, width, height) { + this.width = width || 800; + this.height = height || 800; + 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(); + }; + + var d = document.getElementById(element); + d.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 - 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; + } + }; + d.onmouseup = function () { + selfRef.isDrag && selfRef.isDrag.set.animate({"fill-opacity": .6}, 500); + selfRef.isDrag = false; + }; + this.draw(); +}; +Graph.Renderer.Raphael.prototype = { + translate: function(point) { + return [ + (point[0] - this.graph.layoutMinX) * this.factorX + this.radius, + (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 = { 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)); + this.r.safari(); + return node; + }/* else, draw new nodes */ + + var shape; + + /* if a node renderer function is provided by the user, then use it + or the default render function instead */ + if(!node.render) { + node.render = function(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(this.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(function(item){ item.set = shape; item.node.style.cursor = "move"; }); + shape.mousedown(this.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: function(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 = this.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 = function(graph) { + this.graph = graph; + this.iterations = 500; + this.maxRepulsiveForceDistance = 6; + this.k = 2; + this.c = 0.01; + this.maxVertexMovement = 0.5; + this.layout(); +}; +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 + + var prev = new Array(); + for(var c in this.graph.nodes) { + var node1 = this.graph.nodes[c]; + for (var d in prev) { + var node2 = this.graph.nodes[prev[d]]; + this.layoutRepulsive(node1, node2); + + } + prev.push(c); + } + + // 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; + } +}; + +Graph.Layout.Ordered = function(graph, order) { + this.graph = graph; + this.order = order; + this.layout(); +}; +Graph.Layout.Ordered.prototype = { + layout: function() { + this.layoutPrepare(); + this.layoutCalcBounds(); + }, + + layoutPrepare: function(order) { + for (i in this.graph.nodes) { + var node = this.graph.nodes[i]; + node.layoutPosX = 0; + node.layoutPosY = 0; + } + var counter = 0; + for (i in this.order) { + var node = this.order[i]; + node.layoutPosX = counter; + node.layoutPosY = Math.random(); + counter++; + } + }, + + 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; + } +}; + +/* + * usefull JavaScript extensions, + */ + +function 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 = 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; +}; + +/* For IE */ +if (!Array.prototype.forEach) +{ + Array.prototype.forEach = function(fun /*, thisp*/) + { + var len = this.length; + if (typeof fun != "function") + 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); + } + }; +} diff --git a/addons/base_diagram/static/lib/js/raphael-min.js b/addons/base_diagram/static/lib/js/raphael-min.js new file mode 100644 index 00000000000..8718b5bc749 --- /dev/null +++ b/addons/base_diagram/static/lib/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/addons/base_diagram/static/src/js/diagram.js b/addons/base_diagram/static/src/js/diagram.js new file mode 100644 index 00000000000..af56c256ee7 --- /dev/null +++ b/addons/base_diagram/static/src/js/diagram.js @@ -0,0 +1,183 @@ +/*--------------------------------------------------------- + * OpenERP base library + *---------------------------------------------------------*/ + +openerp.base.diagram = function (openerp) { + +openerp.base.views.add('diagram', 'openerp.base.DiagramView'); +openerp.base.DiagramView = openerp.base.Controller.extend({ + init: function(view_manager, session, element_id, dataset, view_id){ + this._super(session, element_id); + this.view_manager = view_manager; + this.dataset = dataset; + this.model = dataset.model; + this.view_id = view_id; + this.name = ""; + this.domain = this.dataset._domain ? this.dataset._domain: []; + this.context = {}; + this.ids = this.dataset.ids; + + console.log('data set>>',this.dataset) + }, + start: function() { + this.rpc("/base_diagram/diagram/load", {"model": this.model, "view_id": this.view_id}, this.on_loaded); + }, + + toTitleCase: function(str) { + return str.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();}); + }, + + on_loaded: function(result) { + + var self = this; + if(this.ids && this.ids.length) { + this.id = this.ids[0]; + } + + this.fields_view = result.fields_view; + this.view_id = this.fields_view.view_id; + this.name = this.fields_view.name; + + this.fields = this.fields_view.fields; + + var children = this.fields_view.arch.children; + /* + * For Nodes (Fields) + */ + this.node = ''; + this.bgcolor = ''; + this.shape = ''; + this.visible_fields_nodes = []; + this.invisible_fields_nodes = []; + this.fields_nodes_string = []; + + /* + * For Arraows(Connector) + */ + this.connector = ''; + this.src_node = ''; + this.des_node = ''; + this.connector_fields = []; + this.fields_connector_string = []; + + for(ch in children) { + if(children[ch]['tag'] == 'node') { + this.node = children[ch]['attrs']['object']; + this.bgcolor = children[ch]['attrs']['bgcolor'] || ''; + this.shape = children[ch]['attrs']['shape'] || ''; + for(node_chld in children[ch]['children']) { + if (children[ch]['children'][node_chld]['tag'] = 'field') { + var ch_name = children[ch]['children'][node_chld]['attrs']['name']; + + if (children[ch]['children'][node_chld]['attrs']['invisible']) { + if (children[ch]['children'][node_chld]['attrs']['invisible'] == 1 && children[ch]['children'][node_chld]['attrs']['invisible'] == '1') { + this.invisible_fields_nodes.push(ch_name) + } + } + else { + this.visible_fields_nodes.push(ch_name); + var ch_node_string = this.fields[ch_name]['string'] || this.toTitleCase(ch_name); + this.fields_nodes_string.push(ch_node_string) + } + } + } + } else if(children[ch]['tag'] == 'arrow') { + this.connector = children[ch]['attrs']['object']; + this.src_node = children[ch]['attrs']['source']; + this.des_node = children[ch]['attrs']['destination']; + for (arrow_chld in children[ch]['children']) { + if (children[ch]['children'][arrow_chld]['tag'] = 'field') { + var arr_ch_name = children[ch]['children'][arrow_chld]['attrs']['name']; + var ch_node_string = this.fields[arr_ch_name]['string'] || this.toTitleCase(arr_ch_name); + this.fields_connector_string.push(ch_node_string); + this.connector_fields.push(arr_ch_name); + } + } + } + } + this.$element.html(QWeb.render("DiagramView", {"fields_view": this.fields_view})); + + if(this.id) { + this.rpc( + '/base_diagram/diagram/get_diagram_info', + { + 'id': this.id, + 'model': this.model, + 'bgcolor': this.bgcolor, + 'shape': this.shape, + 'node': this.node, + 'connector': this.connector, + 'src_node': this.src_node, + 'des_node': this.des_node, + 'visible_node_fields': this.visible_fields_nodes, + 'invisible_node_fields': this.invisible_fields_nodes, + 'node_fields_string': this.fields_nodes_string, + 'connector_fields': this.connector_fields, + 'connector_fields_string': this.fields_connector_string + }, + function(result) { + self.draw_diagram(result); + } + ) + } + }, + + draw_diagram: function(result) { + console.log('this>>>',this) + var g = new Graph(); +// var raphel = new + this.in_transition_field = result['in_transition_field']; + this.out_transition_field = result['out_transition_field']; + var res_nodes = result['nodes']; + var res_connectors = result['conn']; + + var render = function(r, n) { + var set; + if (n.node.shape == 'ellipse') { + set = r.set().push( + r.ellipse(n.node.x - 30, n.node.y - 13, 40, 40).attr({ + "fill": n.node.color, + r: "12px", + "stroke-width": n.distance == 0 ? "3px" : "1px" + })).push(r.text(n.node.x - 30, n.node.y - 10, (n.label || n.id))); + } else { + set = r.set().push( + r.rect(n.node.x-30, n.node.y-13, 60, 44).attr({"fill": n.node.color, r : "12px", "stroke-width" : n.distance == 0 ? "3px" : "1px" })).push( + r.text(n.point[0], n.point[1] + 10, (n.label || n.id) + "\n(" + (n.distance == undefined ? "Infinity" : n.distance) + ")")); + + } + return set; + }; + + for(nd in res_nodes) { + var res_node = res_nodes[nd]; + g.addNode(res_node['name'], + { + node: res_node, + render: render + }); + } + + for(cr in res_connectors) { + var res_connector = res_connectors[cr]; + g.addEdge(res_connector['source'], res_connector['destination']); + } + + var layouter = new Graph.Layout.Spring(g); + layouter.layout(); + + var renderer = new Graph.Renderer.Raphael('dia-canvas', g, 800, 800); + renderer.draw(); + }, + + do_show: function () { + this.$element.show(); + }, + + do_hide: function () { + this.$element.hide(); + } +}); +}; + +// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/base_diagram/static/src/js/views.js b/addons/base_diagram/static/src/js/views.js deleted file mode 100644 index 606e1e5c78c..00000000000 --- a/addons/base_diagram/static/src/js/views.js +++ /dev/null @@ -1,12 +0,0 @@ -/*--------------------------------------------------------- - * OpenERP base library - *---------------------------------------------------------*/ - -openerp.base_diagram = function(openerp) { - -openerp.base_diagram.DiagramView = openerp.base.Controller.extend({ -}); - -}; - -// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: