341c1056aefa32098ece9c1ab8cdcab1d26a38d0
[scpubgit/stemmaweb.git] / root / js / relationship.js
1 var MARGIN=30;
2 var svg_root = null;
3 var svg_root_element = null;
4 var start_element_height = 0;
5 var reltypes = {};
6 var readingdata = {};
7
8 jQuery.removeFromArray = function(value, arr) {
9     return jQuery.grep(arr, function(elem, index) {
10         return elem !== value;
11     });
12 };
13
14 function arrayUnique(array) {
15     var a = array.concat();
16     for(var i=0; i<a.length; ++i) {
17         for(var j=i+1; j<a.length; ++j) {
18             if(a[i] === a[j])
19                 a.splice(j--, 1);
20         }
21     }
22     return a;
23 };
24
25 function getTextURL( which ) {
26         return basepath + textid + '/' + which;
27 }
28
29 function getReadingURL( reading_id ) {
30         return basepath + textid + '/reading/' + reading_id;
31 }
32
33 // Make an XML ID into a valid selector
34 function jq(myid) { 
35         return '#' + myid.replace(/(:|\.)/g,'\\$1');
36 }
37
38 // Actions for opening the reading panel
39 function node_dblclick_listener( evt ) {
40         // Open the reading dialogue for the given node.
41         // First get the reading info
42         var reading_id = $(this).attr('id');
43         var reading_info = readingdata[reading_id];
44         // and then populate the dialog box with it.
45         // Set the easy properties first
46         $('#reading-form').dialog( 'option', 'title', 'Reading information for "' + reading_info['text'] + '"' );
47         $('#reading_id').val( reading_id );
48         toggle_checkbox( $('#reading_is_nonsense'), reading_info['is_nonsense'] );
49         toggle_checkbox( $('#reading_grammar_invalid'), reading_info['grammar_invalid'] );
50         // Use .text as a backup for .normal_form
51         var normal_form = reading_info['normal_form'];
52         if( !normal_form ) {
53                 normal_form = reading_info['text'];
54         }
55         var nfboxsize = 10;
56         if( normal_form.length > 9 ) {
57                 nfboxsize = normal_form.length + 1;
58         }
59         $('#reading_normal_form').attr( 'size', nfboxsize )
60         $('#reading_normal_form').val( normal_form );
61         if( editable ) {
62                 // Fill in the witnesses for the de-collation box.
63                 $('#reading_decollate_witnesses').empty();
64                 $.each( reading_info['witnesses'], function( idx, wit ) {
65                         $('#reading_decollate_witnesses').append( $('<option/>').attr(
66                                 'value', wit ).text( wit ) );
67                 });
68         }
69         // Now do the morphological properties.
70         morphology_form( reading_info['lexemes'] );
71         // and then open the dialog.
72         $('#reading-form').dialog("open");
73 }
74
75 function toggle_checkbox( box, value ) {
76         if( value == null ) {
77                 value = false;
78         }
79         box.attr('checked', value );
80 }
81
82 function morphology_form ( lexlist ) {
83         if( lexlist.length ) {
84                 $('#morph_outer').show();
85                 $('#morphology').empty();
86                 $.each( lexlist, function( idx, lex ) {
87                         var morphoptions = [];
88                         if( 'wordform_matchlist' in lex ) {
89                                 $.each( lex['wordform_matchlist'], function( tdx, tag ) {
90                                         var tagstr = stringify_wordform( tag );
91                                         morphoptions.push( tagstr );
92                                 });
93                         }
94                         var formtag = 'morphology_' + idx;
95                         var formstr = '';
96                         if( 'form' in lex ) {
97                                 formstr = stringify_wordform( lex['form'] );
98                         } 
99                         var form_morph_elements = morph_elements( 
100                                 formtag, lex['string'], formstr, morphoptions );
101                         $.each( form_morph_elements, function( idx, el ) {
102                                 $('#morphology').append( el );
103                         });
104                 });
105         } else {
106                 $('#morph_outer').hide();
107         }
108 }
109
110 function stringify_wordform ( tag ) {
111         if( tag ) {
112                 var elements = tag.split(' // ');
113                 return elements[1] + ' // ' + elements[2];
114         }
115         return ''
116 }
117
118 function morph_elements ( formtag, formtxt, currform, morphoptions ) {
119         var clicktag = '(Click to select)';
120         if ( !currform ) {
121                 currform = clicktag;
122         }
123         var formlabel = $('<label/>').attr( 'id', 'label_' + formtag ).attr( 
124                 'for', 'reading_' + formtag ).text( formtxt + ': ' );
125         var forminput = $('<input/>').attr( 'id', 'reading_' + formtag ).attr( 
126                 'name', 'reading_' + formtag ).attr( 'size', '50' ).attr(
127                 'class', 'reading_morphology' ).val( currform );
128         forminput.autocomplete({ source: morphoptions, minLength: 0     });
129         forminput.focus( function() { 
130                 if( $(this).val() == clicktag ) {
131                         $(this).val('');
132                 }
133                 $(this).autocomplete('search', '') 
134         });
135         var morphel = [ formlabel, forminput, $('<br/>') ];
136         return morphel;
137 }
138
139 function color_inactive ( el ) {
140         var reading_id = $(el).parent().attr('id');
141         var reading_info = readingdata[reading_id];
142         // If the reading info has any non-disambiguated lexemes, color it yellow;
143         // otherwise color it green.
144         $(el).attr( {stroke:'green', fill:'#b3f36d'} );
145         if( reading_info ) {
146                 $.each( reading_info['lexemes'], function ( idx, lex ) {
147                         if( !lex['is_disambiguated'] || lex['is_disambiguated'] == 0 ) {
148                                 $(el).attr( {stroke:'orange', fill:'#fee233'} );
149                         }
150                 });
151         }
152 }
153
154 function relemmatize () {
155         // Send the reading for a new lemmatization and reopen the form.
156         $('#relemmatize_pending').show();
157         var reading_id = $('#reading_id').val()
158         ncpath = getReadingURL( reading_id );
159         form_values = { 
160                 'normal_form': $('#reading_normal_form').val(), 
161                 'relemmatize': 1 };
162         var jqjson = $.post( ncpath, form_values, function( data ) {
163                 // Update the form with the return
164                 if( 'id' in data ) {
165                         // We got back a good answer. Stash it
166                         readingdata[reading_id] = data;
167                         // and regenerate the morphology form.
168                         morphology_form( data['lexemes'] );
169                 } else {
170                         alert("Could not relemmatize as requested: " + data['error']);
171                 }
172                 $('#relemmatize_pending').hide();
173         });
174 }
175
176 // Initialize the SVG once it exists
177 function svgEnlargementLoaded() {
178         //Give some visual evidence that we are working
179         $('#loading_overlay').show();
180         lo_height = $("#enlargement_container").outerHeight();
181         lo_width = $("#enlargement_container").outerWidth();
182         $("#loading_overlay").height( lo_height );
183         $("#loading_overlay").width( lo_width );
184         $("#loading_overlay").offset( $("#enlargement_container").offset() );
185         $("#loading_message").offset(
186                 { 'top': lo_height / 2 - $("#loading_message").height() / 2,
187                   'left': lo_width / 2 - $("#loading_message").width() / 2 });
188     if( editable ) {
189         // Show the update toggle button.
190             $('#update_workspace_button').data('locked', false);
191         $('#update_workspace_button').css('background-position', '0px 44px');
192     }
193     $('#svgenlargement ellipse').parent().dblclick( node_dblclick_listener );
194     var graph_svg = $('#svgenlargement svg');
195     var svg_g = $('#svgenlargement svg g')[0];
196     if (!svg_g) return;
197     svg_root = graph_svg.svg().svg('get').root();
198
199     // Find the real root and ignore any text nodes
200     for (i = 0; i < svg_root.childNodes.length; ++i) {
201         if (svg_root.childNodes[i].nodeName != '#text') {
202                 svg_root_element = svg_root.childNodes[i];
203                 break;
204            }
205     }
206
207     //Set viewbox width and height to width and height of $('#svgenlargement svg').
208     //This is essential to make sure zooming and panning works properly.
209     svg_root.viewBox.baseVal.width = graph_svg.attr( 'width' );
210     svg_root.viewBox.baseVal.height = graph_svg.attr( 'height' );
211     //Now set scale and translate so svg height is about 150px and vertically centered in viewbox.
212     //This is just to create a nice starting enlargement.
213     var initial_svg_height = 250;
214     var scale = initial_svg_height/graph_svg.attr( 'height' );
215     var additional_translate = (graph_svg.attr( 'height' ) - initial_svg_height)/(2*scale);
216     var transform = svg_g.getAttribute('transform');
217     var translate = parseFloat( transform.match( /translate\([^\)]*\)/ )[0].split('(')[1].split(' ')[1].split(')')[0] );
218     translate += additional_translate;
219     var transform = 'rotate(0) scale(' + scale + ') translate(4 ' + translate + ')';
220     svg_g.setAttribute('transform', transform);
221     //used to calculate min and max zoom level:
222     start_element_height = $('#__START__').children('ellipse')[0].getBBox().height;
223     //some use of call backs to ensure succesive execution
224     add_relations( function() { 
225         var rdgpath = getTextURL( 'readings' );
226         $.getJSON( rdgpath, function( data ) {
227             readingdata = data;
228             $('#svgenlargement ellipse').each( function( i, el ) { color_inactive( el ) });
229             $('#loading_overlay').hide(); 
230         });
231     });
232     
233     //initialize marquee
234     marquee = new Marquee();
235     
236 }
237
238 function add_relations( callback_fn ) {
239         // Add the relationship types to the keymap list
240         $.each( relationship_types, function(index, typedef) {   
241                  li_elm = $('<li class="key">').css( "border-color", 
242                         relation_manager.relation_colors[index] ).text(typedef.name);
243                  li_elm.append( $('<div>').attr('class', 'key_tip_container').append(
244                         $('<div>').attr('class', 'key_tip').text(typedef.description) ) );
245                  $('#keymaplist').append( li_elm ); 
246         });
247         // Now fetch the relationships themselves and add them to the graph
248         var rel_types = $.map( relationship_types, function(t) { return t.name });
249         // Save this list of names to the outer element data so that the relationship
250         // factory can access it
251         $('#keymap').data('relations', rel_types);
252         var textrelpath = getTextURL( 'relationships' );
253         $.getJSON( textrelpath, function(data) {
254                 $.each(data, function( index, rel_info ) {
255                         var type_index = $.inArray(rel_info.type, rel_types);
256                         var source_found = get_ellipse( rel_info.source_id );
257                         var target_found = get_ellipse( rel_info.target_id );
258                         if( type_index != -1 && source_found.size() && target_found.size() ) {
259                                 var relation = relation_manager.create( rel_info.source_id, rel_info.target_id, type_index );
260                                 // Save the relationship data too.
261                                 $.each( rel_info, function( k, v ) {
262                                         relation.data( k, v );
263                                 });
264                                 if( editable ) {
265                                         var node_obj = get_node_obj(rel_info.source_id);
266                                         node_obj.set_selectable( false );
267                                         node_obj.ellipse.data( 'node_obj', null );
268                                         node_obj = get_node_obj(rel_info.target_id);
269                                         node_obj.set_selectable( false );
270                                         node_obj.ellipse.data( 'node_obj', null );
271                                 }
272                         }
273                 });
274                 callback_fn.call();
275         });
276 }
277
278 function get_ellipse( node_id ) {
279         return $( jq( node_id ) + ' ellipse');
280 }
281
282 function get_node_obj( node_id ) {
283     var node_ellipse = get_ellipse( node_id );
284     if( node_ellipse.data( 'node_obj' ) == null ) {
285         node_ellipse.data( 'node_obj', new node_obj(node_ellipse) );
286     };
287     return node_ellipse.data( 'node_obj' );
288 }
289
290 function node_obj(ellipse) {
291   this.ellipse = ellipse;
292   var self = this;
293   
294   this.x = 0;
295   this.y = 0;
296   this.dx = 0;
297   this.dy = 0;
298   this.node_elements = node_elements_for(self.ellipse);
299   
300   this.get_id = function() {
301     return $(self.ellipse).parent().attr('id')
302   }
303   
304   this.set_selectable = function( clickable ) {
305       if( clickable && editable ) {
306           $(self.ellipse).attr( {stroke:'black', fill:'#fff'} );
307           $(self.ellipse).parent().hover( this.enter_node, this.leave_node );
308           $(self.ellipse).parent().mousedown( function(evt) { evt.stopPropagation() } ); 
309           $(self.ellipse).parent().click( function(evt) { 
310               evt.stopPropagation();              
311               if( $('ellipse[fill="#9999ff"]').size() > 0 ) {
312                 $('ellipse[fill="#9999ff"]').each( function() { 
313                     $(this).data( 'node_obj' ).set_draggable( false );
314                 } );
315               }
316               self.set_draggable( true ) 
317           });
318       } else {
319           $(self.ellipse).attr( {stroke:'black', fill:'#fff'} );
320           self.ellipse.siblings('text').attr('class', '');
321           $(self.ellipse).parent().unbind(); 
322           $('body').unbind('mousemove');
323           $('body').unbind('mouseup');
324       }
325   }
326   
327   this.set_draggable = function( draggable ) {
328     if( draggable && editable ) {
329       $(self.ellipse).attr( {stroke:'black', fill:'#9999ff'} );
330       $(self.ellipse).parent().mousedown( this.mousedown_listener );
331       $(self.ellipse).parent().unbind( 'mouseenter' ).unbind( 'mouseleave' );
332       self.ellipse.siblings('text').attr('class', 'noselect draggable');
333     } else {
334       $(self.ellipse).attr( {stroke:'black', fill:'#fff'} );
335       self.ellipse.siblings('text').attr('class', '');
336       $(self.ellipse).parent().unbind( 'mousedown ');
337       $(self.ellipse).parent().mousedown( function(evt) { evt.stopPropagation() } ); 
338       $(self.ellipse).parent().hover( this.enter_node, this.leave_node );
339     }
340   }
341
342   this.mousedown_listener = function(evt) {
343     evt.stopPropagation();
344     self.x = evt.clientX;
345     self.y = evt.clientY;
346     $('body').mousemove( self.mousemove_listener );
347     $('body').mouseup( self.mouseup_listener );
348     $(self.ellipse).parent().unbind('mouseenter').unbind('mouseleave')
349     self.ellipse.attr( 'fill', '#6b6bb2' );
350     first_node_g_element = $("#svgenlargement g .node" ).filter( ":first" );
351     if( first_node_g_element.attr('id') !== self.get_g().attr('id') ) { self.get_g().insertBefore( first_node_g_element ) };
352   }
353
354   this.mousemove_listener = function(evt) {
355     self.dx = (evt.clientX - self.x) / mouse_scale;
356     self.dy = (evt.clientY - self.y) / mouse_scale;
357     self.move_elements();
358     evt.returnValue = false;
359     evt.preventDefault();
360     return false;
361   }
362
363   this.mouseup_listener = function(evt) {    
364     if( $('ellipse[fill="#ffccff"]').size() > 0 ) {
365         var source_node_id = $(self.ellipse).parent().attr('id');
366         var source_node_text = self.ellipse.siblings('text').text();
367         var target_node_id = $('ellipse[fill="#ffccff"]').parent().attr('id');
368         var target_node_text = $('ellipse[fill="#ffccff"]').siblings("text").text();
369         $('#source_node_id').val( source_node_id );
370         $('#source_node_text').val( source_node_text );
371         $('.rel_rdg_a').text( "'" + source_node_text + "'" );
372         $('#target_node_id').val( target_node_id );
373         $('#target_node_text').val( target_node_text );
374         $('.rel_rdg_b').text( "'" + target_node_text + "'" );
375         $('#dialog-form').dialog( 'open' );
376     };
377     $('body').unbind('mousemove');
378     $('body').unbind('mouseup');
379     self.ellipse.attr( 'fill', '#9999ff' );
380     self.reset_elements();
381   }
382   
383   this.cpos = function() {
384     return { x: self.ellipse.attr('cx'), y: self.ellipse.attr('cy') };
385   }
386
387   this.get_g = function() {
388     return self.ellipse.parent('g');
389   }
390
391   this.enter_node = function(evt) {
392     self.ellipse.attr( 'fill', '#ffccff' );
393   }
394
395   this.leave_node = function(evt) {
396     self.ellipse.attr( 'fill', '#fff' );
397   }
398
399   this.greyout_edges = function() {
400       $.each( self.node_elements, function(index, value) {
401         value.grey_out('.edge');
402       });
403   }
404
405   this.ungreyout_edges = function() {
406       $.each( self.node_elements, function(index, value) {
407         value.un_grey_out('.edge');
408       });
409   }
410
411   this.reposition = function( dx, dy ) {
412     $.each( self.node_elements, function(index, value) {
413       value.reposition( dx, dy );
414     } );    
415   }
416
417   this.move_elements = function() {
418     $.each( self.node_elements, function(index, value) {
419       value.move( self.dx, self.dy );
420     } );
421   }
422
423   this.reset_elements = function() {
424     $.each( self.node_elements, function(index, value) {
425       value.reset();
426     } );
427   }
428
429   this.update_elements = function() {
430       self.node_elements = node_elements_for(self.ellipse);
431   }
432
433   this.get_witnesses = function() {
434       return readingdata[self.get_id()].witnesses
435   }
436
437   self.set_selectable( true );
438 }
439
440 function svgshape( shape_element ) {
441   this.shape = shape_element;
442   this.reposx = 0;
443   this.reposy = 0; 
444   this.repositioned = this.shape.parent().data( 'repositioned' );
445   if( this.repositioned != null ) {
446       this.reposx = this.repositioned[0];
447       this.reposy = this.repositioned[1]; 
448   }
449   this.reposition = function (dx, dy) {
450       this.move( dx, dy );
451       this.reposx = this.reposx + dx;
452       this.reposy = this.reposy + dy;
453       this.shape.parent().data( 'repositioned', [this.reposx,this.reposy] );
454   }
455   this.move = function(dx,dy) {
456       this.shape.attr( "transform", "translate( " + (this.reposx + dx) + " " + (this.reposy + dy) + " )" );
457   }
458   this.reset = function() {
459     this.shape.attr( "transform", "translate( " + this.reposx + " " + this.reposy + " )" );
460   }
461   this.grey_out = function(filter) {
462       if( this.shape.parent(filter).size() != 0 ) {
463           this.shape.attr({'stroke':'#e5e5e5', 'fill':'#e5e5e5'});
464       }
465   }
466   this.un_grey_out = function(filter) {
467       if( this.shape.parent(filter).size() != 0 ) {
468         this.shape.attr({'stroke':'#000000', 'fill':'#000000'});
469       }
470   }
471 }
472
473 function svgpath( path_element, svg_element ) {
474   this.svg_element = svg_element;
475   this.path = path_element;
476   this.x = this.path.x;
477   this.y = this.path.y;
478
479   this.reposition = function (dx, dy) {
480       this.x = this.x + dx;
481       this.y = this.y + dy;      
482       this.path.x = this.x;
483       this.path.y = this.y;
484   }
485
486   this.move = function(dx,dy) {
487     this.path.x = this.x + dx;
488     this.path.y = this.y + dy;
489   }
490   
491   this.reset = function() {
492     this.path.x = this.x;
493     this.path.y = this.y;
494   }
495   
496   this.grey_out = function(filter) {
497       if( this.svg_element.parent(filter).size() != 0 ) {
498           this.svg_element.attr('stroke', '#e5e5e5');
499           this.svg_element.siblings('text').attr('fill', '#e5e5e5');
500           this.svg_element.siblings('text').attr('class', 'noselect');
501       }
502   }
503   this.un_grey_out = function(filter) {
504       if( this.svg_element.parent(filter).size() != 0 ) {
505           this.svg_element.attr('stroke', '#000000');
506           this.svg_element.siblings('text').attr('fill', '#000000');
507           this.svg_element.siblings('text').attr('class', '');
508       }
509   }
510 }
511
512 function node_elements_for( ellipse ) {
513   node_elements = get_edge_elements_for( ellipse );
514   node_elements.push( new svgshape( ellipse.siblings('text') ) );
515   node_elements.push( new svgshape( ellipse ) );
516   return node_elements;
517 }
518
519 function get_edge_elements_for( ellipse ) {
520   edge_elements = new Array();
521   node_id = ellipse.parent().attr('id');
522   edge_in_pattern = new RegExp( node_id + '$' );
523   edge_out_pattern = new RegExp( '^' + node_id + '-' );
524   $.each( $('#svgenlargement .edge,#svgenlargement .relation').children('title'), function(index) {
525     title = $(this).text();
526     if( edge_in_pattern.test(title) ) {
527         polygon = $(this).siblings('polygon');
528         if( polygon.size() > 0 ) {
529             edge_elements.push( new svgshape( polygon ) );
530         }
531         path_segments = $(this).siblings('path')[0].pathSegList;
532         edge_elements.push( new svgpath( path_segments.getItem(path_segments.numberOfItems - 1), $(this).siblings('path') ) );
533     }
534     if( edge_out_pattern.test(title) ) {
535       path_segments = $(this).siblings('path')[0].pathSegList;
536       edge_elements.push( new svgpath( path_segments.getItem(0), $(this).siblings('path') ) );
537     }
538   });
539   return edge_elements;
540
541
542 function relation_factory() {
543     var self = this;
544     this.color_memo = null;
545     //TODO: colors hard coded for now
546     this.temp_color = '#FFA14F';
547     this.relation_colors = [ "#5CCCCC", "#67E667", "#F9FE72", "#6B90D4", "#FF7673", "#E467B3", "#AA67D5", "#8370D8", "#FFC173" ];
548
549     this.create_temporary = function( source_node_id, target_node_id ) {
550         var relation_id = get_relation_id( source_node_id, target_node_id );
551         var relation = $( jq( relation_id ) );
552         if( relation.size() == 0 ) { 
553             draw_relation( source_node_id, target_node_id, self.temp_color );
554         } else {
555             self.color_memo = relation.children('path').attr( 'stroke' );
556             relation.children('path').attr( 'stroke', self.temp_color );
557         }
558     }
559     this.remove_temporary = function() {
560         var path_element = $('#svgenlargement .relation').children('path[stroke="' + self.temp_color + '"]');
561         if( self.color_memo != null ) {
562             path_element.attr( 'stroke', self.color_memo );
563             self.color_memo = null;
564         } else {
565             var temporary = path_element.parent('g').remove();
566             temporary.empty();
567             temporary = null; 
568         }
569     }
570     this.create = function( source_node_id, target_node_id, color_index ) {
571         //TODO: Protect from (color_)index out of bound..
572         var relation_color = self.relation_colors[ color_index ];
573         var relation = draw_relation( source_node_id, target_node_id, relation_color );
574         get_node_obj( source_node_id ).update_elements();
575         get_node_obj( target_node_id ).update_elements();
576         return relation;
577     }
578     this.toggle_active = function( relation_id ) {
579         var relation = $( jq( relation_id ) );
580         var relation_path = relation.children('path');
581         if( !relation.data( 'active' ) ) {
582             relation_path.css( {'cursor':'pointer'} );
583             relation_path.mouseenter( function(event) { 
584                 outerTimer = setTimeout( function() { 
585                     timer = setTimeout( function() { 
586                         var related_nodes = get_related_nodes( relation_id );
587                         var source_node_id = related_nodes[0];
588                         var target_node_id = related_nodes[1];
589                         $('#delete_source_node_id').val( source_node_id );
590                         $('#delete_target_node_id').val( target_node_id );
591                         self.showinfo(relation); 
592                     }, 500 ) 
593                 }, 1000 );
594             });
595             relation_path.mouseleave( function(event) {
596                 clearTimeout(outerTimer); 
597                 if( timer != null ) { clearTimeout(timer); } 
598             });
599             relation.data( 'active', true );
600         } else {
601             relation_path.unbind( 'mouseenter' );
602             relation_path.unbind( 'mouseleave' );
603             relation_path.css( {'cursor':'inherit'} );
604             relation.data( 'active', false );
605         }
606     }
607     this.showinfo = function(relation) {
608         $('#delete_relation_type').text( relation.data('type') );
609         $('#delete_relation_scope').text( relation.data('scope') );
610         $('#delete_relation_attributes').empty();
611         if( relation.data( 'a_derivable_from_b' ) ) {
612                 $('#delete_relation_attributes').append( 
613                         "'" + relation.data('source_text') + "' derivable from '" + relation.data('target_text') + "'<br/>");
614         }
615         if( relation.data( 'b_derivable_from_a' ) ) {
616                 $('#delete_relation_attributes').append( 
617                         "'" + relation.data('target_text') + "' derivable from '" + relation.data('source_text') + "'<br/>");
618         }
619         if( relation.data( 'non_independent' ) ) {
620                 $('#delete_relation_attributes').append( 
621                         "Variance unlikely to arise coincidentally<br/>");
622         }
623         if( relation.data( 'note' ) ) {
624                 $('#delete_relation_note').text('note: ' + relation.data( 'note' ) );
625         }
626         var points = relation.children('path').attr('d').slice(1).replace('C',' ').split(' ');
627         var xs = parseFloat( points[0].split(',')[0] );
628         var xe = parseFloat( points[1].split(',')[0] );
629         var ys = parseFloat( points[0].split(',')[1] );
630         var ye = parseFloat( points[3].split(',')[1] );
631         var p = svg_root.createSVGPoint();
632         p.x = xs + ((xe-xs)*1.1);
633         p.y = ye - ((ye-ys)/2);
634         var ctm = svg_root_element.getScreenCTM();
635         var nx = p.matrixTransform(ctm).x;
636         var ny = p.matrixTransform(ctm).y;
637         var dialog_aria = $ ("div[aria-labelledby='ui-dialog-title-delete-form']");
638         $('#delete-form').dialog( 'open' );
639         dialog_aria.offset({ left: nx, top: ny });
640     }
641     this.remove = function( relation_id ) {
642         if( !editable ) {
643                 return;
644         }
645         var relation = $( jq( relation_id ) );
646         relation.remove();
647     }
648 }
649
650 // Utility function to create/return the ID of a relation link between
651 // a source and target.
652 function get_relation_id( source_id, target_id ) {
653         var idlist = [ source_id, target_id ];
654         idlist.sort();
655         return 'relation-' + idlist[0] + '-...-' + idlist[1];
656 }
657
658 function get_related_nodes( relation_id ) {
659         var srctotarg = relation_id.substr( 9 );
660         return srctotarg.split('-...-');
661 }
662
663 function draw_relation( source_id, target_id, relation_color ) {
664     var source_ellipse = get_ellipse( source_id );
665     var target_ellipse = get_ellipse( target_id );
666     var relation_id = get_relation_id( source_id, target_id );
667     var svg = $('#svgenlargement').children('svg').svg().svg('get');
668     var path = svg.createPath(); 
669     var sx = parseInt( source_ellipse.attr('cx') );
670     var rx = parseInt( source_ellipse.attr('rx') );
671     var sy = parseInt( source_ellipse.attr('cy') );
672     var ex = parseInt( target_ellipse.attr('cx') );
673     var ey = parseInt( target_ellipse.attr('cy') );
674     var relation = svg.group( $("#svgenlargement svg g"), { 'class':'relation', 'id':relation_id } );
675     svg.title( relation, source_id + '->' + target_id );
676     svg.path( relation, path.move( sx, sy ).curveC( sx + (2*rx), sy, ex + (2*rx), ey, ex, ey ), {fill: 'none', stroke: relation_color, strokeWidth: 4});
677     var relation_element = $('#svgenlargement .relation').filter( ':last' );
678     relation_element.insertBefore( $('#svgenlargement g g').filter(':first') );
679     return relation_element;
680 }
681
682 function detach_node( readings ) {
683     // separate out the deleted relationships, discard for now
684     if( 'DELETED' in readings ) {
685         // Remove each of the deleted relationship links.
686         $.each( readings['DELETED'], function( idx, pair ) {
687                 var relation_id = get_relation_id( pair[0], pair[1] );
688                         var relation = $( jq( relation_id ) );
689                         if( relation.size() == 0 ) { 
690                         relation_id = get_relation_id( pair[1], pair[0] );
691                 }
692                 relation_manager.remove( relation_id );
693         });
694         delete readings['DELETED'];
695     }
696     // add new node(s)
697     $.extend( readingdata, readings );
698     // remove from existing readings the witnesses for the new nodes/readings
699     $.each( readings, function( node_id, reading ) {
700         $.each( reading.witnesses, function( index, witness ) {
701             var witnesses = readingdata[ reading.orig_rdg ].witnesses;
702             readingdata[ reading.orig_rdg ].witnesses = $.removeFromArray( witness, witnesses );
703         } );
704     } );    
705     
706     detached_edges = [];
707     
708     // here we detach witnesses from the existing edges accoring to what's being relayed by readings
709     $.each( readings, function( node_id, reading ) {
710         var edges = edges_of( get_ellipse( reading.orig_rdg ) );
711         incoming_remaining = [];
712         outgoing_remaining = [];
713         $.each( reading.witnesses, function( index, witness ) {
714             incoming_remaining.push( witness );
715             outgoing_remaining.push( witness );
716         } );
717         $.each( edges, function( index, edge ) {
718             detached_edge = edge.detach_witnesses( reading.witnesses );
719             if( detached_edge != null ) {
720                 detached_edges.push( detached_edge );
721                 $.each( detached_edge.witnesses, function( index, witness ) {
722                     if( detached_edge.is_incoming == true ) {
723                         incoming_remaining = $.removeFromArray( witness, incoming_remaining );
724                     } else {
725                         outgoing_remaining = $.removeFromArray( witness, outgoing_remaining );
726                     }
727                 } );
728             }
729         } );
730         
731         // After detaching we still need to check if for *all* readings
732         // an edge was detached. It may be that a witness was not
733         // explicitly named on an edge but was part of a 'majority' edge
734         // in which case we need to duplicate and name that edge after those
735         // remaining witnesses.
736         if( outgoing_remaining.length > 0 ) {
737             $.each( edges, function( index, edge ) {
738                 if( edge.get_label() == 'majority' && !edge.is_incoming ) {
739                     detached_edges.push( edge.clone_for( outgoing_remaining ) );
740                 }
741             } );
742         }
743         if( incoming_remaining.length > 0 ) {
744             $.each( edges, function( index, edge ) {
745                 if( edge.get_label() == 'majority' && edge.is_incoming ) {
746                     detached_edges.push( edge.clone_for( incoming_remaining ) );
747                 }
748             } );
749         }
750         
751         // Finally multiple selected nodes may share edges
752         var copy_array = [];
753         $.each( detached_edges, function( index, edge ) {
754             var do_copy = true;
755             $.each( copy_array, function( index, copy_edge ) {
756                 if( copy_edge.g_elem.attr( 'id' ) == edge.g_elem.attr( 'id' ) ) { do_copy = false }
757             } );
758             if( do_copy == true ) {
759                 copy_array.push( edge );
760             }
761         } );
762         detached_edges = copy_array;
763         
764         // Lots of unabstracted knowledge down here :/
765         // Clone original node/reading, rename/id it..
766         duplicate_node = get_ellipse( reading.orig_rdg ).parent().clone();
767         duplicate_node.attr( 'id', node_id );
768         duplicate_node.children( 'title' ).text( node_id );
769         
770         // This needs somehow to move to node or even to shapes! #repositioned
771         duplicate_node_data = get_ellipse( reading.orig_rdg ).parent().data( 'repositioned' );
772         if( duplicate_node_data != null ) {
773             duplicate_node.children( 'ellipse' ).parent().data( 'repositioned', duplicate_node_data );
774         }
775         
776         // Add the node and all new edges into the graph
777         var graph_root = $('#svgenlargement svg g.graph');
778         graph_root.append( duplicate_node );
779         $.each( detached_edges, function( index, edge ) {
780             id_suffix = node_id.slice( node_id.indexOf( '_' ) );
781             edge.g_elem.attr( 'id', ( edge.g_elem.attr( 'id' ) + id_suffix ) );
782             edge_title = edge.g_elem.children( 'title' ).text();
783             edge_weight = 0.8 + ( 0.2 * edge.witnesses.length );
784             edge_title = edge_title.replace( reading.orig_rdg, node_id );
785             edge.g_elem.children( 'title' ).text( edge_title );
786             edge.g_elem.children( 'path').attr( 'stroke-width', edge_weight );
787             // Reg unabstracted knowledge: isn't it more elegant to make 
788             // it edge.append_to( graph_root )?
789             graph_root.append( edge.g_elem );
790         } );
791                 
792         // Make the detached node a real node_obj
793         var ellipse_elem = get_ellipse( node_id );
794         var new_node = new node_obj( ellipse_elem );
795         ellipse_elem.data( 'node_obj', new_node );
796
797         // Move the node somewhat up for 'dramatic effect' :-p
798         new_node.reposition( 0, -70 );        
799         
800     } );
801     
802 }
803
804 function merge_nodes( source_node_id, target_node_id, consequences ) {
805     if( consequences.status != null && consequences.status == 'ok' ) {
806         merge_node( source_node_id, target_node_id );
807         if( consequences.checkalign != null ) {
808             $.each( consequences.checkalign, function( index, node_ids ) {
809                 var temp_relation = draw_relation( node_ids[0], node_ids[1], "#89a02c" );
810                 var sy = parseInt( temp_relation.children('path').attr('d').split('C')[0].split(',')[1] );
811                 var ey = parseInt( temp_relation.children('path').attr('d').split(' ')[2].split(',')[1] );
812                 var yC = ey + (( sy - ey )/2); 
813                 // TODO: compute xC to be always the same distance to the amplitude of the curve
814                 var xC = parseInt( temp_relation.children('path').attr('d').split(' ')[1].split(',')[0] );
815                 var svg = $('#svgenlargement').children('svg').svg('get');
816                 parent_g = svg.group( $('#svgenlargement svg g') );
817                 var ids_text = node_ids[0] + '-' + node_ids[1]; 
818                 var merge_id = 'merge-' + ids_text;
819                 svg.image( parent_g, xC, (yC-8), 16, 16, merge_button_yes, { id: merge_id } );
820                 svg.image( parent_g, (xC+20), (yC-8), 16, 16, merge_button_no, { id: 'no' + merge_id } );
821                 $( '#' + merge_id ).hover( function(){ $(this).addClass( 'draggable' ) }, function(){ $(this).removeClass( 'draggable' ) } );
822                 $( '#no' + merge_id ).hover( function(){ $(this).addClass( 'draggable' ) }, function(){ $(this).removeClass( 'draggable' ) } );
823                 $( '#' + merge_id ).click( function( evt ){ 
824                     merge_node( node_ids[0], node_ids[1] );
825                     temp_relation.remove();
826                     $( '#' + merge_id ).parent().remove();
827                     //notify backend
828                     var ncpath = getTextURL( 'merge' );
829                     var form_values = "source_id=" + node_ids[0] + "&target_id=" + node_ids[1] + "&single=true";
830                     $.post( ncpath, form_values );
831                 } );
832                 $( '#no' + merge_id ).click( function( evt ) {
833                     temp_relation.remove();
834                     $( '#' + merge_id ).parent().remove();
835                 } );
836             } );
837         }
838     }
839 }
840
841 function merge_node( source_node_id, target_node_id ) {
842     $.each( edges_of( get_ellipse( source_node_id ) ), function( index, edge ) {
843         if( edge.is_incoming == true ) {
844             edge.attach_endpoint( target_node_id );
845         } else {
846             edge.attach_startpoint( target_node_id );
847         }
848     } );
849     $( jq( source_node_id ) ).remove();    
850 }
851
852 function Marquee() {
853     
854     var self = this;
855     
856     this.x = 0;
857     this.y = 0;
858     this.dx = 0;
859     this.dy = 0;
860     this.enlargementOffset = $('#svgenlargement').offset();
861     this.svg_rect = $('#svgenlargement svg').svg('get');
862
863     this.show = function( event ) {
864         self.x = event.clientX;
865         self.y = event.clientY;
866         p = svg_root.createSVGPoint();
867         p.x = event.clientX - self.enlargementOffset.left;
868         p.y = event.clientY - self.enlargementOffset.top;
869         self.svg_rect.rect( p.x, p.y, 0, 0, { fill: 'black', 'fill-opacity': '0.1', stroke: 'black', 'stroke-dasharray': '4,2', strokeWidth: '0.02em', id: 'marquee' } );
870     };
871
872     this.expand = function( event ) {
873         self.dx = (event.clientX - self.x);
874         self.dy = (event.clientY - self.y);
875         var rect = $('#marquee');
876         if( rect.length != 0 ) {            
877             var rect_w =  Math.abs( self.dx );
878             var rect_h =  Math.abs( self.dy );
879             var rect_x = self.x - self.enlargementOffset.left;
880             var rect_y = self.y - self.enlargementOffset.top;
881             if( self.dx < 0 ) { rect_x = rect_x - rect_w }
882             if( self.dy < 0 ) { rect_y = rect_y - rect_h }
883             rect.attr("x", rect_x).attr("y", rect_y).attr("width", rect_w).attr("height", rect_h);
884         }
885     };
886     
887     this.select = function() {
888         var rect = $('#marquee');
889         if( rect.length != 0 ) {
890             //unselect any possible selected first
891             //TODO: unless SHIFT?
892             if( $('ellipse[fill="#9999ff"]').size() > 0 ) {
893               $('ellipse[fill="#9999ff"]').each( function() { 
894                   $(this).data( 'node_obj' ).set_draggable( false );
895               } );
896             }
897             //compute dimension of marquee
898             var left = $('#marquee').offset().left;
899             var top = $('#marquee').offset().top;
900             var right = left + parseInt( $('#marquee').attr( 'width' ) );
901             var bottom = top + parseInt( $('#marquee').attr( 'height' ) );
902             var tf = svg_root_element.getScreenCTM().inverse(); 
903             var p = svg_root.createSVGPoint();
904             p.x=left;
905             p.y=top;
906             var cx_min = p.matrixTransform(tf).x;
907             var cy_min = p.matrixTransform(tf).y;
908             p.x=right;
909             p.y=bottom;
910             var cx_max = p.matrixTransform(tf).x;
911             var cy_max = p.matrixTransform(tf).y;
912             //select any node with its center inside the marquee
913             var readings = [];
914             //also merge witness sets from nodes
915             var witnesses = [];
916             $('#svgenlargement ellipse').each( function( index ) {
917                 var cx = parseInt( $(this).attr('cx') );
918                 var cy = parseInt( $(this).attr('cy') );
919                 
920                 // This needs somehow to move to node or even to shapes! #repositioned
921                 // We should ask something more aling the lines of: nodes.each { |item| node.selected? }
922                 var org_translate = $(this).parent().data( 'repositioned' );
923                 if( org_translate != null ) {
924                     cx = cx + org_translate[0];
925                     cy = cy + org_translate[1];
926                 }
927                 
928                 if( cx > cx_min && cx < cx_max) {
929                     if( cy > cy_min && cy < cy_max) {
930                         // we actually heve no real 'selected' state for nodes, except coloring
931                         $(this).attr( 'fill', '#9999ff' );
932                         // Take note of the selected reading(s) and applicable witness(es)
933                         // so we can populate the multipleselect-form 
934                         readings.push( $(this).parent().attr('id') ); 
935                         var this_witnesses = $(this).data( 'node_obj' ).get_witnesses();
936                         witnesses = arrayUnique( witnesses.concat( this_witnesses ) );
937                     }
938                 }
939             });
940             if( $('ellipse[fill="#9999ff"]').size() > 0 ) {
941                 //add intersection of witnesses sets to the multi select form and open it
942                 $('#detach_collated_form').empty();
943                 $.each( readings, function( index, value ) {
944                   $('#detach_collated_form').append( $('<input>').attr(
945                     "type", "hidden").attr("name", "readings[]").attr(
946                     "value", value ) );
947                 });
948                                 $.each( witnesses, function( index, value ) {
949                     $('#detach_collated_form').append( 
950                       '<input type="checkbox" name="witnesses[]" value="' + value 
951                       + '">' + value + '<br>' ); 
952                 });
953                 $('#multiple_selected_readings').attr('value', readings.join(',') ); 
954                 $('#multipleselect-form').dialog( 'open' );
955             }
956             self.svg_rect.remove( $('#marquee') );
957         }
958     };
959     
960     this.unselect = function() {
961         $('ellipse[fill="#9999ff"]').attr( 'fill', '#fff' );
962     }
963      
964 }
965
966 function readings_equivalent( source, target ) {
967         var sourcetext = readingdata[source].text;
968         var targettext = readingdata[target].text;
969         if( sourcetext === targettext ) {
970                 return true;
971         }
972         // Lowercase and strip punctuation from both and compare again
973         var stlc = sourcetext.toLowerCase().replace(/[^\w\s]|_/g, "");
974         var ttlc = targettext.toLowerCase().replace(/[^\w\s]|_/g, "");
975         if( stlc === ttlc ) {
976                 return true;
977         }       
978         return false;
979 }
980
981
982 $(document).ready(function () {
983     
984   timer = null;
985   relation_manager = new relation_factory();
986   
987   $('#update_workspace_button').data('locked', false);
988    
989   // Set up the mouse events on the SVG enlargement             
990   $('#enlargement').mousedown(function (event) {
991     $(this)
992         .data('down', true)
993         .data('x', event.clientX)
994         .data('y', event.clientY)
995         .data('scrollLeft', this.scrollLeft)
996     stateTf = svg_root_element.getCTM().inverse();
997     var p = svg_root.createSVGPoint();
998     p.x = event.clientX;
999     p.y = event.clientY;
1000     stateOrigin = p.matrixTransform(stateTf);
1001
1002     // Activate marquee if in interaction mode
1003     if( $('#update_workspace_button').data('locked') == true ) { marquee.show( event ) };
1004         
1005     event.returnValue = false;
1006     event.preventDefault();
1007     return false;
1008   }).mouseup(function (event) {
1009     marquee.select(); 
1010     $(this).data('down', false);
1011   }).mousemove(function (event) {
1012     if( timer != null ) { clearTimeout(timer); } 
1013     if ( ($(this).data('down') == true) && ($('#update_workspace_button').data('locked') == false) ) {
1014         var p = svg_root.createSVGPoint();
1015         p.x = event.clientX;
1016         p.y = event.clientY;
1017         p = p.matrixTransform(stateTf);
1018         var matrix = stateTf.inverse().translate(p.x - stateOrigin.x, p.y - stateOrigin.y);
1019         var s = "matrix(" + matrix.a + "," + matrix.b + "," + matrix.c + "," + matrix.d + "," + matrix.e + "," + matrix.f + ")";
1020         svg_root_element.setAttribute("transform", s);
1021     }
1022     marquee.expand( event ); 
1023     event.returnValue = false;
1024     event.preventDefault();
1025   }).mousewheel(function (event, delta) {
1026     event.returnValue = false;
1027     event.preventDefault();
1028     if ( $('#update_workspace_button').data('locked') == false ) {
1029         if (!delta || delta == null || delta == 0) delta = event.originalEvent.wheelDelta;
1030         if (!delta || delta == null || delta == 0) delta = -1 * event.originalEvent.detail;
1031         if( delta < -9 ) { delta = -9 }; 
1032         var z = 1 + delta/10;
1033         z = delta > 0 ? 1 : -1;
1034         var g = svg_root_element;
1035         if (g && ((z<1 && (g.getScreenCTM().a * start_element_height) > 4.0) || (z>=1 && (g.getScreenCTM().a * start_element_height) < 100))) {
1036             var root = svg_root;
1037             var p = root.createSVGPoint();
1038             p.x = event.originalEvent.clientX;
1039             p.y = event.originalEvent.clientY;
1040             p = p.matrixTransform(g.getCTM().inverse());
1041             var scaleLevel = 1+(z/20);
1042             var k = root.createSVGMatrix().translate(p.x, p.y).scale(scaleLevel).translate(-p.x, -p.y);
1043             var matrix = g.getCTM().multiply(k);
1044             var s = "matrix(" + matrix.a + "," + matrix.b + "," + matrix.c + "," + matrix.d + "," + matrix.e + "," + matrix.f + ")";
1045             g.setAttribute("transform", s);
1046         }
1047     }
1048   }).css({
1049     'overflow' : 'hidden',
1050     'cursor' : '-moz-grab'
1051   });
1052   
1053   
1054   // Set up the relationship creation dialog. This also functions as the reading
1055   // merge dialog where appropriate.
1056                           
1057   if( editable ) {
1058         $( '#dialog-form' ).dialog( {
1059         autoOpen: false,
1060         height: 270,
1061         width: 330,
1062         modal: true,
1063         buttons: {
1064           'Merge readings': function( evt ) {
1065                   $( evt.target ).button( 'disable' );
1066                   $( '#status' ).empty();
1067                   form_values = $( '#collapse_node_form' ).serialize();
1068                   ncpath = getTextURL( 'merge' );
1069                   var jqjson = $.post( ncpath, form_values, function( data ) {
1070                           merge_nodes( $( '#source_node_id' ).val(), $( '#target_node_id' ).val(), data );
1071                           $(evt.target).button( 'enable' );
1072               $( '#dialog-form' ).dialog( 'close' );
1073                   } );
1074           },
1075           OK: function( evt ) {
1076                 $( evt.target ).button( 'disable' );
1077                 $( '#status' ).empty();
1078                 form_values = $( '#collapse_node_form' ).serialize();
1079                 ncpath = getTextURL( 'relationships' );
1080                 var jqjson = $.post( ncpath, form_values, function( data ) {
1081                         $.each( data, function( item, source_target ) { 
1082                                 var source_found = get_ellipse( source_target[0] );
1083                                 var target_found = get_ellipse( source_target[1] );
1084                                 var relation_found = $.inArray( source_target[2], $( '#keymap' ).data( 'relations' ) );
1085                                 if( source_found.size() && target_found.size() && relation_found > -1 ) {
1086                                         var relation = relation_manager.create( source_target[0], source_target[1], relation_found );
1087                                         relation_manager.toggle_active( relation.attr('id') );
1088                                         $.each( $('#collapse_node_form').serializeArray(), function( i, k ) {
1089                                                 relation.data( k.name, k.value );
1090                                         });
1091                                 }
1092                                 $(evt.target).button( 'enable' );
1093                    });
1094                         $( '#dialog-form' ).dialog( 'close' );
1095                 }, 'json' );
1096           },
1097           Cancel: function() {
1098                   $( this ).dialog( 'close' );
1099           }
1100         },
1101         create: function(event, ui) { 
1102                 $(this).data( 'relation_drawn', false );
1103                 $('#rel_type').data( 'changed_after_open', false );
1104                 $.each( relationship_types, function(index, typedef) {   
1105                          $('#rel_type').append( $('<option />').attr( "value", typedef.name ).text(typedef.name) ); 
1106                 });
1107                 $.each( relationship_scopes, function(index, value) {   
1108                          $('#scope').append( $('<option />').attr( "value", value ).text(value) ); 
1109                 });
1110                 // Handler to clear the annotation field, the first time the relationship is
1111                 // changed after opening the form.
1112                 $('#rel_type').change( function () {
1113                         if( !$(this).data( 'changed_after_open' ) ) {
1114                                 $('#note').val('');
1115                         }
1116                         $(this).data( 'changed_after_open', true );
1117                 });
1118         },
1119         open: function() {
1120                 relation_manager.create_temporary( 
1121                         $('#source_node_id').val(), $('#target_node_id').val() );
1122                 var buttonset = $(this).parent().find( '.ui-dialog-buttonset' )
1123                 if( readings_equivalent( $('#source_node_id').val(), 
1124                                 $('#target_node_id').val() ) ) {
1125                         buttonset.find( "button:contains('Merge readings')" ).show();
1126                 } else {
1127                         buttonset.find( "button:contains('Merge readings')" ).hide();
1128                 }
1129                 $(".ui-widget-overlay").css("background", "none");
1130                 $("#dialog_overlay").show();
1131                 $("#dialog_overlay").height( $("#enlargement_container").height() );
1132                 $("#dialog_overlay").width( $("#enlargement_container").innerWidth() );
1133                 $("#dialog_overlay").offset( $("#enlargement_container").offset() );
1134                 $('#rel_type').data( 'changed_after_open', false );
1135         },
1136         close: function() {
1137                 relation_manager.remove_temporary();
1138                 $( '#status' ).empty();
1139                 $("#dialog_overlay").hide();
1140         }
1141         }).ajaxError( function(event, jqXHR, ajaxSettings, thrownError) {
1142                 if( ajaxSettings.url == getTextURL('relationships') 
1143                         && ajaxSettings.type == 'POST' && jqXHR.status == 403 ) {
1144                         var error;
1145                         if( jqXHR.responseText.indexOf('do not have permission to modify') > -1 ) {
1146                                 error = 'You are not authorized to modify this tradition. (Try logging in again?)';
1147                         } else {
1148                                 try {
1149                                         var errobj = jQuery.parseJSON( jqXHR.responseText );
1150                                         error = errobj.error + '</br>The relationship cannot be made.</p>';
1151                                 } catch(e) {
1152                                         error = jqXHR.responseText;
1153                                 }
1154                         }
1155                         $('#status').append( '<p class="error">Error: ' + error );
1156                 }
1157                 $(event.target).parent().find('.ui-button').button("enable");
1158         } );
1159   }
1160
1161   // Set up the relationship info display and deletion dialog.  
1162   $( "#delete-form" ).dialog({
1163     autoOpen: false,
1164     height: 135,
1165     width: 300,
1166     modal: false,
1167     buttons: {
1168         OK: function() { $( this ).dialog( "close" ); },
1169         "Delete all": function () { delete_relation( true ); },
1170         Delete: function() { delete_relation( false ); }
1171     },
1172     create: function(event, ui) {
1173         // TODO What is this logic doing?
1174         // This scales the buttons in the dialog and makes it look proper
1175         // Not sure how essential it is, does anything break if it's not here?
1176         var buttonset = $(this).parent().find( '.ui-dialog-buttonset' ).css( 'width', '100%' );
1177         buttonset.find( "button:contains('OK')" ).css( 'float', 'right' );
1178         // A: This makes sure that the pop up delete relation dialogue for a hovered over
1179         // relation auto closes if the user doesn't engage (mouseover) with it.
1180         var dialog_aria = $("div[aria-labelledby='ui-dialog-title-delete-form']");  
1181         dialog_aria.mouseenter( function() {
1182             if( mouseWait != null ) { clearTimeout(mouseWait) };
1183         })
1184         dialog_aria.mouseleave( function() {
1185             mouseWait = setTimeout( function() { $("#delete-form").dialog( "close" ) }, 2000 );
1186         })
1187     },
1188     open: function() {
1189         // Show the appropriate buttons...
1190                 var buttonset = $(this).parent().find( '.ui-dialog-buttonset' )
1191                 // If the user can't edit, show only the OK button
1192         if( !editable ) {
1193                 buttonset.find( "button:contains('Delete')" ).hide();
1194         // If the relationship scope is local, show only OK and Delete
1195         } else if( $('#delete_relation_scope').text() === 'local' ) {
1196                 $( this ).dialog( "option", "width", 160 );
1197                 buttonset.find( "button:contains('Delete')" ).show();
1198                 buttonset.find( "button:contains('Delete all')" ).hide();
1199         // Otherwise, show all three
1200         } else {
1201                 $( this ).dialog( "option", "width", 200 );
1202                 buttonset.find( "button:contains('Delete')" ).show();
1203                 }       
1204         mouseWait = setTimeout( function() { $("#delete-form").dialog( "close" ) }, 2000 );
1205     },
1206     close: function() {}
1207   });
1208
1209   $( "#multipleselect-form" ).dialog({
1210     autoOpen: false,
1211     height: 150,
1212     width: 250,
1213     modal: true,
1214     buttons: {
1215         Cancel: function() { $( this ).dialog( "close" ); },
1216         Detach: function ( evt ) { 
1217             var self = $(this);
1218             $( evt.target ).button( "disable" );
1219             var form_values = $('#detach_collated_form').serialize();
1220             ncpath = getTextURL( 'duplicate' );
1221             var jqjson = $.post( ncpath, form_values, function(data) {
1222                 detach_node( data );
1223                 $(evt.target).button("enable");
1224                 self.dialog( "close" );
1225             } );
1226         }
1227     },
1228     create: function(event, ui) {
1229         var buttonset = $(this).parent().find( '.ui-dialog-buttonset' ).css( 'width', '100%' );
1230         buttonset.find( "button:contains('Cancel')" ).css( 'float', 'right' );
1231     },
1232     open: function() {
1233         $( this ).dialog( "option", "width", 200 );
1234         $(".ui-widget-overlay").css("background", "none");
1235         $('#multipleselect-form-status').empty();
1236         $("#dialog_overlay").show();
1237         $("#dialog_overlay").height( $("#enlargement_container").height() );
1238         $("#dialog_overlay").width( $("#enlargement_container").innerWidth() );
1239         $("#dialog_overlay").offset( $("#enlargement_container").offset() );
1240     },
1241     close: function() { 
1242         marquee.unselect();
1243         $("#dialog_overlay").hide();
1244     }
1245   }).ajaxError( function(event, jqXHR, ajaxSettings, thrownError) {
1246     if( ajaxSettings.url == getTextURL('duplicate') 
1247       && ajaxSettings.type == 'POST' && jqXHR.status == 403 ) {
1248       var error;
1249       if( jqXHR.responseText.indexOf('do not have permission to modify') > -1 ) {
1250         error = 'You are not authorized to modify this tradition. (Try logging in again?)';
1251       } else {
1252         try {
1253           var errobj = jQuery.parseJSON( jqXHR.responseText );
1254           error = errobj.error + '</br>The relationship cannot be made.</p>';
1255         } catch(e) {
1256           error = jqXHR.responseText;
1257         }
1258       }
1259       $('#multipleselect-form-status').append( '<p class="error">Error: ' + error );
1260     }
1261     $(event.target).parent().find('.ui-button').button("enable");
1262   }); 
1263
1264
1265   // Helpers for relationship deletion
1266   
1267   function delete_relation( scopewide ) {
1268           form_values = $('#delete_relation_form').serialize();
1269           if( scopewide ) {
1270                 form_values += "&scopewide=true";
1271           }
1272           ncpath = getTextURL( 'relationships' );
1273           var jqjson = $.ajax({ url: ncpath, data: form_values, success: function(data) {
1274                   $.each( data, function(item, source_target) { 
1275                           relation_manager.remove( get_relation_id( source_target[0], source_target[1] ) );
1276                   });
1277                   $( "#delete-form" ).dialog( "close" );
1278           }, dataType: 'json', type: 'DELETE' });
1279   }
1280   
1281   function toggle_relation_active( node_id ) {
1282       $('#svgenlargement .relation').find( "title:contains('" + node_id +  "')" ).each( function(index) {
1283           matchid = new RegExp( "^" + node_id );
1284           if( $(this).text().match( matchid ) != null ) {
1285                   var relation_id = $(this).parent().attr('id');
1286               relation_manager.toggle_active( relation_id );
1287           };
1288       });
1289   }
1290
1291   // function for reading form dialog should go here; 
1292   // just hide the element for now if we don't have morphology
1293   if( can_morphologize ) {
1294           if( editable ) {
1295                   $('#reading_decollate_witnesses').multiselect();
1296           } else {
1297                   $('#decollation').hide();
1298           }
1299           $('#reading-form').dialog({
1300                 autoOpen: false,
1301                 // height: 400,
1302                 width: 450,
1303                 modal: true,
1304                 buttons: {
1305                         Cancel: function() {
1306                                 $( this ).dialog( "close" );
1307                         },
1308                         Update: function( evt ) {
1309                                 // Disable the button
1310                                 $(evt.target).button("disable");
1311                                 $('#reading_status').empty();
1312                                 var reading_id = $('#reading_id').val()
1313                                 form_values = {
1314                                         'id' : reading_id,
1315                                         'is_nonsense': $('#reading_is_nonsense').is(':checked'),
1316                                         'grammar_invalid': $('#reading_grammar_invalid').is(':checked'),
1317                                         'normal_form': $('#reading_normal_form').val() };
1318                                 // Add the morphology values
1319                                 $('.reading_morphology').each( function() {
1320                                         if( $(this).val() != '(Click to select)' ) {
1321                                                 var rmid = $(this).attr('id');
1322                                                 rmid = rmid.substring(8);
1323                                                 form_values[rmid] = $(this).val();
1324                                         }
1325                                 });
1326                                 // Make the JSON call
1327                                 ncpath = getReadingURL( reading_id );
1328                                 var reading_element = readingdata[reading_id];
1329                                 // $(':button :contains("Update")').attr("disabled", true);
1330                                 var jqjson = $.post( ncpath, form_values, function(data) {
1331                                         $.each( data, function(key, value) { 
1332                                                 reading_element[key] = value;
1333                                         });
1334                                         if( $('#update_workspace_button').data('locked') == false ) {
1335                                                 color_inactive( get_ellipse( reading_id ) );
1336                                         }
1337                                         $(evt.target).button("enable");
1338                                         $( "#reading-form" ).dialog( "close" );
1339                                 });
1340                                 // Re-color the node if necessary
1341                                 return false;
1342                         }
1343                 },
1344                 create: function() {
1345                         if( !editable ) {
1346                                 // Get rid of the disallowed editing UI bits
1347                                 $( this ).dialog( "option", "buttons", 
1348                                         [{ text: "OK", click: function() { $( this ).dialog( "close" ); }}] );
1349                                 $('#reading_relemmatize').hide();
1350                         }
1351                 },
1352                 open: function() {
1353                         $(".ui-widget-overlay").css("background", "none");
1354                         $('#reading_decollate_witnesses').multiselect("refresh");
1355                         $('#reading_decollate_witnesses').multiselect("uncheckAll");
1356                         $("#dialog_overlay").show();
1357                         $('#reading_status').empty();
1358                         $("#dialog_overlay").height( $("#enlargement_container").height() );
1359                         $("#dialog_overlay").width( $("#enlargement_container").innerWidth() );
1360                         $("#dialog_overlay").offset( $("#enlargement_container").offset() );
1361                         $("#reading-form").parent().find('.ui-button').button("enable");
1362                 },
1363                 close: function() {
1364                         $("#dialog_overlay").hide();
1365                 }
1366           }).ajaxError( function(event, jqXHR, ajaxSettings, thrownError) {
1367                 if( ajaxSettings.url.lastIndexOf( getReadingURL('') ) > -1
1368                         && ajaxSettings.type == 'POST' && jqXHR.status == 403 ) {
1369                         var error;
1370                         if( jqXHR.responseText.indexOf('do not have permission to modify') > -1 ) {
1371                                 error = 'You are not authorized to modify this tradition. (Try logging in again?)';
1372                         } else {
1373                                 try {
1374                                         var errobj = jQuery.parseJSON( jqXHR.responseText );
1375                                         error = errobj.error + '</br>The relationship cannot be made.</p>';
1376                                 } catch(e) {
1377                                         error = jqXHR.responseText;
1378                                 }
1379                         }
1380                         $('#status').append( '<p class="error">Error: ' + error );
1381                 }
1382                 $(event.target).parent().find('.ui-button').button("enable");
1383           });
1384         } else {
1385                 $('#reading-form').hide();
1386         }
1387   
1388
1389   $('#update_workspace_button').click( function() {
1390          if( !editable ) {
1391                 return;
1392          }
1393      var svg_enlargement = $('#svgenlargement').svg().svg('get').root();
1394      mouse_scale = svg_root_element.getScreenCTM().a;
1395      if( $(this).data('locked') == true ) {
1396          $('#svgenlargement ellipse' ).each( function( index ) {
1397              if( $(this).data( 'node_obj' ) != null ) {
1398                  $(this).data( 'node_obj' ).ungreyout_edges();
1399                  $(this).data( 'node_obj' ).set_selectable( false );
1400                  color_inactive( $(this) );
1401                  var node_id = $(this).data( 'node_obj' ).get_id();
1402                  toggle_relation_active( node_id );
1403                  $(this).data( 'node_obj', null );
1404              }
1405          })
1406          $(this).data('locked', false);
1407          $(this).css('background-position', '0px 44px');
1408      } else {
1409          var left = $('#enlargement').offset().left;
1410          var right = left + $('#enlargement').width();
1411          var tf = svg_root_element.getScreenCTM().inverse(); 
1412          var p = svg_root.createSVGPoint();
1413          p.x=left;
1414          p.y=100;
1415          var cx_min = p.matrixTransform(tf).x;
1416          p.x=right;
1417          var cx_max = p.matrixTransform(tf).x;
1418          $('#svgenlargement ellipse').each( function( index ) {
1419              var cx = parseInt( $(this).attr('cx') );
1420              if( cx > cx_min && cx < cx_max) { 
1421                  if( $(this).data( 'node_obj' ) == null ) {
1422                      $(this).data( 'node_obj', new node_obj( $(this) ) );
1423                  } else {
1424                      $(this).data( 'node_obj' ).set_selectable( true );
1425                  }
1426                  $(this).data( 'node_obj' ).greyout_edges();
1427                  var node_id = $(this).data( 'node_obj' ).get_id();
1428                  toggle_relation_active( node_id );
1429              }
1430          });
1431          $(this).css('background-position', '0px 0px');
1432          $(this).data('locked', true );
1433      }
1434   });
1435
1436   if( !editable ) {  
1437     // Hide the unused elements
1438     $('#dialog-form').hide();
1439     $('#update_workspace_button').hide();
1440   }
1441
1442   
1443   $('.helptag').popupWindow({ 
1444           height:500, 
1445           width:800, 
1446           top:50, 
1447           left:50,
1448           scrollbars:1 
1449   }); 
1450
1451   expandFillPageClients();
1452   $(window).resize(function() {
1453     expandFillPageClients();
1454   });
1455
1456 });
1457
1458
1459 function expandFillPageClients() {
1460         $('.fillPage').each(function () {
1461                 $(this).height($(window).height() - $(this).offset().top - MARGIN);
1462         });
1463 }
1464
1465 function loadSVG(svgData) {
1466         var svgElement = $('#svgenlargement');
1467
1468         $(svgElement).svg('destroy');
1469
1470         $(svgElement).svg({
1471                 loadURL: svgData,
1472                 onLoad : svgEnlargementLoaded
1473         });
1474 }
1475
1476
1477
1478 /*      OS Gadget stuff
1479
1480 function svg_select_callback(topic, data, subscriberData) {
1481         svgData = data;
1482         loadSVG(svgData);
1483 }
1484
1485 function loaded() {
1486         var prefs = new gadgets.Prefs();
1487         var preferredHeight = parseInt(prefs.getString('height'));
1488         if (gadgets.util.hasFeature('dynamic-height')) gadgets.window.adjustHeight(preferredHeight);
1489         expandFillPageClients();
1490 }
1491
1492 if (gadgets.util.hasFeature('pubsub-2')) {
1493         gadgets.HubSettings.onConnect = function(hum, suc, err) {
1494                 subId = gadgets.Hub.subscribe("interedition.svg.selected", svg_select_callback);
1495                 loaded();
1496         };
1497 }
1498 else gadgets.util.registerOnLoadHandler(loaded);
1499 */