add relationship deletion functionality
[scpubgit/stemmatology.git] / lib / Text / Tradition / Collation.pm
index 39dd83e..cc197ff 100644 (file)
@@ -27,6 +27,7 @@ has 'relations' => (
        handles => {
                relationships => 'relationships',
                related_readings => 'related_readings',
+               del_relationship => 'del_relationship',
        },
        writer => '_set_relations',
        );
@@ -451,16 +452,7 @@ sub as_svg {
     print $dotfile $self->as_dot( $opts );
     push( @cmd, $dotfile->filename );
     run( \@cmd, ">", binary(), \$svg );
-    # HACK part 3 - remove silent node+edge
-    my $parser = XML::LibXML->new();
-    my $svgdom = $parser->parse_string( $svg );
-    my $xpc = XML::LibXML::XPathContext->new( $svgdom->documentElement );
-    $xpc->registerNs( 'svg', 'http://www.w3.org/2000/svg' );
-    my @hacknodes = $xpc->findnodes( '//svg:g[contains(child::svg:title, "#SILENT#")]' );
-    foreach my $h ( @hacknodes ) {
-       $h->parentNode->removeChild( $h );
-    }
-    return decode_utf8( $svgdom->toString() );
+    return decode_utf8( $svg );
 }
 
 
@@ -486,8 +478,6 @@ sub as_dot {
     my $startrank = $opts->{'from'} if $opts;
     my $endrank = $opts->{'to'} if $opts;
     my $color_common = $opts->{'color_common'} if $opts;
-    my $STRAIGHTENHACK = !$startrank && !$endrank && $self->end->rank 
-       && $self->end->rank > 100;
     
     # Check the arguments
     if( $startrank ) {
@@ -509,7 +499,7 @@ sub as_dot {
        'bgcolor' => 'none',
        );
     my %node_attrs = (
-       'fontsize' => 11,
+       'fontsize' => 14,
        'fillcolor' => 'white',
        'style' => 'filled',
        'shape' => 'ellipse'
@@ -531,13 +521,13 @@ sub as_dot {
        if( $endrank ) {
                $dot .= "\t\"#SUBEND#\" [ label=\"...\" ];\n";  
        }
-       if( $STRAIGHTENHACK ) {
-               ## HACK part 1
-               $dot .= "\tsubgraph { rank=same \"#START#\" \"#SILENT#\" }\n";  
-               $dot .= "\t\"#SILENT#\" [ color=white,penwidth=0,label=\"\" ];"
-       }
+
        my %used;  # Keep track of the readings that actually appear in the graph
-    foreach my $reading ( $self->readings ) {
+       # Sort the readings by rank if we have ranks; this speeds layout.
+       my @all_readings = $self->end->has_rank 
+               ? sort { $a->rank <=> $b->rank } $self->readings
+               : $self->readings;
+    foreach my $reading ( @all_readings ) {
        # Only output readings within our rank range.
        next if $startrank && $reading->rank < $startrank;
        next if $endrank && $reading->rank > $endrank;
@@ -552,22 +542,33 @@ sub as_dot {
         $dot .= sprintf( "\t\"%s\" %s;\n", $reading->id, _dot_attr_string( $rattrs ) );
     }
     
-       # Add the real edges
+       # Add the real edges. Need to weight one edge per rank jump, in a
+       # continuous line.
+       my $weighted = $self->_add_edge_weights;
     my @edges = $self->paths;
        my( %substart, %subend );
     foreach my $edge ( @edges ) {
        # Do we need to output this edge?
        if( $used{$edge->[0]} && $used{$edge->[1]} ) {
-               my $label = $self->path_display_label( $self->path_witnesses( $edge ) );
+               my $label = $self->_path_display_label( $self->path_witnesses( $edge ) );
                        my $variables = { %edge_attrs, 'label' => $label };
+                       
                        # Account for the rank gap if necessary
-                       if( $self->reading( $edge->[1] )->has_rank 
-                               && $self->reading( $edge->[0] )->has_rank
-                               && $self->reading( $edge->[1] )->rank 
-                               - $self->reading( $edge->[0] )->rank > 1 ) {
-                               $variables->{'minlen'} = $self->reading( $edge->[1] )->rank 
-                               - $self->reading( $edge->[0] )->rank;
+                       my $rank0 = $self->reading( $edge->[0] )->rank
+                               if $self->reading( $edge->[0] )->has_rank;
+                       my $rank1 = $self->reading( $edge->[1] )->rank
+                               if $self->reading( $edge->[1] )->has_rank;
+                       if( defined $rank0 && defined $rank1 && $rank1 - $rank0 > 1 ) {
+                               $variables->{'minlen'} = $rank1 - $rank0;
+                       }
+                       
+                       # Add the calculated edge weights
+                       if( exists $weighted->{$edge->[0]} 
+                               && $weighted->{$edge->[0]} eq $edge->[1] ) {
+                               # $variables->{'color'} = 'red';
+                               $variables->{'weight'} = 3.0;
                        }
+
                        # EXPERIMENTAL: make edge width reflect no. of witnesses
                        my $extrawidth = scalar( $self->path_witnesses( $edge ) ) * 0.2;
                        $variables->{'penwidth'} = $extrawidth + 0.8; # gives 1 for a single wit
@@ -583,22 +584,18 @@ sub as_dot {
     }
     # Add substitute start and end edges if necessary
     foreach my $node ( keys %substart ) {
-       my $witstr = $self->path_display_label ( $self->reading_witnesses( $self->reading( $node ) ) );
+       my $witstr = $self->_path_display_label ( $self->reading_witnesses( $self->reading( $node ) ) );
        my $variables = { %edge_attrs, 'label' => $witstr };
         my $varopts = _dot_attr_string( $variables );
         $dot .= "\t\"#SUBSTART#\" -> \"$node\" $varopts;";
        }
     foreach my $node ( keys %subend ) {
-       my $witstr = $self->path_display_label ( $self->reading_witnesses( $self->reading( $node ) ) );
+       my $witstr = $self->_path_display_label ( $self->reading_witnesses( $self->reading( $node ) ) );
        my $variables = { %edge_attrs, 'label' => $witstr };
         my $varopts = _dot_attr_string( $variables );
         $dot .= "\t\"$node\" -> \"#SUBEND#\" $varopts;";
        }
-       # HACK part 2
-       if( $STRAIGHTENHACK ) {
-               $dot .= "\t\"#END#\" -> \"#SILENT#\" [ color=white,penwidth=0 ];\n";
-       }
-       
+
     $dot .= "}\n";
     return $dot;
 }
@@ -613,6 +610,34 @@ sub _dot_attr_string {
        return( '[ ' . join( ', ', @attrs ) . ' ]' );
 }
 
+sub _add_edge_weights {
+       my $self = shift;
+       # Walk the graph from START to END, choosing the successor node with
+       # the largest number of witness paths each time.
+       my $weighted = {};
+       my $curr = $self->start->id;
+       while( $curr ne $self->end->id ) {
+               my @succ = sort { $self->path_witnesses( $curr, $a )
+                                                       <=> $self->path_witnesses( $curr, $b ) } 
+                       $self->sequence->successors( $curr );
+               my $next = pop @succ;
+               # Try to avoid lacunae in the weighted path.
+               while( $self->reading( $next )->is_lacuna && @succ ) {
+                       $next = pop @succ;
+               }
+               $weighted->{$curr} = $next;
+               $curr = $next;
+       }
+       return $weighted;       
+}
+
+=head2 path_witnesses( $edge )
+
+Returns the list of sigils whose witnesses are associated with the given edge.
+The edge can be passed as either an array or an arrayref of ( $source, $target ).
+
+=cut
+
 sub path_witnesses {
        my( $self, @edge ) = @_;
        # If edge is an arrayref, cope.
@@ -624,7 +649,7 @@ sub path_witnesses {
        return @wits;
 }
 
-sub path_display_label {
+sub _path_display_label {
        my $self = shift;
        my @wits = sort @_;
        my $maj = scalar( $self->tradition->witnesses ) * 0.6;
@@ -815,7 +840,7 @@ sub as_graphml {
        }
        
        # Add the relationship graph to the XML
-       $self->relations->as_graphml( $graphml_ns, $root, \%node_hash, 
+       $self->relations->_as_graphml( $graphml_ns, $root, \%node_hash, 
                $node_data_keys{'id'}, \%edge_data_keys );
 
     # Save and return the thing
@@ -1420,16 +1445,16 @@ is( $c->common_successor( 'n21', 'n26' )->id,
 sub common_predecessor {
        my $self = shift;
        my( $r1, $r2 ) = $self->_objectify_args( @_ );
-       return $self->common_in_path( $r1, $r2, 'predecessors' );
+       return $self->_common_in_path( $r1, $r2, 'predecessors' );
 }
 
 sub common_successor {
        my $self = shift;
        my( $r1, $r2 ) = $self->_objectify_args( @_ );
-       return $self->common_in_path( $r1, $r2, 'successors' );
+       return $self->_common_in_path( $r1, $r2, 'successors' );
 }
 
-sub common_in_path {
+sub _common_in_path {
        my( $self, $r1, $r2, $dir ) = @_;
        my $iter = $r1->rank > $r2->rank ? $r1->rank : $r2->rank;
        $iter = $self->end->rank - $iter if $dir eq 'successors';
@@ -1464,10 +1489,12 @@ sub throw {
 no Moose;
 __PACKAGE__->meta->make_immutable;
 
-=head1 BUGS / TODO
+=head1 LICENSE
 
-=over
+This package is free software and is provided "as is" without express
+or implied warranty.  You can redistribute it and/or modify it under
+the same terms as Perl itself.
 
-=item * Get rid of $backup in reading_sequence
+=head1 AUTHOR
 
-=back
+Tara L Andrews E<lt>aurum@cpan.orgE<gt>