~therp-nl/openerp-web/7.0-lp1013636-x2m_honour_required_attribute

« back to all changes in this revision

Viewing changes to addons/web_diagram/static/src/js/graph.js

[MERGE] from trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
                $.when(GraphNode.destruction_callback(entity)).then(function () {
 
89
                    //console.log("remove node",entity);
 
90
                    entity.remove();
 
91
                });
 
92
            }else if(entity_type == "edge"){
 
93
                $.when(GraphEdge.destruction_callback(entity)).then(function () {
 
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
            return get_label_pos(edge);
 
750
            /*  
 
751
            var bbox = edge_label.getBBox(); Does not work... :(
 
752
            return new Vec2(bbox.x + bbox.width, bbox.y);*/
 
753
        }
 
754
 
 
755
        //Straight line from s to e
 
756
        function make_line(){
 
757
            return "M" + s.x + "," + s.y + "L" + e.x + "," + e.y ;
 
758
        }
 
759
        //Curved line from s to e by mc
 
760
        function make_curve(){
 
761
            return "M" + s.x + "," + s.y + "Q" + mc.x + "," + mc.y + " " + e.x + "," + e.y;
 
762
        }
 
763
        //Curved line from s to e by mc1 mc2
 
764
        function make_loop(){
 
765
            return "M" + s.x + " " + s.y + 
 
766
                   "C" + mc1.x + " " + mc1.y + " " + mc2.x + " " + mc2.y + " " + e.x + " " + e.y;
 
767
        }
 
768
            
 
769
        //computes new start and end line coordinates
 
770
        function update_curve(){
 
771
            if(start != end){
 
772
                if(!tmp){
 
773
                    curvature = graph.get_edge_curvature(start,end,self);
 
774
                }else{
 
775
                    curvature = 0;
 
776
                }
 
777
                s = start.get_pos();
 
778
                e = end.get_pos();
 
779
                
 
780
                mc = s.lerp(e,0.5); //middle of the line s->e
 
781
                var se = e.sub(s);
 
782
                se = se.normalize();
 
783
                se = se.rotate_deg(-90);
 
784
                se = se.scale(curvature * graph.style.edge_spacing);
 
785
                mc = mc.add(se);
 
786
 
 
787
                if(start.get_bound){
 
788
                    var col = start.get_bound().collide_segment(s,mc);
 
789
                    if(col.length > 0){
 
790
                        s = col[0];
 
791
                    }
 
792
                }
 
793
                if(end.get_bound){
 
794
                    var col = end.get_bound().collide_segment(mc,e);
 
795
                    if(col.length > 0){
 
796
                        e = col[0];
 
797
                    }
 
798
                }
 
799
                
 
800
                if(curvature != 0){
 
801
                    edge_path = make_curve();
 
802
                }else{
 
803
                    edge_path = make_line();
 
804
                }
 
805
            }else{ // start == end
 
806
                var rad = graph.style.edge_loop_radius || 100;
 
807
                s = start.get_pos();
 
808
                e = end.get_pos();
 
809
 
 
810
                var r = Vec2.new_polar_deg(rad,graph.get_loop_angle(start,self));
 
811
                mc = s.add(r);
 
812
                var p = r.rotate_deg(90);
 
813
                mc1 = mc.add(p.set_len(rad*0.5));
 
814
                mc2 = mc.add(p.set_len(-rad*0.5));
 
815
                
 
816
                if(start.get_bound){
 
817
                    var col = start.get_bound().collide_segment(s,mc1);
 
818
                    if(col.length > 0){
 
819
                        s = col[0];
 
820
                    }
 
821
                    var col = start.get_bound().collide_segment(e,mc2);
 
822
                    if(col.length > 0){
 
823
                        e = col[0];
 
824
                    }
 
825
                }
 
826
                edge_path = make_loop();
 
827
            }
 
828
        }
 
829
        
 
830
        update_curve();
 
831
        var edge = r.path(edge_path).attr({ 'stroke':       graph.style.edge_color, 
 
832
                                            'stroke-width': graph.style.edge_width, 
 
833
                                            'arrow-end':    'block-wide-long', 
 
834
                                            'cursor':'pointer'  }).insertBefore(graph.get_node_list()[0].get_fig());       
 
835
        var labelpos = get_label_pos(edge);
 
836
        var edge_label = r.text(labelpos.x, labelpos.y - elfs, label).attr({
 
837
            'fill':         graph.style.edge_label_color, 
 
838
            'cursor':       'pointer', 
 
839
            'font-size':    elfs    });
 
840
 
 
841
        edge.transform(graph.get_transform());
 
842
        graph.set_scrolling(edge);
 
843
 
 
844
        edge_label.transform(graph.get_transform());
 
845
        graph.set_scrolling(edge_label);
 
846
        
 
847
 
 
848
        //since we create an edge we need to recompute the edges that have the same start and end positions as this one
 
849
        if(!tmp){
 
850
            var edges_start = graph.get_linked_edge_list(start);
 
851
            var edges_end   = graph.get_linked_edge_list(end);
 
852
            var edges = edges_start.length < edges_end.length ? edges_start : edges_end;
 
853
            for(var i = 0; i < edges.length; i ++){
 
854
                if(edges[i] != self){
 
855
                    edges[i].update();
 
856
                }
 
857
            }
 
858
        }
 
859
        function label_enable(){
 
860
            if(!label_enabled){
 
861
                label_enabled = true;
 
862
                edge_label.animate({'opacity':1},100,'linear');
 
863
                if(self.close_button){
 
864
                    self.close_button.show();
 
865
                }
 
866
                self.update();
 
867
            }
 
868
        }
 
869
        function label_disable(){
 
870
            if(label_enabled){
 
871
                label_enabled = false;
 
872
                edge_label.animate({'opacity':0},100,'linear');
 
873
                if(self.close_button){
 
874
                    self.close_button.hide();
 
875
                }
 
876
            }
 
877
        }
 
878
        //update the positions 
 
879
        function update(){
 
880
            update_curve();
 
881
            edge.attr({'path':edge_path});
 
882
            if(label_enabled){
 
883
                var labelpos = get_label_pos(edge);
 
884
                edge_label.attr({'x':labelpos.x, 'y':labelpos.y - 14});
 
885
            }
 
886
        }
 
887
        // removes the edge from the scene, disconnects it from linked        
 
888
        // nodes, destroy its drawable elements.
 
889
        function remove(){
 
890
            edge.remove();
 
891
            edge_label.remove();
 
892
            if(!tmp){
 
893
                graph.remove_edge(self);
 
894
            }
 
895
            if(start.update_linked_edges){
 
896
                start.update_linked_edges();
 
897
            }
 
898
            if(start != end && end.update_linked_edges){
 
899
                end.update_linked_edges();
 
900
            }
 
901
            if(self.close_button){
 
902
                self.close_button.remove();
 
903
            }
 
904
        }
 
905
 
 
906
        this.set_selected = function(){
 
907
            if(!selected){
 
908
                selected = true;
 
909
                edge.attr({ 'stroke': graph.style.node_selected_color, 
 
910
                            'stroke-width': graph.style.node_selected_width });
 
911
                edge_label.attr({ 'fill': graph.style.node_selected_color });
 
912
                if(!self.close_button){
 
913
                    self.close_button = new CloseButton(graph,self,"edge",0,30);
 
914
                    self.close_button.show();
 
915
                }
 
916
            }
 
917
        };
 
918
 
 
919
        this.set_not_selected = function(){
 
920
            if(selected){
 
921
                selected = false;
 
922
                edge.animate({  'stroke':       graph.style.edge_color,
 
923
                                'stroke-width': graph.style.edge_width }, 100,'linear');
 
924
                edge_label.animate({ 'fill':    graph.style.edge_label_color}, 100, 'linear');
 
925
                if(self.close_button){
 
926
                    self.close_button.remove();
 
927
                    self.close_button = null;
 
928
                }
 
929
            }
 
930
        };
 
931
        function click_action(){
 
932
            graph.select(self);
 
933
        }
 
934
        edge.click(click_action);
 
935
        edge_label.click(click_action);
 
936
 
 
937
        function double_click_action(){
 
938
            GraphEdge.double_click_callback(self);
 
939
        }
 
940
 
 
941
        edge.dblclick(double_click_action);
 
942
        edge_label.dblclick(double_click_action);
 
943
 
 
944
 
 
945
        this.label_enable  = label_enable;
 
946
        this.label_disable = label_disable;
 
947
        this.update = update;
 
948
        this.remove = remove;
 
949
        this.is_loop = function(){ return start == end; };
 
950
        this.get_start = function(){ return start; };
 
951
        this.get_end   = function(){ return end; };
 
952
    }
 
953
 
 
954
    GraphEdge.double_click_callback = function(edge){
 
955
        console.log("double click from edge:",edge);
 
956
    };
 
957
 
 
958
    // this is the default edge creation callback. It is called before an edge is created
 
959
    // It returns an object containing the properties of the edge.
 
960
    // If it returns null, the edge is not created.
 
961
    GraphEdge.creation_callback = function(start,end){
 
962
        var edge_prop = {};
 
963
        edge_prop.label = 'new edge!';
 
964
        return edge_prop;
 
965
    };
 
966
    // This is is called after a new edge is created, with the new edge
 
967
    // as parameter
 
968
    GraphEdge.new_edge_callback = function(new_edge){};
 
969
 
 
970
    // this is the default edge destruction callback. It is called before 
 
971
    // an edge is removed from the graph.
 
972
    GraphEdge.destruction_callback = function(edge){ return true; };
 
973
 
 
974
    
 
975
 
 
976
    // returns a new string with the same content as str, but with lines of maximum 'width' characters.
 
977
    // lines are broken on words, or into words if a word is longer than 'width'
 
978
    function wordwrap( str, width) {
 
979
        // http://james.padolsey.com/javascript/wordwrap-for-javascript/
 
980
        width = width || 32;
 
981
        var cut = true;
 
982
        var brk = '\n';
 
983
        if (!str) { return str; }
 
984
        var regex = '.{1,' +width+ '}(\\s|$)' + (cut ? '|.{' +width+ '}|.+$' : '|\\S+?(\\s|$)');
 
985
        return str.match(new RegExp(regex, 'g') ).join( brk );
 
986
    }
 
987
 
 
988
    window.CuteGraph   = Graph;
 
989
    window.CuteNode    = GraphNode;
 
990
    window.CuteEdge    = GraphEdge;
 
991
 
 
992
    window.CuteGraph.wordwrap = wordwrap;
 
993
 
 
994
 
 
995
})(window);
 
996