we can parse our own graph output now
[scpubgit/stemmatology.git] / lib / Text / Tradition / Collation.pm
1 package Text::Tradition::Collation;
2
3 use Graph::Easy;
4 use IPC::Run qw( run binary );
5 use Text::Tradition::Collation::Reading;
6 use Text::Tradition::Collation::Path;
7 use XML::LibXML;
8 use Moose;
9
10 has 'graph' => (
11     is => 'ro',
12     isa => 'Graph::Easy',
13     handles => {
14         add_reading => 'add_node',
15         del_reading => 'del_node',
16         add_path => 'add_edge',
17         del_path => 'del_edge',
18         reading => 'node',
19         path => 'edge',
20         readings => 'nodes',
21         paths => 'edges',
22         relationships => 'edges',
23     },
24     default => sub { Graph::Easy->new( undirected => 0 ) },
25     );
26                 
27
28 has 'tradition' => (
29     is => 'rw',
30     isa => 'Text::Tradition',
31     );
32
33 has 'svg' => (
34     is => 'ro',
35     isa => 'Str',
36     writer => '_save_svg',
37     predicate => 'has_svg',
38     );
39
40 has 'graphml' => (
41     is => 'ro',
42     isa => 'Str',
43     writer => '_save_graphml',
44     predicate => 'has_graphml',
45     );
46
47 # Keeps track of the lemmas within the collation.  At most one lemma
48 # per position in the graph.
49 has 'lemmata' => (
50     is => 'ro',
51     isa => 'HashRef[Maybe[Str]]',
52     default => sub { {} },
53     );
54
55 has 'wit_list_separator' => (
56     is => 'rw',
57     isa => 'Str',
58     default => ', ',
59     );
60
61 has 'baselabel' => (
62     is => 'rw',
63     isa => 'Str',
64     default => 'base text',
65     );
66
67 has 'collapsed' => (
68     is => 'rw',
69     isa => 'Bool',
70     );
71
72 has 'linear' => (
73     is => 'rw',
74     isa => 'Bool',
75     default => 1,
76     );
77
78 has 'ac_label' => (
79     is => 'rw',
80     isa => 'Str',
81     default => ' (a.c.)',
82     );
83
84
85 # The collation can be created two ways:
86 # 1. Collate a set of witnesses (with CollateX I guess) and process
87 #    the results as in 2.
88 # 2. Read a pre-prepared collation in one of a variety of formats,
89 #    and make the graph from that.
90
91 # The graph itself will (for now) be immutable, and the positions
92 # within the graph will also be immutable.  We need to calculate those
93 # positions upon graph construction.  The equivalences between graph
94 # nodes will be mutable, entirely determined by the user (or possibly
95 # by some semantic pre-processing provided by the user.)  So the
96 # constructor should just make an empty equivalences object.  The
97 # constructor will also need to make the witness objects, if we didn't
98 # come through option 1.
99
100 sub BUILD {
101     my( $self, $args ) = @_;
102     $self->graph->use_class('node', 'Text::Tradition::Collation::Reading');
103     $self->graph->use_class('edge', 'Text::Tradition::Collation::Path');
104
105     # Pass through any graph-specific options.
106     my $shape = exists( $args->{'shape'} ) ? $args->{'shape'} : 'ellipse';
107     $self->graph->set_attribute( 'node', 'shape', $shape );
108 }
109
110 # Wrapper around add_path 
111
112 around add_path => sub {
113     my $orig = shift;
114     my $self = shift;
115
116     # Make sure there are three arguments
117     unless( @_ == 3 ) {
118         warn "Call add_path with args source, target, witness";
119         return;
120     }
121     # Make sure the proposed path does not yet exist
122     my( $source, $target, $wit ) = @_;
123     $source = $self->reading( $source )
124         unless ref( $source ) eq 'Text::Tradition::Collation::Reading';
125     $target = $self->reading( $target )
126         unless ref( $target ) eq 'Text::Tradition::Collation::Reading';
127     foreach my $path ( $source->edges_to( $target ) ) {
128         if( $path->label eq $wit && $path->class eq 'edge.path' ) {
129             return;
130         }
131     }
132     # Do the deed
133     $self->$orig( @_ );
134 };
135
136 # Wrapper around paths
137 around paths => sub {
138     my $orig = shift;
139     my $self = shift;
140
141     my @result = grep { $_->class eq 'edge.path' } $self->$orig( @_ );
142     return @result;
143 };
144
145 around relationships => sub {
146     my $orig = shift;
147     my $self = shift;
148     my @result = grep { $_->class eq 'edge.relationship' } $self->$orig( @_ );
149     return @result;
150 };
151
152 # Wrapper around merge_nodes
153
154 sub merge_readings {
155     my $self = shift;
156     my $first_node = shift;
157     my $second_node = shift;
158     $first_node->merge_from( $second_node );
159     unshift( @_, $first_node, $second_node );
160     return $self->graph->merge_nodes( @_ );
161 }
162
163 # Extra graph-alike utility
164 sub has_path {
165     my( $self, $source, $target, $label ) = @_;
166     my @paths = $source->edges_to( $target );
167     my @relevant = grep { $_->label eq $label } @paths;
168     return scalar @paths;
169 }
170
171 ## Dealing with relationships between readings.  This is a different
172 ## sort of graph edge.
173
174 sub add_relationship {
175     my( $self, $type, $source, $target, $global ) = @_;
176
177     # Make sure there is not another relationship between these two
178     # readings already
179     $source = $self->reading( $source )
180         unless ref( $source ) eq 'Text::Tradition::Collation::Reading';
181     $target = $self->reading( $target )
182         unless ref( $target ) eq 'Text::Tradition::Collation::Reading';
183     foreach my $rel ( $source->edges_to( $target ) ) {
184         if( $rel->label eq $type && $rel->class eq 'edge.relationship' ) {
185             return;
186         }
187     }
188
189     my $rel = Text::Tradition::Collation::Relationship->new(
190             'sort' => $type,
191             'global' => $global,
192             'orig_relation' => [ $source, $target ],
193     );
194     print STDERR sprintf( "Setting relationship %s between readings %s (%s)"
195                           . " and %s (%s)\n", $type, 
196                           $source->label, $source->name,
197                           $target->label, $target->name );
198     $self->graph->add_edge( $source, $target, $rel );
199     if( $global ) {
200         # Look for all readings with the source label, and if there are
201         # colocated readings with the target label, join them too.
202         foreach my $r ( $self->readings() ) {
203             next unless $r->label eq $source->label;
204             my @colocated = grep { $_->label eq $target->label }
205                 $self->same_position_as( $r );
206             if( @colocated ) {
207                 warn "Multiple readings with same label at same position!"
208                     if @colocated > 1;
209                 my $dup_rel = Text::Tradition::Collation::Relationship->new(
210                     'sort' => $type,
211                     'global' => $global,
212                     'orig_relation' => [ $source, $target ],
213                     );
214                 $self->graph->add_edge( $r, $colocated[0], $dup_rel );
215             }
216         }
217     }
218 }
219
220 =head2 Output method(s)
221
222 =over
223
224 =item B<as_svg>
225
226 print $graph->as_svg( $recalculate );
227
228 Returns an SVG string that represents the graph.  Uses GraphViz to do
229 this, because Graph::Easy doesn\'t cope well with long graphs. Unless
230 $recalculate is passed (and is a true value), the method will return a
231 cached copy of the SVG after the first call to the method.
232
233 =cut
234
235 sub as_svg {
236     my( $self, $recalc ) = @_;
237     return $self->svg if $self->has_svg;
238     
239     $self->collapse_graph_paths();
240     
241     my @cmd = qw/dot -Tsvg/;
242     my( $svg, $err );
243     my $in = $self->as_dot();
244     run( \@cmd, \$in, ">", binary(), \$svg );
245     $self->_save_svg( $svg );
246     $self->expand_graph_paths();
247     return $svg;
248 }
249
250 =item B<as_dot>
251
252 print $graph->as_dot( $view, $recalculate );
253
254 Returns a string that is the collation graph expressed in dot
255 (i.e. GraphViz) format.  The 'view' argument determines what kind of
256 graph is produced.
257     * 'path': a graph of witness paths through the collation (DEFAULT)
258     * 'relationship': a graph of how collation readings relate to 
259       each other
260
261 =cut
262
263 sub as_dot {
264     my( $self, $view ) = @_;
265     $view = 'path' unless $view;
266     # TODO consider making some of these things configurable
267     my $dot = sprintf( "digraph %s {\n", $self->tradition->name );
268     $dot .= "\tedge [ arrowhead=open ];\n";
269     $dot .= "\tgraph [ rankdir=LR ];\n";
270     $dot .= sprintf( "\tnode [ fontsize=%d, fillcolor=%s, style=%s, shape=%s ];\n",
271                      11, "white", "filled", $self->graph->get_attribute( 'node', 'shape' ) );
272
273     foreach my $reading ( $self->readings ) {
274         next if $reading->name eq $reading->label;
275         $dot .= sprintf( "\t\"%s\" [ label=\"%s\" ]\n", $reading->name, $reading->label );
276     }
277
278     my @edges = $view eq 'relationship' ? $self->relationships : $self->paths;
279     foreach my $edge ( @edges ) {
280         $dot .= sprintf( "\t\"%s\" -> \"%s\" [ color=\"%s\", fontcolor=\"%s\", label=\"%s\" ]\n",
281                          $edge->from->name, $edge->to->name, '#000000', '#000000', $edge->label );
282     }
283
284     $dot .= "}\n";
285     return $dot;
286 }
287
288 =item B<as_graphml>
289
290 print $graph->as_graphml( $recalculate )
291
292 Returns a GraphML representation of the collation graph, with
293 transposition information and position information. Unless
294 $recalculate is passed (and is a true value), the method will return a
295 cached copy of the SVG after the first call to the method.
296
297 =cut
298
299 sub as_graphml {
300     my( $self, $recalc ) = @_;
301     return $self->graphml if $self->has_graphml;
302
303     # Some namespaces
304     my $graphml_ns = 'http://graphml.graphdrawing.org/xmlns';
305     my $xsi_ns = 'http://www.w3.org/2001/XMLSchema-instance';
306     my $graphml_schema = 'http://graphml.graphdrawing.org/xmlns ' .
307         'http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd';
308
309     # Create the document and root node
310     my $graphml = XML::LibXML->createDocument( "1.0", "UTF-8" );
311     my $root = $graphml->createElementNS( $graphml_ns, 'graphml' );
312     $graphml->setDocumentElement( $root );
313     $root->setNamespace( $xsi_ns, 'xsi', 0 );
314     $root->setAttributeNS( $xsi_ns, 'schemaLocation', $graphml_schema );
315
316     # Add the data keys for nodes
317     my @node_data = ( 'name', 'reading', 'identical', 'position' );
318     # HACKY HACKY HACK Relationship data
319     my %node_data_keys;
320     my $ndi = 0;
321     foreach my $datum ( @node_data ) {
322         $node_data_keys{$datum} = 'dn'.$ndi++;
323         my $key = $root->addNewChild( $graphml_ns, 'key' );
324         $key->setAttribute( 'attr.name', $datum );
325         $key->setAttribute( 'attr.type', 'string' );
326         $key->setAttribute( 'for', 'node' );
327         $key->setAttribute( 'id', $node_data_keys{$datum} );
328     }
329
330     # Add the data keys for edges, i.e. witnesses
331     my $edi = 0;
332     my %edge_data_keys;
333     foreach my $edge_key( qw/ witness_main witness_ante_corr relationship / ) {
334         $edge_data_keys{$edge_key} = 'de'.$edi++;
335         my $key = $root->addNewChild( $graphml_ns, 'key' );
336         $key->setAttribute( 'attr.name', $edge_key );
337         $key->setAttribute( 'attr.type', 'string' );
338         $key->setAttribute( 'for', 'edge' );
339         $key->setAttribute( 'id', $edge_data_keys{$edge_key} );
340     }
341     
342     # Add the graph, its nodes, and its edges
343     my $graph = $root->addNewChild( $graphml_ns, 'graph' );
344     $graph->setAttribute( 'edgedefault', 'directed' );
345     $graph->setAttribute( 'id', 'g0' ); # TODO make this meaningful
346     $graph->setAttribute( 'parse.edgeids', 'canonical' );
347     $graph->setAttribute( 'parse.edges', scalar($self->paths) );
348     $graph->setAttribute( 'parse.nodeids', 'canonical' );
349     $graph->setAttribute( 'parse.nodes', scalar($self->readings) );
350     $graph->setAttribute( 'parse.order', 'nodesfirst' );
351
352     my $node_ctr = 0;
353     my %node_hash;
354     foreach my $n ( sort { $a->name cmp $b->name } $self->readings ) {
355         my %this_node_data = ();
356         foreach my $datum ( @node_data ) {
357             my $key = $node_data_keys{$datum};
358             if( $datum eq 'name' ) {
359                 $this_node_data{$key} = $n->name;
360             } elsif( $datum eq 'reading' ) {
361                 $this_node_data{$key} = $n->label;
362             } elsif( $datum eq 'identical' && $n->has_primary ) {
363                 $this_node_data{$key} = $n->primary->name;
364             } elsif( $datum eq 'position' ) {
365                 $this_node_data{$key} = $n->position;
366             }
367         }
368         my $node_el = $graph->addNewChild( $graphml_ns, 'node' );
369         my $node_xmlid = 'n' . $node_ctr++;
370         $node_hash{ $n->name } = $node_xmlid;
371         $node_el->setAttribute( 'id', $node_xmlid );
372             
373         foreach my $dk ( keys %this_node_data ) {
374             my $d_el = $node_el->addNewChild( $graphml_ns, 'data' );
375             $d_el->setAttribute( 'key', $dk );
376             $d_el->appendText( $this_node_data{$dk} );
377         }
378     }
379
380     # Add the path edges
381     my $edge_ctr = 0;
382     foreach my $e ( sort { $a->from->name cmp $b->from->name } $self->graph->edges() ) {
383         my( $name, $from, $to ) = ( 'e'.$edge_ctr++,
384                                     $node_hash{ $e->from->name() },
385                                     $node_hash{ $e->to->name() } );
386         my $edge_el = $graph->addNewChild( $graphml_ns, 'edge' );
387         $edge_el->setAttribute( 'source', $from );
388         $edge_el->setAttribute( 'target', $to );
389         $edge_el->setAttribute( 'id', $name );
390         if( $e->class() eq 'edge.path' ) {
391             # It's a witness path, so add the witness
392             my $base = $e->label;
393             my $key = $edge_data_keys{'witness_main'};
394             # TODO kind of hacky
395             if( $e->label =~ /^(.*?)\s+(\(a\.c\.\))$/ ) {
396                 $base = $1;
397                 $key = $edge_data_keys{'witness_ante_corr'};
398             }
399             my $wit_el = $edge_el->addNewChild( $graphml_ns, 'data' );
400             $wit_el->setAttribute( 'key', $key );
401             $wit_el->appendText( $base );
402         } else {
403             # It's a relationship
404             my $rel_el = $edge_el->addNewChild( $graphml_ns, 'data' );
405             $rel_el->setAttribute( 'key', $edge_data_keys{'relationship'} );
406             $rel_el->appendText( $e->label() );
407         }
408     }
409
410     # Return the thing
411     $self->_save_graphml( $graphml->toString(1) );
412     return $graphml->toString(1);
413 }
414
415 sub _make_xml_attr {
416     my $str = shift;
417     $str =~ s/\s/_/g;
418     $str =~ s/\W//g;
419     $str =~ "a$str" if $str =~ /^\d/;
420     return $str;
421 }
422
423 sub collapse_graph_paths {
424     my $self = shift;
425     # Our collation graph has an path per witness.  This is great for
426     # calculation purposes, but terrible for display.  Thus we want to
427     # display only one path between any two nodes.
428
429     return if $self->collapsed;
430
431     print STDERR "Collapsing witness paths in graph...\n";
432
433     # Don't list out every witness if we have more than half to list.
434     my $majority = int( scalar( @{$self->tradition->witnesses} ) / 2 ) + 1;
435     # But don't compress if there are only a few witnesses.
436     $majority = 4 if $majority < 4;
437     foreach my $node( $self->readings ) {
438         my $newlabels = {};
439         # We will visit each node, so we only look ahead.
440         foreach my $edge ( $node->outgoing() ) {
441             next unless $edge->class eq 'edge.path';
442             add_hash_entry( $newlabels, $edge->to->name, $edge->name );
443             $self->del_path( $edge );
444         }
445
446         foreach my $newdest ( keys %$newlabels ) {
447             my $label;
448             my @compressed_wits = ();
449             if( @{$newlabels->{$newdest}} < $majority ) {
450                 $label = join( ', ', sort( @{$newlabels->{$newdest}} ) );
451             } else {
452                 ## TODO FIX THIS HACK
453                 my @aclabels;
454                 foreach my $wit ( @{$newlabels->{$newdest}} ) {
455                     if( $wit =~ /^(.*?)(\s*\(?a\.\s*c\.\)?)$/ ) {
456                         push( @aclabels, $wit );
457                     } else {
458                         push( @compressed_wits, $wit );
459                     }
460                 }
461                 $label = join( ', ', 'majority', sort( @aclabels ) );
462             }
463             
464             my $newpath = 
465                 $self->add_path( $node, $self->reading( $newdest ), $label );
466             if( @compressed_wits ) {
467                 $newpath->hidden_witnesses( \@compressed_wits );
468             }
469         }
470     }
471
472     $self->collapsed( 1 );
473 }
474
475 sub expand_graph_paths {
476     my $self = shift;
477     # Our collation graph has only one path between any two nodes.
478     # This is great for display, but not so great for analysis.
479     # Expand this so that each witness has its own path between any
480     # two reading nodes.
481     return unless $self->collapsed;
482     
483     print STDERR "Expanding witness paths in graph...\n";
484     foreach my $path( $self->paths ) {
485         my $from = $path->from;
486         my $to = $path->to;
487         my @wits = split( /, /, $path->label );
488         if( $path->has_hidden_witnesses ) {
489             push( @wits, @{$path->hidden_witnesses} );
490         }
491         $self->del_path( $path );
492         foreach ( @wits ) {
493             $self->add_path( $from, $to, $_ );
494         }
495     }
496     $self->collapsed( 0 );
497 }
498
499 =back
500
501 =head2 Navigation methods
502
503 =over
504
505 =item B<start>
506
507 my $beginning = $collation->start();
508
509 Returns the beginning of the collation, a meta-reading with label '#START#'.
510
511 =cut
512
513 sub start {
514     # Return the beginning reading of the graph.
515     my $self = shift;
516     my( $new_start ) = @_;
517     if( $new_start ) {
518         $self->del_reading( '#START#' );
519         $self->graph->rename_node( $new_start, '#START#' );
520     }
521     return $self->reading('#START#');
522 }
523
524 =item B<reading_sequence>
525
526 my @readings = $graph->reading_sequence( $first, $last, $path[, $alt_path] );
527
528 Returns the ordered list of readings, starting with $first and ending
529 with $last, along the given witness path.  If no path is specified,
530 assume that the path is that of the base text (if any.)
531
532 =cut
533
534 sub reading_sequence {
535     my( $self, $start, $end, $witness, $backup ) = @_;
536
537     $witness = $self->baselabel unless $witness;
538     my @readings = ( $start );
539     my %seen;
540     my $n = $start;
541     while( $n && $n ne $end ) {
542         if( exists( $seen{$n->name()} ) ) {
543             warn "Detected loop at " . $n->name();
544             last;
545         }
546         $seen{$n->name()} = 1;
547         
548         my $next = $self->next_reading( $n, $witness, $backup );
549         warn "Did not find any path for $witness from reading " . $n->name
550             unless $next;
551         push( @readings, $next );
552         $n = $next;
553     }
554     # Check that the last reading is our end reading.
555     my $last = $readings[$#readings];
556     warn "Last reading found from " . $start->label() .
557         " for witness $witness is not the end!"
558         unless $last eq $end;
559     
560     return @readings;
561 }
562
563 =item B<next_reading>
564
565 my $next_reading = $graph->next_reading( $reading, $witpath );
566
567 Returns the reading that follows the given reading along the given witness
568 path.  
569
570 =cut
571
572 sub next_reading {
573     # Return the successor via the corresponding path.
574     my $self = shift;
575     return $self->_find_linked_reading( 'next', @_ );
576 }
577
578 =item B<prior_reading>
579
580 my $prior_reading = $graph->prior_reading( $reading, $witpath );
581
582 Returns the reading that precedes the given reading along the given witness
583 path.  
584
585 =cut
586
587 sub prior_reading {
588     # Return the predecessor via the corresponding path.
589     my $self = shift;
590     return $self->_find_linked_reading( 'prior', @_ );
591 }
592
593 sub _find_linked_reading {
594     my( $self, $direction, $node, $path, $alt_path ) = @_;
595     my @linked_paths = $direction eq 'next' 
596         ? $node->outgoing() : $node->incoming();
597     return undef unless scalar( @linked_paths );
598     
599     # We have to find the linked path that contains all of the
600     # witnesses supplied in $path.
601     my( @path_wits, @alt_path_wits );
602     @path_wits = $self->witnesses_of_label( $path ) if $path;
603     @alt_path_wits = $self->witnesses_of_label( $alt_path ) if $alt_path;
604     my $base_le;
605     my $alt_le;
606     foreach my $le ( @linked_paths ) {
607         if( $le->name eq $self->baselabel ) {
608             $base_le = $le;
609         } else {
610             my @le_wits = $self->witnesses_of_label( $le->name );
611             if( _is_within( \@path_wits, \@le_wits ) ) {
612                 # This is the right path.
613                 return $direction eq 'next' ? $le->to() : $le->from();
614             } elsif( _is_within( \@alt_path_wits, \@le_wits ) ) {
615                 $alt_le = $le;
616             }
617         }
618     }
619     # Got this far? Return the alternate path if it exists.
620     return $direction eq 'next' ? $alt_le->to() : $alt_le->from()
621         if $alt_le;
622
623     # Got this far? Return the base path if it exists.
624     return $direction eq 'next' ? $base_le->to() : $base_le->from()
625         if $base_le;
626
627     # Got this far? We have no appropriate path.
628     warn "Could not find $direction node from " . $node->label 
629         . " along path $path";
630     return undef;
631 }
632
633 # Some set logic.
634 sub _is_within {
635     my( $set1, $set2 ) = @_;
636     my $ret = @$set1; # will be 0, i.e. false, if set1 is empty
637     foreach my $el ( @$set1 ) {
638         $ret = 0 unless grep { /^\Q$el\E$/ } @$set2;
639     }
640     return $ret;
641 }
642
643
644 ## INITIALIZATION METHODS - for use by parsers
645 # Walk the paths for each witness in the graph, and return the nodes
646 # that the graph has in common.  If $using_base is true, some 
647 # different logic is needed.
648
649 sub walk_witness_paths {
650     my( $self, $end ) = @_;
651     # For each witness, walk the path through the graph.
652     # Then we need to find the common nodes.  
653     # TODO This method is going to fall down if we have a very gappy 
654     # text in the collation.
655     my $paths = {};
656     my @common_readings;
657     foreach my $wit ( @{$self->tradition->witnesses} ) {
658         my $curr_reading = $self->start;
659         my @wit_path = $self->reading_sequence( $self->start, $end, 
660                                                 $wit->sigil );
661         $wit->path( \@wit_path );
662
663         # Detect the common readings.
664         @common_readings = _find_common( \@common_readings, \@wit_path );
665     }
666
667     # Mark all the nodes as either common or not.
668     foreach my $cn ( @common_readings ) {
669         print STDERR "Setting " . $cn->name . " / " . $cn->label 
670             . " as common node\n";
671         $cn->make_common;
672     }
673     foreach my $n ( $self->readings() ) {
674         $n->make_variant unless $n->is_common;
675     }
676     # Return an array of the common nodes in order.
677     return @common_readings;
678 }
679
680 sub _find_common {
681     my( $common_readings, $new_path ) = @_;
682     my @cr;
683     if( @$common_readings ) {
684         foreach my $n ( @$new_path ) {
685             push( @cr, $n ) if grep { $_ eq $n } @$common_readings;
686         }
687     } else {
688         push( @cr, @$new_path );
689     }
690     return @cr;
691 }
692
693 sub _remove_common {
694     my( $common_readings, $divergence ) = @_;
695     my @cr;
696     my %diverged;
697     map { $diverged{$_->name} = 1 } @$divergence;
698     foreach( @$common_readings ) {
699         push( @cr, $_ ) unless $diverged{$_->name};
700     }
701     return @cr;
702 }
703
704
705 # An alternative to walk_witness_paths, for use when a collation is
706 # constructed from a base text and an apparatus.  We have the
707 # sequences of readings and just need to add path edges.
708
709 sub make_witness_paths {
710     my( $self ) = @_;
711
712     my @common_readings;
713     foreach my $wit ( @{$self->tradition->witnesses} ) {
714         print STDERR "Making path for " . $wit->sigil . "\n";
715         $self->make_witness_path( $wit );
716         @common_readings = _find_common( \@common_readings, $wit->path );
717         @common_readings = _find_common( \@common_readings, $wit->uncorrected_path );
718     }
719     return @common_readings;
720 }
721
722 sub make_witness_path {
723     my( $self, $wit ) = @_;
724     my @chain = @{$wit->path};
725     my $sig = $wit->sigil;
726     foreach my $idx ( 0 .. $#chain-1 ) {
727         $self->add_path( $chain[$idx], $chain[$idx+1], $sig );
728     }
729     @chain = @{$wit->uncorrected_path};
730     foreach my $idx( 0 .. $#chain-1 ) {
731         my $source = $chain[$idx];
732         my $target = $chain[$idx+1];
733         $self->add_path( $source, $target, "$sig (a.c.)" )
734             unless $self->has_path( $source, $target, $sig );
735     }
736 }
737
738 sub common_readings {
739     my $self = shift;
740     my @common = grep { $_->is_common } $self->readings();
741     return sort { _cmp_position( $a->position, $b->position ) } @common;
742 }
743
744 # Calculate the relative positions of nodes in the graph, if they
745 # were not given to us.
746 sub calculate_positions {
747     my( $self, @ordered_common ) = @_;
748
749     # We have to calculate the position identifiers for each word,
750     # keyed on the common nodes.  This will be 'fun'.  The end result
751     # is a hash per witness, whose key is the word node and whose
752     # value is its position in the text.  Common nodes are always N,1
753     # so have identical positions in each text.
754
755     my $node_pos = {};
756     foreach my $wit ( @{$self->tradition->witnesses} ) {
757         print STDERR "Calculating positions in " . $wit->sigil . "\n";
758         _update_positions_from_path( $wit->path, @ordered_common );
759         _update_positions_from_path( $wit->uncorrected_path, @ordered_common )
760             if $wit->has_ante_corr;
761     }
762     
763     # DEBUG
764     foreach my $r ( $self->readings() ) {
765         print STDERR "Reading " . $r->name . "/" . $r->label . " has no position\n"
766             unless( $r->has_position );
767     }
768
769     $self->init_lemmata();
770 }
771
772 sub _update_positions_from_path {
773     my( $path, @ordered_common ) = @_;
774
775     # First we walk the given path, making a matrix for the witness
776     # that corresponds to its eventual position identifier.  Common
777     # nodes always start a new row, and are thus always in the first
778     # column.
779     my $wit_matrix = [];
780     my $cn = 0;  # We should hit the common readings in order.
781     my $row = [];
782     foreach my $wn ( @{$path} ) {
783         if( $wn eq $ordered_common[$cn] ) {
784             # Set up to look for the next common node, and
785             # start a new row of words.
786             $cn++;
787             push( @$wit_matrix, $row ) if scalar( @$row );
788             $row = [];
789         }
790         push( @$row, $wn );
791     }
792     push( @$wit_matrix, $row );  # Push the last row onto the matrix
793
794     # Now we have a matrix per witness, so that each row in the
795     # matrix begins with a common node, and continues with all the
796     # variant words that appear in the witness.  We turn this into
797     # real positions in row,cell format.  But we need some
798     # trickery in order to make sure that each node gets assigned
799     # to only one position.
800     
801     foreach my $li ( 1..scalar(@$wit_matrix) ) {
802         foreach my $di ( 1..scalar(@{$wit_matrix->[$li-1]}) ) {
803             my $reading = $wit_matrix->[$li-1]->[$di-1];
804             my $position = "$li,$di";
805
806             # If we have seen this node before, we need to compare
807             # its position with what went before.
808             unless( $reading->has_position &&
809                     _cmp_position( $position, $reading->position ) < 1 ) {
810                 # The new position ID replaces the old one.
811                 $reading->position( $position );
812             } # otherwise, the old position needs to stay.
813         }
814     }
815 }
816
817 sub _cmp_position {
818     my( $a, $b ) = @_;
819     if ( $a && $b ) {
820         my @pos_a = split(/,/, $a );
821         my @pos_b = split(/,/, $b );
822
823         my $big_cmp = $pos_a[0] <=> $pos_b[0];
824         return $big_cmp if $big_cmp;
825         # else 
826         return $pos_a[1] <=> $pos_b[1];
827     } elsif ( $b ) { # a is undefined
828         return -1;
829     } elsif ( $a ) { # b is undefined
830         return 1;
831     }
832     return 0; # they are both undefined
833 }
834
835 sub all_positions {
836     my $self = shift;
837     my %positions = ();
838     map { $positions{$_->position} = 1 } $self->readings;
839     my @answer = sort { _cmp_position( $a, $b ) } keys( %positions );
840     return @answer;
841 }
842
843 sub readings_at_position {
844     my( $self, $pos ) = @_;
845     my @answer = grep { $_->position eq $pos } $self->readings;
846     return @answer;
847 }
848
849 ## Lemmatizer functions
850
851 sub init_lemmata {
852     my $self = shift;
853     
854     foreach my $position ( $self->all_positions ) {
855         $self->lemmata->{$position} = undef;
856     }
857
858     foreach my $cr ( $self->common_readings ) {
859         $self->lemmata->{$cr->position} = $cr->name;
860     }
861 }
862     
863 =item B<lemma_readings>
864
865 my @state = $graph->lemma_readings( @readings_delemmatized );
866
867 Takes a list of readings that have just been delemmatized, and returns
868 a set of tuples of the form ['reading', 'state'] that indicates what
869 changes need to be made to the graph.
870
871 =over
872
873 =item * 
874
875 A state of 1 means 'lemmatize this reading'
876
877 =item * 
878
879 A state of 0 means 'delemmatize this reading'
880
881 =item * 
882
883 A state of undef means 'an ellipsis belongs in the text here because
884 no decision has been made / an earlier decision was backed out'
885
886 =back
887
888 =cut
889
890 sub lemma_readings {
891     my( $self, @toggled_off_nodes ) = @_;
892
893     # First get the positions of those nodes which have been
894     # toggled off.
895     my $positions_off = {};
896     map { $positions_off->{ $_->position } = $_->name } @toggled_off_nodes;
897
898     # Now for each position, we have to see if a node is on, and we
899     # have to see if a node has been turned off.
900     my @answer;
901     foreach my $pos ( $self->all_positions() ) {
902         # Find the state of this position.  If there is an active node,
903         # its name will be the state; otherwise the state will be 0 
904         # (nothing at this position) or undef (ellipsis at this position)
905         my $active = $self->lemmata->{$pos};
906         
907         # Is there a formerly active node that was toggled off?
908         if( exists( $positions_off->{$pos} ) ) {
909             my $off_node = $positions_off->{$pos};
910             if( $active && $active ne $off_node) {
911                 push( @answer, [ $off_node, 0 ], [ $active, 1 ] );
912             } else {
913                 push( @answer, [ $off_node, $active ] );
914             }
915
916         # No formerly active node, so we just see if there is a currently
917         # active one.
918         } elsif( $active ) {
919             # Push the active node, whatever it is.
920             push( @answer, [ $active, 1 ] );
921         } else {
922             # Push the state that is there. Arbitrarily use the first node
923             # at that position.
924             my @pos_nodes = $self->readings_at_position( $pos );
925             push( @answer, [ $pos_nodes[0]->name, $self->lemmata->{$pos} ] );
926         }
927     }
928     
929     return @answer;
930 }
931
932 =item B<toggle_reading>
933
934 my @readings_delemmatized = $graph->toggle_reading( $reading_name );
935
936 Takes a reading node name, and either lemmatizes or de-lemmatizes
937 it. Returns a list of all readings that are de-lemmatized as a result
938 of the toggle.
939
940 =cut
941
942 sub toggle_reading {
943     my( $self, $rname ) = @_;
944     
945     return unless $rname;
946     my $reading = $self->reading( $rname );
947     if( !$reading || $reading->is_common() ) {
948         # Do nothing, it's a common node.
949         return;
950     } 
951     
952     my $pos = $reading->position;
953     my $old_state = $self->lemmata->{$pos};
954     my @readings_off;
955     if( $old_state && $old_state eq $rname ) {
956         # Turn off the node. We turn on no others by default.
957         push( @readings_off, $reading );
958     } else {
959         # Turn on the node.
960         $self->lemmata->{$pos} = $rname;
961         # Any other 'on' readings in the same position should be off.
962         push( @readings_off, $self->same_position_as( $reading ) );
963         # Any node that is an identical transposed one should be off.
964         push( @readings_off, $reading->identical_readings );
965     }
966     @readings_off = unique_list( @readings_off );
967
968     # Turn off the readings that need to be turned off.
969     my @readings_delemmatized;
970     foreach my $n ( @readings_off ) {
971         my $state = $self->lemmata->{$n->position};
972         if( $state && $state eq $n->name ) { 
973             # this reading is still on, so turn it off
974             push( @readings_delemmatized, $n );
975             my $new_state = undef;
976             if( $n eq $reading ) {
977                 # This is the reading that was clicked, so if there are no
978                 # other readings there, turn off the position.  In all other
979                 # cases, restore the ellipsis.
980                 my @other_n = $self->same_position_as( $n );
981                 $new_state = 0 unless @other_n;
982             }
983             $self->lemmata->{$n->position} = $new_state;
984         } elsif( $old_state && $old_state eq $n->name ) { 
985             # another reading has already been turned on here
986             push( @readings_delemmatized, $n );
987         } # else some other reading was on anyway, so pass.
988     }
989     return @readings_delemmatized;
990 }
991
992 sub same_position_as {
993     my( $self, $reading ) = @_;
994     my $pos = $reading->position;
995     my @same = grep { $_ ne $reading } $self->readings_at_position( $reading->position );
996     return @same;
997 }
998
999 # Return the string that joins together a list of witnesses for
1000 # display on a single path.
1001 sub path_label {
1002     my $self = shift;
1003     return join( $self->wit_list_separator, @_ );
1004 }
1005
1006 sub witnesses_of_label {
1007     my( $self, $label ) = @_;
1008     my $regex = $self->wit_list_separator;
1009     my @answer = split( /\Q$regex\E/, $label );
1010     return @answer;
1011 }    
1012
1013 sub unique_list {
1014     my( @list ) = @_;
1015     my %h;
1016     map { $h{$_->name} = $_ } @list;
1017     return values( %h );
1018 }
1019
1020 sub add_hash_entry {
1021     my( $hash, $key, $entry ) = @_;
1022     if( exists $hash->{$key} ) {
1023         push( @{$hash->{$key}}, $entry );
1024     } else {
1025         $hash->{$key} = [ $entry ];
1026     }
1027 }
1028
1029 no Moose;
1030 __PACKAGE__->meta->make_immutable;