start of relationship mapper
Tara L Andrews [Thu, 12 Jan 2012 16:24:59 +0000 (17:24 +0100)]
TreeOfTexts/lib/TreeOfTexts.pm
TreeOfTexts/lib/TreeOfTexts/Controller/Root.pm
TreeOfTexts/lib/TreeOfTexts/View/JSON.pm [new file with mode: 0644]
TreeOfTexts/root/js/relationship.js [new file with mode: 0644]
TreeOfTexts/root/js/svginteraction.js [new file with mode: 0644]
TreeOfTexts/root/src/relate.tt [new file with mode: 0644]
TreeOfTexts/t/view_JSON.t [new file with mode: 0644]

index 20b70ae..e29f3a8 100644 (file)
@@ -41,6 +41,9 @@ __PACKAGE__->config(
     # Disable deprecated behavior needed by old applications
     disable_component_resolution_regex_fallback => 1,
     default_view => 'TT',
+       'View::JSON' => {
+               expose_stash => 'result',
+       },
 );
 
 # Start the application
index 3141c0b..137d28a 100644 (file)
@@ -61,8 +61,11 @@ sub relationships :Local {
        my( $self, $c ) = @_;
        my $m = $c->model('Directory');
        my $tradition = $m->tradition( $c->request->params->{'textid'} );
-       $c->stash->{alignment} = $tradition->collation->make_alignment_table( 'refs' );
-       $c->stash->{template} = 'relationships.tt';     
+       my $table = $tradition->collation->make_alignment_table();
+       my $witlist = shift @$table;
+       $c->stash->{witnesses} = $wits;
+       $c->stash->{alignment} = $table;
+       $c->stash->{template} = 'relate.tt';    
 }
 
 =head2 stexaminer
@@ -88,6 +91,21 @@ sub stexaminer :Local {
        $c->stash->{conflict} = $t->{'conflict_count'};
 }
 
+=head2 alignment_table 
+
+Return a JSON alignment table of a given text.
+
+=cut
+
+sub alignment_table :Local {
+       my( $self, $c ) = @_;
+       my $m = $c->model( 'Directory' );
+       my $tradition = $m->tradition( $c->request->params->{'textid'} );
+       my $table = $tradition->collation->make_alignment_table();
+       $c->stash->{'result'} => $table;
+       $c->forward-( 'View::JSON' );
+}
+
 =head1 OPENSOCIAL URLs
 
 =head2 view_table
diff --git a/TreeOfTexts/lib/TreeOfTexts/View/JSON.pm b/TreeOfTexts/lib/TreeOfTexts/View/JSON.pm
new file mode 100644 (file)
index 0000000..b08faa9
--- /dev/null
@@ -0,0 +1,29 @@
+package TreeOfTexts::View::JSON;
+
+use strict;
+use base 'Catalyst::View::JSON';
+
+=head1 NAME
+
+TreeOfTexts::View::JSON - Catalyst JSON View
+
+=head1 SYNOPSIS
+
+See L<TreeOfTexts>
+
+=head1 DESCRIPTION
+
+Catalyst JSON View.
+
+=head1 AUTHOR
+
+Tara Andrews
+
+=head1 LICENSE
+
+This library is free software, you can redistribute it and/or modify
+it under the same terms as Perl itself.
+
+=cut
+
+1;
diff --git a/TreeOfTexts/root/js/relationship.js b/TreeOfTexts/root/js/relationship.js
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/TreeOfTexts/root/js/svginteraction.js b/TreeOfTexts/root/js/svginteraction.js
new file mode 100644 (file)
index 0000000..8fa72e2
--- /dev/null
@@ -0,0 +1,310 @@
+function getRelativePath( action ) {
+    path_elements = window.location.pathname.split('/'); 
+    if( path_elements[1].length > 0 ) {
+        return window.location.pathname.split('/')[1] + '/' + action;
+    } else {
+        return action;
+    }
+}
+
+function svgLoaded() {
+  $('ellipse').attr( {stroke:'black', fill:'#fff'} );
+  ncpath = getRelativePath( 'node_click' );
+  var jqjson = $.getJSON( ncpath, 'node_id=null', function(data) {
+    $.each( data, function(item, node_id_and_state) {
+      if( node_id_and_state[1] == 1 ) {
+        node_ellipse = $('.node').children('title').filter( function(index) {
+          return $(this).text() == node_id_and_state[0];
+        }).siblings('ellipse');
+        node_ellipse.attr( {stroke:'green', fill:'#b3f36d'} );
+        $('#constructedtext').append( node_ellipse.siblings('text').text() + '&#32;' );
+      } else {
+        if( node_id_and_state[1] == null ) {
+          $('#constructedtext').append( ' &hellip; ' );
+        }
+      }
+    });
+    add_node_objs();
+  });
+}
+
+function add_node_objs() {
+  $('ellipse[fill="#fff"]').each( function() {
+      $(this).data( 'node_obj', new node_obj( $(this) ) );
+    }
+  );
+}
+
+function get_node_obj( node_id ) {
+  return $('.node').children('title').filter( function(index) {
+    return $(this).text() == node_id;
+  }).siblings('ellipse').data( 'node_obj' );
+}
+
+function get_edge( edge_id ) {
+  return $('.edge').filter( function(index) {
+    return $(this).children( 'title' ).text() == $('<div/>').html(edge_id).text() ;
+  });
+}
+
+function node_obj(ellipse) {
+  this.ellipse = ellipse;
+  var self = this;
+  
+  this.x = 0;
+  this.y = 0;
+  this.dx = 0;
+  this.dy = 0;
+  this.node_elements = node_elements_for(self.ellipse);
+  this.sub_nodes = [];
+  this.super_node = null;
+
+  this.dblclick_listener = function(evt) {
+    node_id = self.ellipse.siblings('title').text();
+    ncpath = getRelativePath( 'node_click' );
+    var jqjson = $.getJSON( ncpath, 'node_id=' + node_id, function(data) {
+      $('#constructedtext').empty();
+      $.each( data, function(item, node_id_and_state) {
+        node = get_node_obj( node_id_and_state[0] );
+        // 1 -> turn the associated SVG node on, put in the associate word in the text box.
+        // 0 -> turn SVG node off.
+        // null -> turn node off, put in ellipsis in text box at the corresponding place.
+        if( node_id_and_state[1] == 1 ) {
+//TODO: create test suite en refactor this in to more OO! (node and node_ellipse are 'conflated')
+          node_ellipse = $('.node').children('title').filter( function(index) {
+            return $(this).text() == node_id_and_state[0];
+          }).siblings('ellipse');
+          $('#constructedtext').append( node_ellipse.siblings('text').text() + '&#32;' );
+          if( node ) { node.set_draggable( false ) }
+        } else {
+          if( node ) { node.set_draggable( true ) };
+          if( node_id_and_state[1] == null ) {
+            $('#constructedtext').append( ' &hellip; ' );
+          }
+        }
+      });
+    });
+  }
+
+  this.set_draggable = function( draggable ) {
+    if( draggable ) {
+      self.ellipse.attr( {stroke:'black', fill:'#fff'} );
+      self.ellipse.mousedown( this.mousedown_listener );
+      self.ellipse.hover( this.enter_node, this.leave_node );  
+    } else {
+      self.ellipse.unbind('mouseenter').unbind('mouseleave').unbind('mousedown');
+      self.ellipse.attr( {stroke:'green', fill:'#b3f36d'} );
+    }
+  }
+
+  this.mousedown_listener = function(evt) {
+    evt.stopPropagation();
+    self.x = evt.clientX;
+    self.y = evt.clientY;
+    $('body').mousemove( self.mousemove_listener );
+    $('body').mouseup( self.mouseup_listener );
+    self.ellipse.unbind('mouseenter').unbind('mouseleave')
+    self.ellipse.attr( 'fill', '#ff66ff' );
+  }
+
+  this.mousemove_listener = function(evt) {
+    self.dx = evt.clientX - self.x;
+    self.dy = evt.clientY - self.y;
+    self.move_elements();
+  }
+
+  this.mouseup_listener = function(evt) {    
+    if( $('ellipse[fill="#ffccff"]').size() > 0 ) {
+      $('#source_node_id').val( self.ellipse.siblings('title').text() );
+      $('#target_node_id').val( $('ellipse[fill="#ffccff"]').siblings("title").text() );
+      $( '#dialog-form' ).dialog( 'open' );
+    };
+    $('body').unbind('mousemove');
+    $('body').unbind('mouseup');
+    self.ellipse.attr( 'fill', '#fff' );
+    self.ellipse.hover( self.enter_node, self.leave_node );
+    if( self.super_node ) {
+      self.eclipse();
+    } else {
+      self.reset_elements();
+    }
+  }
+
+  this.cpos = function() {
+    return { x: self.ellipse.attr('cx'), y: self.ellipse.attr('cy') };
+  }
+
+  this.get_g = function() {
+    return self.ellipse.parent('g');
+  }
+
+  this.stack_behind = function( collapse_info ) {
+    self.super_node = get_node_obj( collapse_info.target );
+    self.super_node.sub_nodes.push( self );
+    self.eclipse();
+    if( collapse_info.edges ) {
+      $.each( collapse_info.edges, function( source_edge_id, target_info ) {
+        get_edge(source_edge_id).attr( 'display', 'none' );
+        target_edge = get_edge(target_info.target);
+        // Unfortunately, the simple solution doesn't work...
+        // target_edge.children( 'text' ).replaceWith( '<text x="2270" y="-59.400001525878906"><tspan text-anchor="middle">A, B</tspan><tspan fill="red">, C</tspan></text>' );
+        // ..so we take the long and winding road...
+        var svg = $('#svgbasics').children('svg').svg().svg('get');
+        textx = target_edge.children( 'text' )[0].x.baseVal.getItem(0).value
+        texty = target_edge.children( 'text' )[0].y.baseVal.getItem(0).value
+        current_label = target_edge.children( 'text' ).text(); 
+        target_edge.children( 'text' ).remove();
+        texts = svg.createText();
+        texts.span(current_label, {'text-anchor': 'middle'}).span(target_info.label, {fill: 'red'});
+        svg.text(target_edge, textx, texty, texts);
+      }); 
+    }
+  }
+
+  this.eclipse = function() {
+    self.dx = new Number( self.super_node.cpos().x ) - new Number( self.cpos().x ) + ( 10 * (self.super_node.sub_nodes.indexOf(self) + 1) );
+    self.dy = new Number( self.super_node.cpos().y ) - new Number( self.cpos().y ) + ( 5 * (self.super_node.sub_nodes.indexOf(self) + 1) );
+    self.move_elements();
+    eclipse_index = self.super_node.sub_nodes.indexOf(self) - 1;
+    if( eclipse_index > -1 ) {
+      self.get_g().insertBefore( self.super_node.sub_nodes[eclipse_index].get_g() );
+    } else {
+      self.get_g().insertBefore( self.super_node.get_g() );
+    }
+  }
+
+  this.enter_node = function(evt) {
+    self.ellipse.attr( 'fill', '#ffccff' );
+  }
+
+  this.leave_node = function(evt) {
+    self.ellipse.attr( 'fill', '#fff' );
+  }
+
+  this.move_elements = function() {
+    $.each( self.node_elements, function(index, value) {
+      value.move(self.dx,self.dy);
+    });
+  }
+
+  this.reset_elements = function() {
+    $.each( self.node_elements, function(index, value) {
+      value.reset();
+    });
+  }
+
+  this.ellipse.dblclick( this.dblclick_listener );
+  self.set_draggable( true );
+}
+
+function svgshape( shape_element ) {
+  this.shape = shape_element;
+  this.move = function(dx,dy) {
+    this.shape.attr( "transform", "translate(" + dx + " " + dy + ")" );
+  }
+  this.reset = function() {
+    this.shape.attr( "transform", "translate( 0, 0 )" );
+  }
+}
+
+function svgpath( path_element ) {
+  this.path = path_element;
+  this.x = this.path.x;
+  this.y = this.path.y;
+  this.move = function(dx,dy) {
+    this.path.x = this.x + dx;
+    this.path.y = this.y + dy;
+  }
+  this.reset = function() {
+    this.path.x = this.x;
+    this.path.y = this.y;
+  }
+}
+
+function node_elements_for( ellipse ) {
+  node_elements = get_edge_elements_for( ellipse );
+  node_elements.push( new svgshape( ellipse.siblings('text') ) );
+  node_elements.push( new svgshape( ellipse ) );
+  return node_elements;
+}
+
+function get_edge_elements_for( ellipse ) {
+  edge_elements = new Array();
+  node_id = ellipse.siblings('title').text();
+  edge_in_pattern = new RegExp( node_id + '$' );
+  edge_out_pattern = new RegExp( '^' + node_id );
+  $.each( $('.edge').children('title'), function(index) {
+    title = $(this).text();
+    if( edge_in_pattern.test(title) ) {
+      edge_elements.push( new svgshape( $(this).siblings('polygon') ) );
+      path_segments = $(this).siblings('path')[0].pathSegList;
+      edge_elements.push( new svgpath( path_segments.getItem(path_segments.numberOfItems - 1) ) );
+    }
+    if( edge_out_pattern.test(title) ) {
+      path_segments = $(this).siblings('path')[0].pathSegList;
+      edge_elements.push( new svgpath( path_segments.getItem(0) ) );
+    }
+  });
+  return edge_elements;
+} 
+
+$(document).ready(function () {
+  $('#graph').ajaxError(function() {
+    console.log( 'Oops.. something went wrong with trying to save this change. Please try again...' );
+  });
+  $('#graph').mousedown(function (event) {
+    $(this)
+      .data('down', true)
+      .data('x', event.clientX)
+      .data('scrollLeft', this.scrollLeft);
+      return false;
+  }).mouseup(function (event) {
+    $(this).data('down', false);
+  }).mousemove(function (event) {
+    if ($(this).data('down') == true ) {
+      this.scrollLeft = $(this).data('scrollLeft') + $(this).data('x') - event.clientX;
+    }
+  }).mousewheel(function (event, delta) {
+      this.scrollLeft -= (delta * 30);
+  }).css({
+    'overflow' : 'hidden',
+    'cursor' : '-moz-grab'
+  });
+  $( "#dialog-form" ).dialog({
+    autoOpen: false,
+    height: 150,
+    width: 250,
+    modal: true,
+    buttons: {
+      "Ok": function() {
+        form_values = $('#collapse_node_form').serialize()
+       ncpath = getRelativePath( 'node_collapse' );
+        var jqjson = $.getJSON( ncpath, form_values, function(data) {
+          $.each( data, function(item, collapse_info) { 
+            get_node_obj( item ).stack_behind( collapse_info );
+          });
+        });
+        $( this ).dialog( "close" );
+      },
+      Cancel: function() {
+        $( this ).dialog( "close" );
+      }
+    },
+    close: function() {
+      $('#reason').val( "" ).removeClass( "ui-state-error" );
+    }
+  });
+});
+
+
+$(window).mouseout(function (event) {
+  if ($('#graph').data('down')) {
+    try {
+      if (event.originalTarget.nodeName == 'BODY' || event.originalTarget.nodeName == 'HTML') {
+        $('#graph').data('down', false);
+      }                
+    } catch (e) {}
+  }
+});
+
+
diff --git a/TreeOfTexts/root/src/relate.tt b/TreeOfTexts/root/src/relate.tt
new file mode 100644 (file)
index 0000000..4ef43c3
--- /dev/null
@@ -0,0 +1,22 @@
+[% BLOCK js %]
+    <script type="text/javascript" src="js/jquery-1.4.4.min.js"></script>
+    <script type="text/javascript" src="js/jquery-ui-1.8.10.custom.min.js"></script>
+    <script type="text/javascript" src="js/interaction.js"></script>
+[% END %]
+
+<div id="row_directory">
+       <table>
+               <tr>
+[% FOREACH wit IN witnesses -%]
+                       <th>[% wit %]</th>
+[% END -%]
+               </tr>
+[% FOREACH row IN alignment -%]
+               <tr>
+[% FOREACH item in row -%]
+                       <td>[% item %]</td>
+[% END -%]
+               </tr>
+[% END -%]
+       </table>
+</div>
\ No newline at end of file
diff --git a/TreeOfTexts/t/view_JSON.t b/TreeOfTexts/t/view_JSON.t
new file mode 100644 (file)
index 0000000..aa5a7ce
--- /dev/null
@@ -0,0 +1,8 @@
+use strict;
+use warnings;
+use Test::More;
+
+
+BEGIN { use_ok 'TreeOfTexts::View::JSON' }
+
+done_testing();