f2ac421d0884a6f39809393423004e6b2fe08c37
[odoo/odoo.git] / addons / web_diagram / static / src / js / graph.js
1
2 (function(window){
3
4     // this serves as the end of an edge when creating a link
5     function EdgeEnd(pos_x,pos_y){
6         this.x = pos_x;
7         this.y = pos_y;
8
9         this.get_pos = function(){
10             return new Vec2(this.x,this.y);
11         }
12     }
13
14     // A close button, 
15     // if entity_type == "node":
16     //      GraphNode.destruction_callback(entity) is called where entity is a node.
17     //      If it returns true the node and all connected edges are destroyed.
18     // if entity_type == "edge":
19     //      GraphEdge.destruction_callback(entity) is called where entity is an edge
20     //      If it returns true the edge is destroyed
21     // pos_x,pos_y is the relative position of the close button to the entity position (entity.get_pos())
22
23     function CloseButton(graph, entity, entity_type, pos_x,pos_y){
24         var self = this;
25         var visible = false;
26         var close_button_radius = graph.style.close_button_radius || 8;
27         var close_circle = graph.r.circle(  entity.get_pos().x + pos_x, 
28                                             entity.get_pos().y + pos_y, 
29                                             close_button_radius           );
30         //the outer gray circle
31         close_circle.attr({ 'opacity':  0,
32                             'fill':     graph.style.close_button_color || "black",
33                             'cursor':   'pointer',
34                             'stroke':   'none'  });
35         close_circle.transform(graph.get_transform());
36         graph.set_scrolling(close_circle);
37         
38         //the 'x' inside the circle
39         var close_label = graph.r.text( entity.get_pos().x + pos_x, entity.get_pos().y + pos_y,"x");
40         close_label.attr({  'fill':         graph.style.close_button_x_color || "white",
41                             'font-size':    close_button_radius,
42                             'cursor':       'pointer'   });
43         
44         close_label.transform(graph.get_transform());
45         graph.set_scrolling(close_label);
46         
47         // the dummy_circle is used to catch events, and avoid hover in/out madness 
48         // between the 'x' and the button
49         var dummy_circle = graph.r.circle(  entity.get_pos().x + pos_x,
50                                             entity.get_pos().y + pos_y,
51                                             close_button_radius           );
52         dummy_circle.attr({'opacity':1, 'fill': 'transparent', 'stroke':'none', 'cursor':'pointer'});
53         dummy_circle.transform(graph.get_transform());
54         graph.set_scrolling(dummy_circle);
55
56         this.get_pos = function(){
57             return entity.get_pos().add_xy(pos_x,pos_y);
58         };
59
60         this.update_pos = function(){
61             var pos = self.get_pos(); 
62             close_circle.attr({'cx':pos.x, 'cy':pos.y});
63             dummy_circle.attr({'cx':pos.x, 'cy':pos.y});
64             close_label.attr({'x':pos.x, 'y':pos.y});
65         };
66         
67         function hover_in(){
68             if(!visible){ return; }
69             close_circle.animate({'r': close_button_radius * 1.5}, 300, 'elastic');
70             dummy_circle.animate({'r': close_button_radius * 1.5}, 300, 'elastic');
71         }
72         function hover_out(){
73             if(!visible){ return; }
74             close_circle.animate({'r': close_button_radius},400,'linear');
75             dummy_circle.animate({'r': close_button_radius},400,'linear');
76         }
77         dummy_circle.hover(hover_in,hover_out);
78
79         function click_action(){
80             if(!visible){ return; }
81
82             close_circle.attr({'r': close_button_radius * 2 });
83             dummy_circle.attr({'r': close_button_radius * 2 });
84             close_circle.animate({'r': close_button_radius }, 400, 'linear');
85             dummy_circle.animate({'r': close_button_radius }, 400, 'linear');
86
87             if(entity_type == "node"){
88                 if(GraphNode.destruction_callback(entity)){
89                     //console.log("remove node",entity);
90                     entity.remove();
91                 }
92             }else if(entity_type == "edge"){
93                 if(GraphEdge.destruction_callback(entity)){
94                     //console.log("remove edge",entity);
95                     entity.remove();
96                 }
97             }
98         }
99         dummy_circle.click(click_action);
100
101         this.show = function(){
102             if(!visible){
103                 close_circle.animate({'opacity':1}, 100, 'linear');
104                 close_label.animate({'opacity':1}, 100, 'linear');
105                 visible = true;
106             }
107         }
108         this.hide = function(){
109             if(visible){
110                 close_circle.animate({'opacity':0}, 100, 'linear');
111                 close_label.animate({'opacity':0}, 100, 'linear');
112                 visible = false;
113             }
114         }
115         //destroy this object and remove it from the graph
116         this.remove = function(){
117             if(visible){
118                 visible = false;
119                 close_circle.animate({'opacity':0}, 100, 'linear');
120                 close_label.animate({'opacity':0}, 100, 'linear',self.remove);
121             }else{
122                 close_circle.remove();
123                 close_label.remove();
124                 dummy_circle.remove();
125             }
126         }
127     }
128
129     // connectors are start and end point of edge creation drags.
130     function Connector(graph,node,pos_x,pos_y){
131         var visible = false;
132         var conn_circle = graph.r.circle(node.get_pos().x + pos_x, node.get_pos().y + pos_y,4);
133         conn_circle.attr({  'opacity':  0, 
134                             'fill':     graph.style.node_outline_color,
135                             'stroke':   'none' });
136         conn_circle.transform(graph.get_transform());
137         graph.set_scrolling(conn_circle);
138
139         var self = this;
140
141         this.update_pos = function(){
142             conn_circle.attr({'cx':node.get_pos().x + pos_x, 'cy':node.get_pos().y + pos_y});
143         };
144         this.get_pos = function(){
145             return new node.get_pos().add_xy(pos_x,pos_y);
146         };
147         this.remove = function(){
148             conn_circle.remove();
149         }
150         function hover_in(){
151             if(!visible){ return;}
152             conn_circle.animate({'r':8},300,'elastic');
153             if(graph.creating_edge){
154                 graph.target_node = node; 
155                 conn_circle.animate({   'fill':         graph.style.connector_active_color,
156                                         'stroke':       graph.style.node_outline_color,
157                                         'stroke-width': graph.style.node_selected_width,
158                                     },100,'linear');
159             }
160         }
161         function hover_out(){
162             if(!visible){ return;}
163             conn_circle.animate({   'r':graph.style.connector_radius, 
164                                     'fill':graph.style.node_outline_color, 
165                                     'stroke':'none'},400,'linear');
166             graph.target_node = null;
167         }
168         conn_circle.hover(hover_in,hover_out);
169
170
171         var drag_down = function(){
172             if(!visible){ return; }
173             self.ox = conn_circle.attr("cx");
174             self.oy = conn_circle.attr("cy");
175             self.edge_start = new EdgeEnd(self.ox,self.oy);
176             self.edge_end = new EdgeEnd(self.ox, self.oy);
177             self.edge_tmp = new GraphEdge(graph,'',self.edge_start,self.edge_end,true);
178             graph.creating_edge = true;
179         };
180         var drag_move = function(dx,dy){
181             if(!visible){ return; }
182             self.edge_end.x = self.ox + dx;
183             self.edge_end.y = self.oy + dy;
184             self.edge_tmp.update();
185         };
186         var drag_up = function(){
187             if(!visible){ return; }
188             graph.creating_edge = false;
189             self.edge_tmp.remove();
190             if(graph.target_node){  
191                 var edge_prop = GraphEdge.creation_callback(node,graph.target_node);
192                 if(edge_prop){
193                     var new_edge = new GraphEdge(graph,edge_prop.label, node,graph.target_node);
194                     GraphEdge.new_edge_callback(new_edge);
195                 }
196             }
197         };
198         conn_circle.drag(drag_move,drag_down,drag_up);
199
200         function show(){
201             if(!visible){
202                 conn_circle.animate({'opacity':1}, 100, 'linear');
203                 visible = true;
204             }
205         }
206         function hide(){
207             if(visible){
208                 conn_circle.animate({'opacity':0}, 100, 'linear');
209                 visible = false;
210             }
211         }
212         this.show = show;
213         this.hide = hide;
214     }
215     
216     //Creates a new graph on raphael document r.
217     //style is a dictionary containing the style definitions
218     //viewport (optional) is the dom element representing the viewport of the graph. It is used
219     //to prevent scrolling to scroll the graph outside the viewport.
220
221     function Graph(r,style,viewport){
222         var self = this;
223         var nodes = [];  // list of all nodes in the graph
224         var edges = [];  // list of all edges in the graph
225         var graph = {};  // graph[n1.uid][n2.uid] -> list of all edges from n1 to n2
226         var links = {};  // links[n.uid] -> list of all edges from or to n
227         var uid = 1;     // all nodes and edges have an uid used to order their display when they are curved
228         var selected_entity = null; //the selected entity (node or edge) 
229         
230         self.creating_edge = false; // true if we are dragging a new edge onto a node
231         self.target_node = null;    // this holds the target node when creating an edge and hovering a connector
232         self.r = r;                 // the raphael instance
233         self.style  = style;        // definition of the colors, spacing, fonts, ... used by the elements
234         var tr_x = 0, tr_y = 0;         // global translation coordinate
235
236         var background = r.rect(0,0,'100%','100%').attr({'fill':'white', 'stroke':'none', 'opacity':0, 'cursor':'move'});
237         
238         // return the global transform of the scene
239         this.get_transform = function(){
240             return "T"+tr_x+","+tr_y
241         };
242
243         
244         // translate every element of the graph except the background. 
245         // elements inserted in the graph after a translate_all() must manually apply transformation 
246         // via get_transform() 
247         var translate_all = function(dx,dy){
248             tr_x += dx;
249             tr_y += dy;
250             var tstr = self.get_transform();
251             
252             r.forEach(function(el){
253                 if(el != background){
254                     el.transform(tstr);
255                 }
256             });
257         };
258         //returns {minx, miny, maxx, maxy}, the translated bounds containing all nodes
259         var get_bounds = function(){
260             var minx = Number.MAX_VALUE;
261             var miny = Number.MAX_VALUE;
262             var maxx = Number.MIN_VALUE;
263             var maxy = Number.MIN_VALUE;
264             
265             for(var i = 0; i < nodes.length; i++){
266                 var pos = nodes[i].get_pos();
267                 minx = Math.min(minx,pos.x);
268                 miny = Math.min(miny,pos.y);
269                 maxx = Math.max(maxx,pos.x);
270                 maxy = Math.max(maxy,pos.y);
271             }
272
273             minx = minx - style.node_size_x / 2 + tr_x;
274             miny = miny - style.node_size_y / 2 + tr_y;
275             maxx = maxx + style.node_size_x / 2 + tr_x;
276             maxy = maxy + style.node_size_y / 2 + tr_y;
277
278             return { minx:minx, miny:miny, maxx:maxx, maxy:maxy };
279         
280         };
281         // returns false if the translation dx,dy of the viewport 
282         // hides the graph (with optional margin)
283         var translation_respects_viewport = function(dx,dy,margin){
284             if(!viewport){
285                 return true;
286             }
287             margin = margin || 0;
288             var b = get_bounds();
289             var width = viewport.offsetWidth; 
290             var height = viewport.offsetHeight;
291             
292             if( ( dy < 0 && b.maxy + dy < margin )   ||
293                 ( dy > 0 && b.miny + dy > height - margin ) ||
294                 ( dx < 0 && b.maxx + dx < margin ) ||
295                 ( dx > 0 && b.minx + dx > width - margin ) ){
296                 return false;
297             }
298
299             return true;
300         }
301         //Adds a mousewheel event callback to raph_element that scrolls the viewport
302         this.set_scrolling = function(raph_element){
303             $(raph_element.node).bind('mousewheel',function(event,delta){
304                 var dy = delta * 20;
305                 if( translation_respects_viewport(0,dy, style.viewport_margin) ){
306                     translate_all(0,dy);
307                 }
308             });
309         };
310
311         var px, py;
312         // Graph translation when background is dragged
313         var bg_drag_down = function(){
314             px = py = 0;
315         };
316         var bg_drag_move = function(x,y){
317             var dx = x - px;
318             var dy = y - py;
319             px = x;
320             py = y;
321             if( translation_respects_viewport(dx,dy, style.viewport_margin) ){
322                 translate_all(dx,dy);
323             }
324         };
325         var bg_drag_up   = function(){};
326         background.drag( bg_drag_move, bg_drag_down, bg_drag_up);
327         
328         this.set_scrolling(background);
329
330         //adds a node to the graph and sets its uid.
331         this.add_node = function (n){
332             nodes.push(n);
333             n.uid = uid++;
334         };
335
336         //return the list of all nodes in the graph
337         this.get_node_list = function(){
338             return nodes;
339         };
340
341         //adds an edge to the graph and sets its uid
342         this.add_edge = function (n1,n2,e){
343             edges.push(e);
344             e.uid = uid++;
345             if(!graph[n1.uid])          graph[n1.uid] = {};
346             if(!graph[n1.uid][n2.uid])  graph[n1.uid][n2.uid] = [];
347             if(!links[n1.uid]) links[n1.uid] = [];
348             if(!links[n2.uid]) links[n2.uid] = [];
349
350             graph[n1.uid][n2.uid].push(e);
351             links[n1.uid].push(e);
352             if(n1 != n2){
353                 links[n2.uid].push(e);
354             }
355         };
356
357         //removes an edge from the graph
358         this.remove_edge = function(edge){
359             edges = _.without(edges,edge);
360             var n1 = edge.get_start();
361             var n2 = edge.get_end();
362             links[n1.uid] = _.without(links[n1.uid],edge);
363             links[n2.uid] = _.without(links[n2.uid],edge);
364             graph[n1.uid][n2.uid] = _.without(graph[n1.uid][n2.uid],edge);
365             if ( selected_entity == edge ){
366                 selected_entity = null;
367             }
368         };
369         //removes a node and all connected edges from the graph
370         this.remove_node = function(node){
371             var linked_edges = self.get_linked_edge_list(node);
372             for(var i = 0; i < linked_edges.length; i++){
373                 linked_edges[i].remove();
374             }
375             nodes = _.without(nodes,node);
376
377             if ( selected_entity == node ){
378                 selected_entity = null;
379             }
380         }
381
382
383         //return the list of edges from n1 to n2
384         this.get_edge_list = function(n1,n2){
385             var list = [];
386             if(!graph[n1.uid]) return list;
387             if(!graph[n1.uid][n2.uid]) return list;
388             return graph[n1.uid][n2.uid];
389         };
390         //returns the list of all edge connected to n
391         this.get_linked_edge_list = function(n){
392             if(!links[n.uid]) return [];
393             return links[n.uid];
394         };
395         //return a curvature index so that all edges connecting n1,n2 have different curvatures
396         this.get_edge_curvature = function(n1,n2,e){
397             var el_12 = this.get_edge_list(n1,n2);
398             var c12   = el_12.length;
399             var el_21 = this.get_edge_list(n2,n1);
400             var c21   = el_21.length;
401             if(c12 + c21 == 1){ // only one edge 
402                 return 0;
403             }else{ 
404                 var index = 0;
405                 for(var i = 0; i < c12; i++){
406                     if (el_12[i].uid < e.uid){
407                         index++;
408                     }
409                 }
410                 if(c21 == 0){   // all edges in the same direction
411                     return index - (c12-1)/2.0;
412                 }else{
413                     return index + 0.5;
414                 }
415             }
416         };
417         
418
419         // Returns the angle in degrees of the edge loop. We do not support more than 8 loops on one node
420         this.get_loop_angle = function(n,e){
421             var loop_list = this.get_edge_list(n,n);
422
423             var slots = []; // the 8 angles where we can put the loops 
424             for(var angle = 0; angle < 360; angle += 45){
425                 slots.push(Vec2.new_polar_deg(1,angle));
426             }
427             
428             //we assign to each slot a score. The higher the score, the closer it is to other edges.
429             var links = this.get_linked_edge_list(n);
430             for(var i = 0; i < links.length; i++){
431                 var edge = links[i];
432                 if(!edge.is_loop || edge.is_loop()){
433                     continue;
434                 }
435                 var end = edge.get_end();
436                 if (end == n){
437                     end = edge.get_start();
438                 }
439                 var dir = end.get_pos().sub(n.get_pos()).normalize();
440                 for(var s = 0; s < slots.length; s++){
441                     var score = slots[s].dot(dir);
442                     if(score < 0){
443                         score = -0.2*Math.pow(score,2);
444                     }else{
445                         score = Math.pow(score,2);
446                     }
447                     if(!slots[s].score){
448                         slots[s].score = score;
449                     }else{
450                         slots[s].score += score;
451                     }
452                 }
453             }
454             //we want the loops with lower uid to get the slots with the lower score
455             slots.sort(function(a,b){ return a.score < b.score ? -1: 1; });
456             
457             var index = 0;
458             for(var i = 0; i < links.length; i++){
459                 var edge = links[i];
460                 if(!edge.is_loop || !edge.is_loop()){
461                     continue;
462                 }
463                 if(edge.uid < e.uid){
464                     index++;
465                 }
466             }
467             index %= slots.length;
468             
469             return slots[index].angle_deg();
470         }
471
472         //selects a node or an edge and deselects everything else
473         this.select = function(entity){
474             if(selected_entity){
475                 if(selected_entity == entity){
476                     return;
477                 }else{
478                     if(selected_entity.set_not_selected){
479                         selected_entity.set_not_selected();
480                     }
481                     selected_entity = null;
482                 }
483             }
484             selected_entity = entity;
485             if(entity && entity.set_selected){
486                 entity.set_selected();
487             }
488         };
489     }
490
491     // creates a new Graph Node on Raphael document r, centered on [pos_x,pos_y], with label 'label', 
492     // and of type 'circle' or 'rect', and of color 'color'
493     function GraphNode(graph,pos_x, pos_y,label,type,color){
494         var self = this;
495         var r  = graph.r;
496         var sy = graph.style.node_size_y;
497         var sx = graph.style.node_size_x;
498         var node_fig = null;
499         var selected = false;
500         this.connectors = [];
501         this.close_button = null;
502         this.uid = 0;
503         
504         graph.add_node(this);
505
506         if(type == 'circle'){
507             node_fig = r.ellipse(pos_x,pos_y,sx/2,sy/2);
508         }else{
509             node_fig = r.rect(pos_x-sx/2,pos_y-sy/2,sx,sy);
510         }
511         node_fig.attr({ 'fill':         color, 
512                         'stroke':       graph.style.node_outline_color,
513                         'stroke-width': graph.style.node_outline_width,
514                         'cursor':'pointer'  });
515         node_fig.transform(graph.get_transform());
516         graph.set_scrolling(node_fig);
517
518         var node_label = r.text(pos_x,pos_y,label);
519         node_label.attr({   'fill':         graph.style.node_label_color,
520                             'font-size':    graph.style.node_label_font_size,
521                             'cursor':       'pointer'   });
522         node_label.transform(graph.get_transform());
523         graph.set_scrolling(node_label);
524
525         // redraws all edges linked to this node 
526         var update_linked_edges = function(){
527             var edges = graph.get_linked_edge_list(self);
528             for(var i = 0; i < edges.length; i++){
529                 edges[i].update();
530             }
531         };
532
533         // sets the center position of the node
534         var set_pos = function(pos){
535             if(type == 'circle'){
536                 node_fig.attr({'cx':pos.x,'cy':pos.y});
537             }else{
538                 node_fig.attr({'x':pos.x-sx/2,'y':pos.y-sy/2});
539             }
540             node_label.attr({'x':pos.x,'y':pos.y});
541             for(var i = 0; i < self.connectors.length; i++){
542                 self.connectors[i].update_pos();
543             }
544             if(self.close_button){
545                 self.close_button.update_pos();
546             }
547             update_linked_edges();
548         };
549         // returns the figure used to draw the node
550         var get_fig = function(){
551             return node_fig;
552         };
553         // returns the center coordinates
554         var get_pos = function(){
555             if(type == 'circle'){ 
556                 return new Vec2(node_fig.attr('cx'), node_fig.attr('cy')); 
557             }else{ 
558                 return new Vec2(node_fig.attr('x') + sx/2, node_fig.attr('y') + sy/2); 
559             }
560         };
561         // return the label string
562         var get_label = function(){
563             return node_label.attr("text");
564         };
565         // sets the label string
566         var set_label = function(text){
567             node_label.attr({'text':text});
568         };
569         var get_bound = function(){
570             if(type == 'circle'){
571                 return new BEllipse(get_pos().x,get_pos().y,sx/2,sy/2);
572             }else{
573                 return BRect.new_centered(get_pos().x,get_pos().y,sx,sy);
574             }
575         };
576         // selects this node and deselects all other nodes
577         var set_selected = function(){
578             if(!selected){
579                 selected = true;
580                 node_fig.attr({ 'stroke':       graph.style.node_selected_color, 
581                                 'stroke-width': graph.style.node_selected_width });
582                 if(!self.close_button){
583                     self.close_button = new CloseButton(graph,self, "node" ,sx/2 , - sy/2);
584                     self.close_button.show();
585                 }
586                 for(var i = 0; i < self.connectors.length; i++){
587                     self.connectors[i].show();
588                 }
589             }
590         };
591         // deselect this node
592         var set_not_selected = function(){
593             if(selected){
594                 node_fig.animate({  'stroke':       graph.style.node_outline_color,
595                                     'stroke-width': graph.style.node_outline_width },
596                                     100,'linear');
597                 if(self.close_button){
598                     self.close_button.remove();
599                     self.close_button = null;
600                 }
601                 selected = false;
602             }
603             for(var i = 0; i < self.connectors.length; i++){
604                 self.connectors[i].hide();
605             }
606         };
607         var remove = function(){
608             if(self.close_button){
609                 self.close_button.remove();
610             }
611             for(var i = 0; i < self.connectors.length; i++){
612                 self.connectors[i].remove();
613             }
614             graph.remove_node(self);
615             node_fig.remove();
616             node_label.remove();
617         }
618
619
620         this.set_pos = set_pos;
621         this.get_pos = get_pos;
622         this.set_label = set_label;
623         this.get_label = get_label;
624         this.get_bound = get_bound;
625         this.get_fig   = get_fig;
626         this.set_selected = set_selected;
627         this.set_not_selected = set_not_selected;
628         this.update_linked_edges = update_linked_edges;
629         this.remove = remove;
630
631        
632         //select the node and play an animation when clicked
633         var click_action = function(){
634             if(type == 'circle'){
635                 node_fig.attr({'rx':sx/2 + 3, 'ry':sy/2+ 3});
636                 node_fig.animate({'rx':sx/2, 'ry':sy/2},500,'elastic');
637             }else{
638                 var cx = get_pos().x;
639                 var cy = get_pos().y;
640                 node_fig.attr({'x':cx - (sx/2) - 3, 'y':cy - (sy/2) - 3, 'ẃidth':sx+6, 'height':sy+6});
641                 node_fig.animate({'x':cx - sx/2, 'y':cy - sy/2, 'ẃidth':sx, 'height':sy},500,'elastic');
642             }
643             graph.select(self);
644         };
645         node_fig.click(click_action);
646         node_label.click(click_action);
647
648         //move the node when dragged
649         var drag_down = function(){
650             this.opos = get_pos();
651         };
652         var drag_move = function(dx,dy){
653             // we disable labels when moving for performance reasons, 
654             // updating the label position is quite expensive
655             // we put this here because drag_down is also called on simple clicks ... and this causes unwanted flicker
656             var edges = graph.get_linked_edge_list(self);
657             for(var i = 0; i < edges.length; i++){
658                 edges[i].label_disable();
659             }
660             if(self.close_button){
661                 self.close_button.hide();
662             }
663             set_pos(this.opos.add_xy(dx,dy));
664         };
665         var drag_up = function(){
666             //we re-enable the 
667             var edges = graph.get_linked_edge_list(self);
668             for(var i = 0; i < edges.length; i++){
669                 edges[i].label_enable();
670             }
671             if(self.close_button){
672                 self.close_button.show();
673             }
674         };
675         node_fig.drag(drag_move,drag_down,drag_up);
676         node_label.drag(drag_move,drag_down,drag_up);
677
678         //allow the user to create edges by dragging onto the node
679         function hover_in(){
680             if(graph.creating_edge){
681                 graph.target_node = self; 
682             }
683         }
684         function hover_out(){
685             graph.target_node = null;
686         }
687         node_fig.hover(hover_in,hover_out);
688         node_label.hover(hover_in,hover_out);
689
690         function double_click(){
691             GraphNode.double_click_callback(self);
692         }
693         node_fig.dblclick(double_click);
694         node_label.dblclick(double_click);
695
696         this.connectors.push(new Connector(graph,this,-sx/2,0));
697         this.connectors.push(new Connector(graph,this,sx/2,0));
698         this.connectors.push(new Connector(graph,this,0,-sy/2));
699         this.connectors.push(new Connector(graph,this,0,sy/2));
700
701         this.close_button = new CloseButton(graph,this,"node",sx/2 , - sy/2 );
702     }
703
704     GraphNode.double_click_callback = function(node){
705         console.log("double click from node:",node);
706     };
707
708     // this is the default node destruction callback. It is called before the node is removed from the graph
709     // and before the connected edges are destroyed 
710     GraphNode.destruction_callback = function(node){ return true; };
711
712     // creates a new edge with label 'label' from start to end. start and end must implement get_pos_*, 
713     // if tmp is true, the edge is not added to the graph, used for drag edges. 
714     // replace tmp == false by graph == null 
715     function GraphEdge(graph,label,start,end,tmp){
716         var self = this;
717         var r = graph.r;
718         var curvature = 0;  // 0 = straight, != 0 curved
719         var s,e;            // positions of the start and end point of the line between start and end
720         var mc;             // position of the middle of the curve (bezier control point) 
721         var mc1,mc2;        // control points of the cubic bezier for the loop edges
722         var elfs =  graph.style.edge_label_font_size || 10 ; 
723         var label_enabled = true;
724         this.uid = 0;       // unique id used to order the curved edges
725         var edge_path = ""; // svg definition of the edge vector path
726         var selected = false;
727
728         if(!tmp){
729             graph.add_edge(start,end,this);
730         }
731         
732         //Return the position of the label
733         function get_label_pos(path){
734             var cpos = path.getTotalLength() * 0.5;
735             var cindex = Math.abs(Math.floor(curvature));
736             var mod = ((cindex % 3)) * (elfs * 3.1) - (elfs * 0.5);
737             var verticality = Math.abs(end.get_pos().sub(start.get_pos()).normalize().dot_xy(0,1));
738             verticality = Math.max(verticality-0.5,0)*2;
739
740             var lpos = path.getPointAtLength(cpos + mod * verticality);
741             return new Vec2(lpos.x,lpos.y - elfs *(1-verticality));
742         }
743         
744         //used by close_button
745         this.get_pos = function(){
746             if(!edge){
747                 return start.get_pos().lerp(end.get_pos(),0.5);
748             }
749             if(!edge_label){
750                 return get_label_pos(edge);
751             }
752             var bbox = edge_label.getBBox();
753             return new Vec2(bbox.x + bbox.width, bbox.y);
754         }
755
756         //Straight line from s to e
757         function make_line(){
758             return "M" + s.x + "," + s.y + "L" + e.x + "," + e.y ;
759         }
760         //Curved line from s to e by mc
761         function make_curve(){
762             return "M" + s.x + "," + s.y + "Q" + mc.x + "," + mc.y + " " + e.x + "," + e.y;
763         }
764         //Curved line from s to e by mc1 mc2
765         function make_loop(){
766             return "M" + s.x + " " + s.y + 
767                    "C" + mc1.x + " " + mc1.y + " " + mc2.x + " " + mc2.y + " " + e.x + " " + e.y;
768         }
769             
770         //computes new start and end line coordinates
771         function update_curve(){
772             if(start != end){
773                 if(!tmp){
774                     curvature = graph.get_edge_curvature(start,end,self);
775                 }else{
776                     curvature = 0;
777                 }
778                 s = start.get_pos();
779                 e = end.get_pos();
780                 
781                 mc = s.lerp(e,0.5); //middle of the line s->e
782                 var se = e.sub(s);
783                 se = se.normalize();
784                 se = se.rotate_deg(-90);
785                 se = se.scale(curvature * graph.style.edge_spacing);
786                 mc = mc.add(se);
787
788                 if(start.get_bound){
789                     var col = start.get_bound().collide_segment(s,mc);
790                     if(col.length > 0){
791                         s = col[0];
792                     }
793                 }
794                 if(end.get_bound){
795                     var col = end.get_bound().collide_segment(mc,e);
796                     if(col.length > 0){
797                         e = col[0];
798                     }
799                 }
800                 
801                 if(curvature != 0){
802                     edge_path = make_curve();
803                 }else{
804                     edge_path = make_line();
805                 }
806             }else{ // start == end
807                 var rad = graph.style.edge_loop_radius || 100;
808                 s = start.get_pos();
809                 e = end.get_pos();
810
811                 var r = Vec2.new_polar_deg(rad,graph.get_loop_angle(start,self));
812                 mc = s.add(r);
813                 var p = r.rotate_deg(90);
814                 mc1 = mc.add(p.set_len(rad*0.5));
815                 mc2 = mc.add(p.set_len(-rad*0.5));
816                 
817                 if(start.get_bound){
818                     var col = start.get_bound().collide_segment(s,mc1);
819                     if(col.length > 0){
820                         s = col[0];
821                     }
822                     var col = start.get_bound().collide_segment(e,mc2);
823                     if(col.length > 0){
824                         e = col[0];
825                     }
826                 }
827                 edge_path = make_loop();
828             }
829         }
830         
831         update_curve();
832         var edge = r.path(edge_path).attr({ 'stroke':       graph.style.edge_color, 
833                                             'stroke-width': graph.style.edge_width, 
834                                             'arrow-end':    'block-wide-long', 
835                                             'cursor':'pointer'  }).insertBefore(graph.get_node_list()[0].get_fig());       
836         var labelpos = get_label_pos(edge);
837         var edge_label = r.text(labelpos.x, labelpos.y - elfs, label).attr({
838             'fill':         graph.style.edge_label_color, 
839             'cursor':       'pointer', 
840             'font-size':    elfs    });
841
842         edge.transform(graph.get_transform());
843         graph.set_scrolling(edge);
844
845         edge_label.transform(graph.get_transform());
846         graph.set_scrolling(edge_label);
847         
848
849         //since we create an edge we need to recompute the edges that have the same start and end positions as this one
850         if(!tmp){
851             var edges_start = graph.get_linked_edge_list(start);
852             var edges_end   = graph.get_linked_edge_list(end);
853             var edges = edges_start.length < edges_end.length ? edges_start : edges_end;
854             for(var i = 0; i < edges.length; i ++){
855                 if(edges[i] != self){
856                     edges[i].update();
857                 }
858             }
859         }
860         function label_enable(){
861             if(!label_enabled){
862                 label_enabled = true;
863                 edge_label.animate({'opacity':1},100,'linear');
864                 if(self.close_button){
865                     self.close_button.show();
866                 }
867                 self.update();
868             }
869         }
870         function label_disable(){
871             if(label_enabled){
872                 label_enabled = false;
873                 edge_label.animate({'opacity':0},100,'linear');
874                 if(self.close_button){
875                     self.close_button.hide();
876                 }
877             }
878         }
879         //update the positions 
880         function update(){
881             update_curve();
882             edge.attr({'path':edge_path});
883             if(label_enabled){
884                 var labelpos = get_label_pos(edge);
885                 edge_label.attr({'x':labelpos.x, 'y':labelpos.y - 14});
886             }
887         }
888         // removes the edge from the scene, disconnects it from linked        
889         // nodes, destroy its drawable elements.
890         function remove(){
891             edge.remove();
892             edge_label.remove();
893             if(!tmp){
894                 graph.remove_edge(self);
895             }
896             if(start.update_linked_edges){
897                 start.update_linked_edges();
898             }
899             if(start != end && end.update_linked_edges){
900                 end.update_linked_edges();
901             }
902             if(self.close_button){
903                 self.close_button.remove();
904             }
905         }
906
907         this.set_selected = function(){
908             if(!selected){
909                 selected = true;
910                 edge.attr({ 'stroke': graph.style.node_selected_color, 
911                             'stroke-width': graph.style.node_selected_width });
912                 edge_label.attr({ 'fill': graph.style.node_selected_color });
913                 if(!self.close_button){
914                     self.close_button = new CloseButton(graph,self,"edge",6,-6);
915                     self.close_button.show();
916                 }
917             }
918         };
919
920         this.set_not_selected = function(){
921             if(selected){
922                 selected = false;
923                 edge.animate({  'stroke':       graph.style.edge_color,
924                                 'stroke-width': graph.style.edge_width }, 100,'linear');
925                 edge_label.animate({ 'fill':    graph.style.edge_label_color}, 100, 'linear');
926                 if(self.close_button){
927                     self.close_button.remove();
928                     self.close_button = null;
929                 }
930             }
931         };
932         function click_action(){
933             graph.select(self);
934         }
935         edge.click(click_action);
936         edge_label.click(click_action);
937
938         function double_click_action(){
939             GraphEdge.double_click_callback(self);
940         }
941
942         edge.dblclick(double_click_action);
943         edge_label.dblclick(double_click_action);
944
945
946         this.label_enable  = label_enable;
947         this.label_disable = label_disable;
948         this.update = update;
949         this.remove = remove;
950         this.is_loop = function(){ return start == end; };
951         this.get_start = function(){ return start; };
952         this.get_end   = function(){ return end; };
953     }
954
955     GraphEdge.double_click_callback = function(edge){
956         console.log("double click from edge:",edge);
957     };
958
959     // this is the default edge creation callback. It is called before an edge is created
960     // It returns an object containing the properties of the edge.
961     // If it returns null, the edge is not created.
962     GraphEdge.creation_callback = function(start,end){
963         var edge_prop = {};
964         edge_prop.label = 'new edge!';
965         return edge_prop;
966     };
967     // This is is called after a new edge is created, with the new edge
968     // as parameter
969     GraphEdge.new_edge_callback = function(new_edge){};
970
971     // this is the default edge destruction callback. It is called before 
972     // an edge is removed from the graph.
973     GraphEdge.destruction_callback = function(edge){ return true; };
974
975     
976
977     // returns a new string with the same content as str, but with lines of maximum 'width' characters.
978     // lines are broken on words, or into words if a word is longer than 'width'
979     function wordwrap( str, width) {
980         // http://james.padolsey.com/javascript/wordwrap-for-javascript/
981         width = width || 32;
982         var cut = true;
983         var brk = '\n';
984         if (!str) { return str; }
985         var regex = '.{1,' +width+ '}(\\s|$)' + (cut ? '|.{' +width+ '}|.+$' : '|\\S+?(\\s|$)');
986         return str.match(new RegExp(regex, 'g') ).join( brk );
987     }
988
989     window.CuteGraph   = Graph;
990     window.CuteNode    = GraphNode;
991     window.CuteEdge    = GraphEdge;
992
993     window.CuteGraph.wordwrap = wordwrap;
994
995
996 })(window);
997