From: Tara L Andrews Date: Thu, 2 Feb 2012 04:21:17 +0000 (+0100) Subject: first pass integration of relationship mapper X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=commitdiff_plain;h=581aee24412a96f284c487dfbcea2d8957aae10e;p=scpubgit%2Fstemmatology.git first pass integration of relationship mapper --- diff --git a/stemmaweb/lib/stemmaweb/Controller/Relation.pm b/stemmaweb/lib/stemmaweb/Controller/Relation.pm index f4828e1..536269a 100644 --- a/stemmaweb/lib/stemmaweb/Controller/Relation.pm +++ b/stemmaweb/lib/stemmaweb/Controller/Relation.pm @@ -1,6 +1,7 @@ package stemmaweb::Controller::Relation; use Moose; use namespace::autoclean; +use TryCatch; BEGIN { extends 'Catalyst::Controller' } @@ -11,31 +12,112 @@ stemmaweb::Controller::Relation - Controller for the relationship mapper =head1 DESCRIPTION -The stemma analysis tool with the pretty colored table. +The reading relationship mapper with draggable nodes. =head1 METHODS +=head2 index + GET relation/$textid Renders the application for the text identified by $textid. -=head2 index - -The relationship editor tool. - =cut sub index :Path :Args(1) { my( $self, $c, $textid ) = @_; my $m = $c->model('Directory'); my $tradition = $m->tradition( $textid ); - my $table = $tradition->collation->make_alignment_table(); - my $witlist = map { $_->{'witness'} } @{$table->{'alignment'}}; - $c->stash->{witnesses} = $witlist; - $c->stash->{alignment} = $table; - $c->stash->{template} = 'relate.tt'; + my $collation = $tradition->collation; + my $svg_str = $collation->as_svg; + $svg_str =~ s/\n//gs; + $c->stash->{'svg_string'} = $svg_str; + $c->stash->{'template'} = 'relate.tt'; +} + +sub dispatcher :Path :Args(2) { + my( $self, $c, $textid, $forward ) = @_; + $c->stash->{'collation'} = $c->model('Directory')->tradition( $textid )->collation; + $c->forward( $forward ); +} + +=head2 relationship_definition + + GET relation/relationship_definition + +Returns a data structure giving the valid types and scopes for a relationship. + +=cut + +sub relationship_definition :Local :Args(0) { + my( $self, $c ) = @_; + my $valid_relationships = [ qw/ spelling orthographic grammatical meaning / ]; + my $valid_scopes = [ qw/ local global / ]; + $c->stash->{'result'} = { 'types' => $valid_relationships, 'scopes' => $valid_scopes }; + $c->forward('View::JSON'); } +=head2 set_relationship + + POST relation/$textid/relationship + source_id: $source, target_id: $target, rel_type: $type, scope: $scope + +Sets the specified relationship between the readings in $source and $target. +Returns 200 and a list of node pairs where the relationship was added on success; +returns 403 and an { error: message } struct on failure. + +=cut + +sub relationship :Private { + my( $self, $c ) = @_; + my $collation = delete $c->stash->{'collation'}; + my $node = $c->request->param('source_id'); + my $target = $c->request->param('target_id'); + my $relation = $c->request->param('rel_type'); + my $note = $c->request->param('note'); + my $scope = $c->request->param('scope'); + + my $opts = { 'type' => $relation, + 'scope' => $scope }; + + try { + my @vectors = $collation->add_relationship( $node, $target, $opts ); + $c->stash->{'result'} = \@vectors; + } catch( Text::Tradition::Error $e ) { + $c->response->status( '403' ); + $c->stash->{'result'} = { 'error' => $e->message }; + } + $c->forward('View::JSON'); +} + +=head2 relationships + + GET relation/$textid/relationships + +Returns a list of relationships that exist in the specified text. Each +relationship is returned in a struct that looks like: + +{ source: $sid, target: $tid, type: $rel_type, scope: $rel_scope } + +=cut + +sub get_relationships :Private { + my( $self, $c ) = @_; + my $collation = delete $c->stash->{'collation'}; + # TODO make this API + my @pairs = $collation->relationships; # returns the edges + my @all_relations; + foreach my $p ( @pairs ) { + my $relobj = $collation->relations->get_relationship( @$p ); + push( @all_relations, + { source => $p->[0], target => $p->[1], + type => $relobj->type, scope => $relobj->scope } ); + } + $c->stash->{'result'} = \@all_relations; + $c->forward('View::JSON'); +} + + =head2 end Attempt to render a view, if needed. diff --git a/stemmaweb/root/css/relationship.css b/stemmaweb/root/css/relationship.css new file mode 100644 index 0000000..315cee0 --- /dev/null +++ b/stemmaweb/root/css/relationship.css @@ -0,0 +1,117 @@ + diff --git a/stemmaweb/root/images/act_arrs.gif b/stemmaweb/root/images/act_arrs.gif new file mode 100644 index 0000000..d2fe22b Binary files /dev/null and b/stemmaweb/root/images/act_arrs.gif differ diff --git a/stemmaweb/root/js/jquery.mousewheel.min.js b/stemmaweb/root/js/jquery.mousewheel.min.js new file mode 100644 index 0000000..05ebb0a --- /dev/null +++ b/stemmaweb/root/js/jquery.mousewheel.min.js @@ -0,0 +1,11 @@ +/* Copyright (c) 2009 Brandon Aaron (http://brandonaaron.net) + * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) + * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. + * Thanks to: http://adomas.org/javascript-mouse-wheel/ for some pointers. + * Thanks to: Mathias Bank(http://www.mathias-bank.de) for a scope bug fix. + * + * Version: 3.0.2 + * + * Requires: 1.2.2+ + */ +(function(c){var a=["DOMMouseScroll","mousewheel"];c.event.special.mousewheel={setup:function(){if(this.addEventListener){for(var d=a.length;d;){this.addEventListener(a[--d],b,false)}}else{this.onmousewheel=b}},teardown:function(){if(this.removeEventListener){for(var d=a.length;d;){this.removeEventListener(a[--d],b,false)}}else{this.onmousewheel=null}}};c.fn.extend({mousewheel:function(d){return d?this.bind("mousewheel",d):this.trigger("mousewheel")},unmousewheel:function(d){return this.unbind("mousewheel",d)}});function b(f){var d=[].slice.call(arguments,1),g=0,e=true;f=c.event.fix(f||window.event);f.type="mousewheel";if(f.wheelDelta){g=f.wheelDelta/120}if(f.detail){g=-f.detail/3}d.unshift(f,g);return c.event.handle.apply(this,d)}})(jQuery); \ No newline at end of file diff --git a/stemmaweb/root/js/relationship.js b/stemmaweb/root/js/relationship.js index e69de29..45ca260 100644 --- a/stemmaweb/root/js/relationship.js +++ b/stemmaweb/root/js/relationship.js @@ -0,0 +1,469 @@ +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() { + // some initial scaling + var svg_element = $('#svgbasics').children('svg'); + var svg_graph = svg_element.svg().svg('get').root(); + var svg_vbwidth = svg_graph.viewBox.baseVal.width; + var svg_vbheight = svg_graph.viewBox.baseVal.height; + var scroll_padding = $('#graph_container').width(); + // (Use attr('width') to set width attr, otherwise style="width: npx;" is set.) + var svg_element_width = svg_vbwidth/svg_vbheight * parseInt(svg_element.attr('height')); + svg_element_width += scroll_padding; + svg_element.attr( 'width', svg_element_width ); + $('ellipse').attr( {stroke:'black', fill:'#fff'} ); +} + +function svgEnlargementLoaded() { + // some initial scaling + var svg_element = $('#svgenlargement').children('svg'); + var svg_graph = svg_element.svg().svg('get').root() + var svg_vbwidth = svg_graph.viewBox.baseVal.width; + var svg_vbheight = svg_graph.viewBox.baseVal.height; + var scroll_padding = $('#enlargement_container').width(); + // (Use attr('width') to set width attr, otherwise style="width: npx;" is set.) + var svg_element_width = svg_vbwidth/svg_vbheight * parseInt(svg_element.attr('height')); + svg_element_width += scroll_padding; + svg_element.attr( 'width', svg_element_width ); + $('ellipse').attr( {stroke:'black', fill:'#fff'} ); + var svg_height = parseInt( $('#svgenlargement').height() ); + scroll_enlargement_ratio = svg_height/svg_vbheight; +} + +function get_ellipse( node_id ) { + return $('#svgenlargement .node').children('title').filter( function(index) { + return $(this).text() == node_id; + }).siblings('ellipse'); +} + +function get_node_obj( node_id ) { + return get_ellipse( node_id ).data( 'node_obj' ); +} + +function get_edge( edge_id ) { + return $('#svgenlargement .edge').filter( function(index) { + return $(this).children( 'title' ).text() == $('
').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.get_id = function() { + return self.ellipse.siblings('title').text() + } + + 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' ); + first_node_g_element = $("#svgenlargement g .node" ).filter( ":first" ); + if( first_node_g_element.attr('id') !== self.get_g().attr('id') ) { self.get_g().insertBefore( first_node_g_element ) }; + } + + this.mousemove_listener = function(evt) { + self.dx = (evt.clientX - self.x) / mousemove_enlargement_ratio; + self.dy = (evt.clientY - self.y) / mousemove_enlargement_ratio; + self.move_elements(); + } + + this.mouseup_listener = function(evt) { + if( $('ellipse[fill="#ffccff"]').size() > 0 ) { + var source_node_id = self.ellipse.siblings('title').text(); + var target_node_id = $('ellipse[fill="#ffccff"]').siblings("title").text(); + $('#source_node_id').val( source_node_id ); + $('#target_node_id').val( target_node_id ); + $('#dialog-form').dialog( 'open' ); + }; + $('body').unbind('mousemove'); + $('body').unbind('mouseup'); + self.ellipse.attr( 'fill', '#fff' ); + self.ellipse.hover( self.enter_node, self.leave_node ); + 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.enter_node = function(evt) { + self.ellipse.attr( 'fill', '#ffccff' ); + } + + this.leave_node = function(evt) { + self.ellipse.attr( 'fill', '#fff' ); + } + + this.greyout_edges = function() { + $.each( self.node_elements, function(index, value) { + value.grey_out('.edge'); + }); + } + + this.ungreyout_edges = function() { + $.each( self.node_elements, function(index, value) { + value.un_grey_out('.edge'); + }); + } + + 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.update_elements = function() { + self.node_elements = node_elements_for(self.ellipse); + } + + 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 )" ); + } + this.grey_out = function(filter) { + if( this.shape.parent(filter).size() != 0 ) { + this.shape.attr({'stroke':'#e5e5e5', 'fill':'#e5e5e5'}); + } + } + this.un_grey_out = function(filter) { + if( this.shape.parent(filter).size() != 0 ) { + this.shape.attr({'stroke':'#000000', 'fill':'#000000'}); + } + } +} + +function svgpath( path_element, svg_element ) { + this.svg_element = svg_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; + } + this.grey_out = function(filter) { + if( this.svg_element.parent(filter).size() != 0 ) { + this.svg_element.attr('stroke', '#e5e5e5'); + this.svg_element.siblings('text').attr('fill', '#e5e5e5'); + } + } + this.un_grey_out = function(filter) { + if( this.svg_element.parent(filter).size() != 0 ) { + this.svg_element.attr('stroke', '#000000'); + this.svg_element.siblings('text').attr('fill', '#000000'); + } + } +} + +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( $('#svgenlargement .edge,#svgenlargement .relation').children('title'), function(index) { + title = $(this).text(); + if( edge_in_pattern.test(title) ) { + polygon = $(this).siblings('polygon'); + if( polygon.size() > 0 ) { + edge_elements.push( new svgshape( polygon ) ); + } + path_segments = $(this).siblings('path')[0].pathSegList; + edge_elements.push( new svgpath( path_segments.getItem(path_segments.numberOfItems - 1), $(this).siblings('path') ) ); + } + if( edge_out_pattern.test(title) ) { + path_segments = $(this).siblings('path')[0].pathSegList; + edge_elements.push( new svgpath( path_segments.getItem(0), $(this).siblings('path') ) ); + } + }); + return edge_elements; +} + +function relation_factory() { + var self = this; + this.color_memo = null; + //TODO: colors hard coded for now + this.temp_color = '#FFA14F'; + this.relation_colors = [ "#5CCCCC", "#67E667", "#F9FE72", "#6B90D4", "#FF7673", "#E467B3", "#AA67D5", "#8370D8", "#FFC173" ]; + + this.create_temporary = function( source_node_id, target_node_id ) { + var relation = $('#svgenlargement .relation').filter( function(index) { + var relation_id = $(this).children('title').text(); + if( ( relation_id == ( source_node_id + '->' + target_node_id ) ) || ( relation_id == ( target_node_id + '->' + source_node_id ) ) ) { + return true; + } + } ); + if( relation.size() == 0 ) { + draw_relation( source_node_id, target_node_id, self.temp_color ); + } else { + self.color_memo = relation.children('path').attr( 'stroke' ); + relation.children('path').attr( 'stroke', self.temp_color ); + } + } + this.remove_temporary = function() { + var path_element = $('#svgenlargement .relation').children('path[stroke="' + self.temp_color + '"]'); + if( self.color_memo != null ) { + path_element.attr( 'stroke', self.color_memo ); + self.color_memo = null; + } else { + path_element.parent('g').remove(); + } + } + this.create = function( source_node_id, target_node_id, color_index ) { + //TODO: Protect from (color_)index out of bound.. + var relation_color = self.relation_colors[ color_index ]; + draw_relation( source_node_id, target_node_id, relation_color ); + get_node_obj( source_node_id ).update_elements(); + get_node_obj( target_node_id ).update_elements(); + } + this.remove = function( source_node_id, target_id ) { + //TODO (When needed) + console.log( "Unsupported function node_obj.remove()." ); + } +} + +function draw_relation( source_id, target_id, relation_color ) { + var source_ellipse = get_ellipse( source_id ); + var target_ellipse = get_ellipse( target_id ); + var svg = $('#svgenlargement').children('svg').svg().svg('get'); + var path = svg.createPath(); + var sx = parseInt( source_ellipse.attr('cx') ); + var rx = parseInt( source_ellipse.attr('rx') ); + var sy = parseInt( source_ellipse.attr('cy') ); + var ex = parseInt( target_ellipse.attr('cx') ); + var ey = parseInt( target_ellipse.attr('cy') ); + var relation = svg.group( $("#svgenlargement svg g"), {'class':'relation'} ); + svg.title( relation, source_id + '->' + target_id ); + 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}); + var relation_element = $('#svgenlargement .relation').filter( ':last' ); + relation_element.insertBefore( $('#svgenlargement g g').filter(':first') ); +} + +$(document).ready(function () { + + relation_manager = new relation_factory(); + + scroll_ratio = $('#enlargement').height() / $('#graph').height(); + + $('#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 ) { + if ( $('#update_workspace_button').data('locked') != true ) { + var scroll_left = $(this).data('scrollLeft') + $(this).data('x') - event.clientX; + this.scrollLeft = scroll_left; + var enlarged_scroll_left = scroll_left * scroll_ratio; + $('#enlargement').scrollLeft( enlarged_scroll_left ); + color_enlarged(); + } + } + }).mousewheel(function (event, delta) { + if ( $('#update_workspace_button').data('locked') != true ) { + var scroll_left = delta * 30; + this.scrollLeft -= scroll_left; + var enlarged_scroll_left = $('#enlargement').scrollLeft(); + enlarged_scroll_left -= (scroll_left * scroll_ratio); + $('#enlargement').scrollLeft( enlarged_scroll_left ); + color_enlarged(); + } + }).css({ + 'overflow' : 'hidden', + 'cursor' : '-moz-grab' + }); + + + $( "#dialog-form" ).dialog({ + autoOpen: false, + height: 270, + width: 290, + modal: true, + buttons: { + "Ok": function() { + $('#status').empty(); + form_values = $('#collapse_node_form').serialize() + ncpath = window.location.pathname + '/relationship'; + var jqjson = $.post( ncpath, form_values, function(data) { + $.each( data, function(item, source_target) { + relation_manager.create( source_target[0], source_target[1], $('#rel_type').attr('selectedIndex') ); + }); + relation_manager.remove_temporary(); + $( "#dialog-form" ).dialog( "close" ); + }, 'json'); + }, + Cancel: function() { + relation_manager.remove_temporary(); + $( this ).dialog( "close" ); + } + }, + create: function(event, ui) { + $(this).data( 'relation_drawn', false ); + //TODO? Err handling? + var jqjson = $.getJSON( 'relationship_definition', function(data) { + var types = data.types.sort(); + $.each( types, function(index, value) { + $('#rel_type').append( $('