use Text::CSV_XS;
use Text::Tradition::Collation::Reading;
use Text::Tradition::Collation::RelationshipStore;
+use Text::Tradition::Error;
use XML::LibXML;
use Moose;
}
# First check to see if a reading with this ID exists.
if( $self->reading( $reading->id ) ) {
- warn "Collation already has a reading with id " . $reading->id;
- return undef;
+ throw( "Collation already has a reading with id " . $reading->id );
}
$self->_add_reading( $reading->id => $reading );
# Once the reading has been added, put it in both graphs.
sub add_relationship {
my $self = shift;
my( $source, $target, $opts ) = $self->_stringify_args( @_ );
- my( $ret, @vectors ) = $self->relations->add_relationship( $source,
+ my( @vectors ) = $self->relations->add_relationship( $source,
$self->reading( $source ), $target, $self->reading( $target ), $opts );
# Force a full rank recalculation every time. Yuck.
- $self->calculate_ranks() if $ret && $self->end->has_rank;
- return( $ret, @vectors );
+ $self->calculate_ranks() if $self->end->has_rank;
+ return @vectors;
}
=head2 reading_witnesses( $reading )
my $dot = $self->as_dot( $from, $to );
unless( $dot ) {
- warn "Could not output a graph with range $from - $to";
- return;
+ throw( "Could not output a graph with range $from - $to" );
}
my @cmd = qw/dot -Tsvg/;
$used{$reading->id} = 1;
# Need not output nodes without separate labels
next if $reading->id eq $reading->text;
+ my $rattrs;
my $label = $reading->text;
$label =~ s/\"/\\\"/g;
- $dot .= sprintf( "\t\"%s\" [ label=\"%s\" ];\n", $reading->id, $label );
+ $rattrs->{'label'} = $label;
+ # TODO make this an option?
+ # $rattrs->{'fillcolor'} = 'green' if $reading->is_common;
+ $dot .= sprintf( "\t\"%s\" %s;\n", $reading->id, _dot_attr_string( $rattrs ) );
}
# Add the real edges
my( %substart, %subend );
foreach my $edge ( @edges ) {
# Do we need to output this edge?
- if( $used{$edge->[0]} && $used{$edge->[1]} ) {;
+ if( $used{$edge->[0]} && $used{$edge->[1]} ) {
my $label = $self->path_display_label( $self->path_witnesses( $edge ) );
my $variables = { %edge_attrs, 'label' => $label };
# Account for the rank gap if necessary
$variables->{'minlen'} = $self->reading( $edge->[1] )->rank
- $self->reading( $edge->[0] )->rank;
}
+ # 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
+
my $varopts = _dot_attr_string( $variables );
$dot .= sprintf( "\t\"%s\" -> \"%s\" %s;\n",
$edge->[0], $edge->[1], $varopts );
@edge = @$e;
}
my @wits = keys %{$self->sequence->get_edge_attributes( @edge )};
- return sort @wits;
+ return @wits;
}
sub path_display_label {
- my( $self, @wits ) = @_;
+ my $self = shift;
+ my @wits = sort @_;
my $maj = scalar( $self->tradition->witnesses ) * 0.6;
if( scalar @wits > $maj ) {
# TODO break out a.c. wits
my $edge_ctr = 0;
foreach my $e ( sort { $a->[0] cmp $b->[0] } $self->sequence->edges() ) {
# We add an edge in the graphml for every witness in $e.
- foreach my $wit ( $self->path_witnesses( $e ) ) {
+ foreach my $wit ( sort $self->path_witnesses( $e ) ) {
my( $id, $from, $to ) = ( 'e'.$edge_ctr++,
$node_hash{ $e->[0] },
$node_hash{ $e->[1] } );
sub make_alignment_table {
my( $self, $noderefs, $include ) = @_;
unless( $self->linear ) {
- warn "Need a linear graph in order to make an alignment table";
- return;
+ throw( "Need a linear graph in order to make an alignment table" );
}
my $table = { 'alignment' => [], 'length' => $self->end->rank - 1 };
my @all_pos = ( 1 .. $self->end->rank - 1 );
my $n = $start;
while( $n && $n->id ne $end->id ) {
if( exists( $seen{$n->id} ) ) {
- warn "Detected loop at " . $n->id;
- last;
+ throw( "Detected loop for $witness at " . $n->id );
}
$seen{$n->id} = 1;
my $next = $self->next_reading( $n, $witness );
unless( $next ) {
- warn "Did not find any path for $witness from reading " . $n->id;
- last;
+ throw( "Did not find any path for $witness from reading " . $n->id );
}
push( @readings, $next );
$n = $next;
}
# Check that the last reading is our end reading.
my $last = $readings[$#readings];
- warn "Last reading found from " . $start->text .
- " for witness $witness is not the end!"
+ throw( "Last reading found from " . $start->text .
+ " for witness $witness is not the end!" ) # TODO do we get this far?
unless $last->id eq $end->id;
return @readings;
if( $self->sequence->has_edge_attribute( @$le, $self->baselabel ) ) {
$base_le = $le;
}
- my @le_wits = $self->path_witnesses( $le );
+ my @le_wits = sort $self->path_witnesses( $le );
if( _is_within( \@path_wits, \@le_wits ) ) {
# This is the right path.
return $direction eq 'next' ? $le->[1] : $le->[0];
return @answer;
}
+=head2 common_readings
+
+Returns the list of common readings in the graph (i.e. those readings that are
+shared by all non-lacunose witnesses.)
+
+=cut
+
+sub common_readings {
+ my $self = shift;
+ my @common = grep { $_->is_common } $self->readings;
+ return @common;
+}
+
=head2 path_text( $sigil, $mainsigil [, $start, $end ] )
Returns the text of a witness (plus its backup, if we are using a layer)
if( defined $node_ranks->{$rel_containers{$r->id}} ) {
$r->rank( $node_ranks->{$rel_containers{$r->id}} );
} else {
- die "No rank calculated for node " . $r->id
- . " - do you have a cycle in the graph?";
+ # Die. Find the last rank we calculated.
+ my @all_defined = sort { $node_ranks->{$rel_containers{$a->id}}
+ <=> $node_ranks->{$rel_containers{$b->id}} }
+ $self->readings;
+ my $last = pop @all_defined;
+ throw( "Ranks not calculated after $last - do you have a cycle in the graph?" );
}
}
}
}
}
+=head2 calculate_common_readings
+
+Goes through the graph identifying the readings that appear in every witness
+(apart from those with lacunae at that spot.) Marks them as common and returns
+the list.
+
+=begin testing
+
+use Text::Tradition;
+
+my $cxfile = 't/data/Collatex-16.xml';
+my $t = Text::Tradition->new(
+ 'name' => 'inline',
+ 'input' => 'CollateX',
+ 'file' => $cxfile,
+ );
+my $c = $t->collation;
+
+my @common = $c->calculate_common_readings();
+is( scalar @common, 8, "Found correct number of common readings" );
+my @marked = sort $c->common_readings();
+is( scalar @common, 8, "All common readings got marked as such" );
+my @expected = qw/ n1 n12 n16 n19 n20 n5 n6 n7 /;
+is_deeply( \@marked, \@expected, "Found correct list of common readings" );
+
+=end testing
+
+=cut
+
+sub calculate_common_readings {
+ my $self = shift;
+ my @common;
+ my $table = $self->make_alignment_table( 1 );
+ foreach my $idx ( 0 .. $table->{'length'} - 1 ) {
+ my @row = map { $_->{'tokens'}->[$idx]->{'t'} } @{$table->{'alignment'}};
+ my %hash;
+ foreach my $r ( @row ) {
+ if( $r ) {
+ $hash{$r->id} = $r unless $r->is_meta;
+ } else {
+ $hash{'UNDEF'} = $r;
+ }
+ }
+ if( keys %hash == 1 && !exists $hash{'UNDEF'} ) {
+ my( $r ) = values %hash;
+ $r->is_common( 1 );
+ push( @common, $r );
+ }
+ }
+ return @common;
+}
+
=head2 text_from_paths
Calculate the text array for all witnesses from the path, for later consistency
return $dir eq 'predecessors' ? pop( @answer ) : shift ( @answer );
}
+sub throw {
+ Text::Tradition::Error->throw(
+ 'ident' => 'Collation error',
+ 'message' => $_[0],
+ );
+}
+
no Moose;
__PACKAGE__->meta->make_immutable;