we can parse our own graph output now
[scpubgit/stemmatology.git] / lib / Text / Tradition / Collation.pm
index 8489ddf..f66e902 100644 (file)
@@ -4,6 +4,7 @@ use Graph::Easy;
 use IPC::Run qw( run binary );
 use Text::Tradition::Collation::Reading;
 use Text::Tradition::Collation::Path;
+use XML::LibXML;
 use Moose;
 
 has 'graph' => (
@@ -18,6 +19,7 @@ has 'graph' => (
        path => 'edge',
        readings => 'nodes',
        paths => 'edges',
+       relationships => 'edges',
     },
     default => sub { Graph::Easy->new( undirected => 0 ) },
     );
@@ -35,16 +37,9 @@ has 'svg' => (
     predicate => 'has_svg',
     );
 
-has 'graphviz' => (
-    is => 'ro',
-    isa => 'Str',
-    writer => '_save_graphviz',
-    predicate => 'has_graphviz',
-    );
-
 has 'graphml' => (
     is => 'ro',
-    isa => 'XML::LibXML::Document',
+    isa => 'Str',
     writer => '_save_graphml',
     predicate => 'has_graphml',
     );
@@ -80,6 +75,12 @@ has 'linear' => (
     default => 1,
     );
 
+has 'ac_label' => (
+    is => 'rw',
+    isa => 'Str',
+    default => ' (a.c.)',
+    );
+
 
 # The collation can be created two ways:
 # 1. Collate a set of witnesses (with CollateX I guess) and process
@@ -124,7 +125,7 @@ around add_path => sub {
     $target = $self->reading( $target )
        unless ref( $target ) eq 'Text::Tradition::Collation::Reading';
     foreach my $path ( $source->edges_to( $target ) ) {
-       if( $path->label eq $wit ) {
+       if( $path->label eq $wit && $path->class eq 'edge.path' ) {
            return;
        }
     }
@@ -137,7 +138,14 @@ around paths => sub {
     my $orig = shift;
     my $self = shift;
 
-    my @result = grep { $_->class eq 'path' } $self->$orig( @_ );
+    my @result = grep { $_->class eq 'edge.path' } $self->$orig( @_ );
+    return @result;
+};
+
+around relationships => sub {
+    my $orig = shift;
+    my $self = shift;
+    my @result = grep { $_->class eq 'edge.relationship' } $self->$orig( @_ );
     return @result;
 };
 
@@ -165,6 +173,19 @@ sub has_path {
 
 sub add_relationship {
     my( $self, $type, $source, $target, $global ) = @_;
+
+    # Make sure there is not another relationship between these two
+    # readings already
+    $source = $self->reading( $source )
+       unless ref( $source ) eq 'Text::Tradition::Collation::Reading';
+    $target = $self->reading( $target )
+       unless ref( $target ) eq 'Text::Tradition::Collation::Reading';
+    foreach my $rel ( $source->edges_to( $target ) ) {
+       if( $rel->label eq $type && $rel->class eq 'edge.relationship' ) {
+           return;
+       }
+    }
+
     my $rel = Text::Tradition::Collation::Relationship->new(
            'sort' => $type,
            'global' => $global,
@@ -216,18 +237,54 @@ sub as_svg {
     return $self->svg if $self->has_svg;
     
     $self->collapse_graph_paths();
-    $self->_save_graphviz( $self->graph->as_graphviz() )
-       unless( $self->has_graphviz && !$recalc );
     
     my @cmd = qw/dot -Tsvg/;
     my( $svg, $err );
-    my $in = $self->graphviz;
+    my $in = $self->as_dot();
     run( \@cmd, \$in, ">", binary(), \$svg );
-    $self->{'svg'} = $svg;
+    $self->_save_svg( $svg );
     $self->expand_graph_paths();
     return $svg;
 }
 
+=item B<as_dot>
+
+print $graph->as_dot( $view, $recalculate );
+
+Returns a string that is the collation graph expressed in dot
+(i.e. GraphViz) format.  The 'view' argument determines what kind of
+graph is produced.
+    * 'path': a graph of witness paths through the collation (DEFAULT)
+    * 'relationship': a graph of how collation readings relate to 
+      each other
+
+=cut
+
+sub as_dot {
+    my( $self, $view ) = @_;
+    $view = 'path' unless $view;
+    # TODO consider making some of these things configurable
+    my $dot = sprintf( "digraph %s {\n", $self->tradition->name );
+    $dot .= "\tedge [ arrowhead=open ];\n";
+    $dot .= "\tgraph [ rankdir=LR ];\n";
+    $dot .= sprintf( "\tnode [ fontsize=%d, fillcolor=%s, style=%s, shape=%s ];\n",
+                    11, "white", "filled", $self->graph->get_attribute( 'node', 'shape' ) );
+
+    foreach my $reading ( $self->readings ) {
+       next if $reading->name eq $reading->label;
+       $dot .= sprintf( "\t\"%s\" [ label=\"%s\" ]\n", $reading->name, $reading->label );
+    }
+
+    my @edges = $view eq 'relationship' ? $self->relationships : $self->paths;
+    foreach my $edge ( @edges ) {
+       $dot .= sprintf( "\t\"%s\" -> \"%s\" [ color=\"%s\", fontcolor=\"%s\", label=\"%s\" ]\n",
+                        $edge->from->name, $edge->to->name, '#000000', '#000000', $edge->label );
+    }
+
+    $dot .= "}\n";
+    return $dot;
+}
+
 =item B<as_graphml>
 
 print $graph->as_graphml( $recalculate )
@@ -257,52 +314,56 @@ sub as_graphml {
     $root->setAttributeNS( $xsi_ns, 'schemaLocation', $graphml_schema );
 
     # Add the data keys for nodes
-    my @node_data = ( 'name', 'token', 'identical', 'position' );
-    foreach my $ndi ( 0 .. $#node_data ) {
+    my @node_data = ( 'name', 'reading', 'identical', 'position' );
+    # HACKY HACKY HACK Relationship data
+    my %node_data_keys;
+    my $ndi = 0;
+    foreach my $datum ( @node_data ) {
+       $node_data_keys{$datum} = 'dn'.$ndi++;
        my $key = $root->addNewChild( $graphml_ns, 'key' );
-       $key->setAttribute( 'attr.name', $node_data[$ndi] );
+       $key->setAttribute( 'attr.name', $datum );
        $key->setAttribute( 'attr.type', 'string' );
        $key->setAttribute( 'for', 'node' );
-       $key->setAttribute( 'id', 'd'.$ndi );
+       $key->setAttribute( 'id', $node_data_keys{$datum} );
     }
 
-    # Add the data keys for edges
-    my %wit_hash;
-    my $wit_ctr = 0;
-    foreach my $wit ( @{$self->tradition->witnesses} ) {
-       my $wit_key = 'w' . $wit_ctr++;
-       $wit_hash{$wit} = $wit_key;
+    # Add the data keys for edges, i.e. witnesses
+    my $edi = 0;
+    my %edge_data_keys;
+    foreach my $edge_key( qw/ witness_main witness_ante_corr relationship / ) {
+       $edge_data_keys{$edge_key} = 'de'.$edi++;
        my $key = $root->addNewChild( $graphml_ns, 'key' );
-       $key->setAttribute( 'attr.name', $wit );
+       $key->setAttribute( 'attr.name', $edge_key );
        $key->setAttribute( 'attr.type', 'string' );
        $key->setAttribute( 'for', 'edge' );
-       $key->setAttribute( 'id', $wit_key );
+       $key->setAttribute( 'id', $edge_data_keys{$edge_key} );
     }
-
+    
     # Add the graph, its nodes, and its edges
-    $self->collapse_graph_edges();
     my $graph = $root->addNewChild( $graphml_ns, 'graph' );
     $graph->setAttribute( 'edgedefault', 'directed' );
     $graph->setAttribute( 'id', 'g0' ); # TODO make this meaningful
     $graph->setAttribute( 'parse.edgeids', 'canonical' );
-    $graph->setAttribute( 'parse.edges', $self->edges() );
+    $graph->setAttribute( 'parse.edges', scalar($self->paths) );
     $graph->setAttribute( 'parse.nodeids', 'canonical' );
-    $graph->setAttribute( 'parse.nodes', $self->nodes() );
+    $graph->setAttribute( 'parse.nodes', scalar($self->readings) );
     $graph->setAttribute( 'parse.order', 'nodesfirst' );
 
     my $node_ctr = 0;
     my %node_hash;
-    foreach my $n ( $self->readings ) {
+    foreach my $n ( sort { $a->name cmp $b->name } $self->readings ) {
        my %this_node_data = ();
-       foreach my $ndi ( 0 .. $#node_data ) {
-           my $value;
-           $this_node_data{'d'.$ndi} = $n->name if $node_data[$ndi] eq 'name';
-           $this_node_data{'d'.$ndi} = $n->label 
-               if $node_data[$ndi] eq 'token';
-           $this_node_data{'d'.$ndi} = $n->primary->name if $n->has_primary;
-           $this_node_data{'d'.$ndi} = 
-               $self->{'positions'}->node_position( $n )
-               if $node_data[$ndi] eq 'position';
+       foreach my $datum ( @node_data ) {
+           my $key = $node_data_keys{$datum};
+           if( $datum eq 'name' ) {
+               $this_node_data{$key} = $n->name;
+           } elsif( $datum eq 'reading' ) {
+               $this_node_data{$key} = $n->label;
+           } elsif( $datum eq 'identical' && $n->has_primary ) {
+               $this_node_data{$key} = $n->primary->name;
+           } elsif( $datum eq 'position' ) {
+               $this_node_data{$key} = $n->position;
+           }
        }
        my $node_el = $graph->addNewChild( $graphml_ns, 'node' );
        my $node_xmlid = 'n' . $node_ctr++;
@@ -312,25 +373,51 @@ sub as_graphml {
        foreach my $dk ( keys %this_node_data ) {
            my $d_el = $node_el->addNewChild( $graphml_ns, 'data' );
            $d_el->setAttribute( 'key', $dk );
-           $d_el->appendTextChild( $this_node_data{$dk} );
+           $d_el->appendText( $this_node_data{$dk} );
        }
     }
 
-    foreach my $e ( $self->paths() ) {
-       my( $name, $from, $to ) = ( $e->name,
-                                   $node_hash{ $e->from()->name() },
-                                   $node_hash{ $e->to()->name() } );
+    # Add the path edges
+    my $edge_ctr = 0;
+    foreach my $e ( sort { $a->from->name cmp $b->from->name } $self->graph->edges() ) {
+       my( $name, $from, $to ) = ( 'e'.$edge_ctr++,
+                                   $node_hash{ $e->from->name() },
+                                   $node_hash{ $e->to->name() } );
        my $edge_el = $graph->addNewChild( $graphml_ns, 'edge' );
        $edge_el->setAttribute( 'source', $from );
        $edge_el->setAttribute( 'target', $to );
        $edge_el->setAttribute( 'id', $name );
-       # TODO Got to add the witnesses
+       if( $e->class() eq 'edge.path' ) {
+           # It's a witness path, so add the witness
+           my $base = $e->label;
+           my $key = $edge_data_keys{'witness_main'};
+           # TODO kind of hacky
+           if( $e->label =~ /^(.*?)\s+(\(a\.c\.\))$/ ) {
+               $base = $1;
+               $key = $edge_data_keys{'witness_ante_corr'};
+           }
+           my $wit_el = $edge_el->addNewChild( $graphml_ns, 'data' );
+           $wit_el->setAttribute( 'key', $key );
+           $wit_el->appendText( $base );
+       } else {
+           # It's a relationship
+           my $rel_el = $edge_el->addNewChild( $graphml_ns, 'data' );
+           $rel_el->setAttribute( 'key', $edge_data_keys{'relationship'} );
+           $rel_el->appendText( $e->label() );
+       }
     }
 
     # Return the thing
-    $self->_save_graphml( $graphml );
-    $self->expand_graph_edges();
-    return $graphml;
+    $self->_save_graphml( $graphml->toString(1) );
+    return $graphml->toString(1);
+}
+
+sub _make_xml_attr {
+    my $str = shift;
+    $str =~ s/\s/_/g;
+    $str =~ s/\W//g;
+    $str =~ "a$str" if $str =~ /^\d/;
+    return $str;
 }
 
 sub collapse_graph_paths {
@@ -345,19 +432,22 @@ sub collapse_graph_paths {
 
     # Don't list out every witness if we have more than half to list.
     my $majority = int( scalar( @{$self->tradition->witnesses} ) / 2 ) + 1;
+    # But don't compress if there are only a few witnesses.
+    $majority = 4 if $majority < 4;
     foreach my $node( $self->readings ) {
        my $newlabels = {};
        # We will visit each node, so we only look ahead.
-       foreach my $path ( $node->outgoing() ) {
-           add_hash_entry( $newlabels, $path->to->name, $path->name );
-           $self->del_path( $path );
+       foreach my $edge ( $node->outgoing() ) {
+           next unless $edge->class eq 'edge.path';
+           add_hash_entry( $newlabels, $edge->to->name, $edge->name );
+           $self->del_path( $edge );
        }
 
        foreach my $newdest ( keys %$newlabels ) {
            my $label;
            my @compressed_wits = ();
            if( @{$newlabels->{$newdest}} < $majority ) {
-               $label = join( ', ', @{$newlabels->{$newdest}} );
+               $label = join( ', ', sort( @{$newlabels->{$newdest}} ) );
            } else {
                ## TODO FIX THIS HACK
                my @aclabels;
@@ -368,7 +458,7 @@ sub collapse_graph_paths {
                        push( @compressed_wits, $wit );
                    }
                }
-               $label = join( ', ', 'majority', @aclabels );
+               $label = join( ', ', 'majority', sort( @aclabels ) );
            }
            
            my $newpath = 
@@ -391,7 +481,6 @@ sub expand_graph_paths {
     return unless $self->collapsed;
     
     print STDERR "Expanding witness paths in graph...\n";
-    $DB::single = 1;
     foreach my $path( $self->paths ) {
        my $from = $path->from;
        my $to = $path->to;