add support for lacunas within the witnesses
[scpubgit/stemmatology.git] / lib / Text / Tradition / Collation / Reading.pm
index 86b2869..f598418 100644 (file)
@@ -1,19 +1,26 @@
 package Text::Tradition::Collation::Reading;
 
 use Moose;
-use Moose::Util::TypeConstraints;
 use MooseX::NonMoose;
+use Text::Tradition::Collation::Position;
 
 extends 'Graph::Easy::Node';
 
-subtype 'Position'
-    => as 'Str',
-    => where { $_ =~ /^\d+\,\d+$/ },
-    message { 'Position must be of the form x,y' };
-
 has 'position' => (
     is => 'rw',
-    isa => 'Position',
+    isa => 'Text::Tradition::Collation::Position',
+    predicate => 'has_position',
+    );
+    
+has 'rank' => (
+    is => 'rw',
+    isa => 'Int',
+    predicate => 'has_rank',
+    );
+    
+has 'is_lacuna' => (
+    is => 'rw',
+    isa => 'Bool',
     );
 
 # This contains an array of reading objects; the array is a pool,
@@ -24,18 +31,77 @@ has 'same_as' => (
     isa => 'ArrayRef[Text::Tradition::Collation::Reading]',
     );
 
-# This is a hash mapping of 'relationship => reading'.
-# TODO we should validate the relationships sometime.
-has 'relationships' => (
-    is => 'ro',
-    isa => 'HashRef[Text::Tradition::Collation::Reading]',
-    default => sub { {} },
-    );
+# Deal with the non-arg option for Graph::Easy's constructor.
+around BUILDARGS => sub {
+    my $orig = shift;
+    my $class = shift;
+
+    my %args;
+    if( @_ == 1 && ref( $_[0] ) ne 'HASH' ) {
+       return $class->$orig( 'name' => $_[0] );
+    } else {
+       return $class->$orig( @_ );
+    }
+};
+
+# Take constructor args as well as a Position argument.
+around position => sub {
+    my $orig = shift;
+    my $self = shift;
+    return $self->$orig() unless @_;
+
+    my @args = @_;
+    unless( @_ == 1 && ref( $_[0] ) eq 'Text::Tradition::Collation::Position' ) {
+       # We have constructor arguments; pass them to Position.
+       @args = ( Text::Tradition::Collation::Position->new( @_ ) );
+    }
+    $self->$orig( @args );
+};
 
 # Initialize the identity pool. 
 sub BUILD {
     my( $self, $args ) = @_;
-#    $self->same_as( [ $self ] );
+    $self->same_as( [ $self ] );
+}
+
+sub text {
+    # Wrapper function around 'label' attribute.
+    my $self = shift;
+    if( @_ ) {
+        if( defined $_[0] ) {
+               $self->set_attribute( 'label', $_[0] );
+        } else {
+            $self->del_attribute( 'label' );
+        }
+    }
+    return $self->label;
+}
+
+sub witnessed_by {
+    my( $self, $sigil, $backup ) = @_;
+    my @wits = $self->witnesses;
+    return 1 if grep { $_ eq $sigil } @wits;
+    if( $backup ) {
+        return 1 if grep { $_ eq $backup } @wits;
+    }
+    return 0;
+}
+    
+sub witnesses {
+    my( $self ) = @_;
+    my @paths = grep { $_->get_attribute( 'class' ) eq 'path' } $self->outgoing;
+    push( @paths, grep { $_->get_attribute( 'class' ) eq 'path' } $self->incoming );
+    my %wits;
+    foreach my $p ( @paths ) {
+        if( $p->has_hidden_witnesses ) {
+            foreach ( @{$p->hidden_witnesses} ) {
+                $wits{$_} = 1;
+            }
+        } else {
+            $wits{$p->label} = 1;
+        }
+    }
+    return keys %wits;
 }
 
 sub merge_from {
@@ -45,32 +111,27 @@ sub merge_from {
     my $new_pool = _merge_array_pool( \@now_identical, $self->same_as )
        if @now_identical;
 
-    # Adopt the relationship attributes of the other node.
-    my $now_rel = $merged_node->relationships;
-    foreach my $key ( %$now_rel ) {
-       if( $self->has_relationship( $key ) ) {
-           my $related = $self->get_relationship( $key );
-           if( $now_rel->{$key} ne $related ) {
-               warn( sprintf( "Merged reading %s has relationship %s to reading %s instead of %s; skipping",
-                              $merged_node->name, $key,
-                              $now_rel->{$key},
-                              $related) );
-           } # else no action needed
-       } else {
-           $self->set_relationship( $key, $now_rel->{$key} );
-       }
-    }
+    # TODO Adopt the relationship attributes and segment memberships of the other node.
 }
 
+## Dealing with transposed readings.  These methods are only really
+## applicable if we have a linear collation graph.
+
 sub set_identical {
     my( $self, $other_node ) = @_; 
     my $enlarged_pool = _merge_array_pool( $self->same_as, 
                                           $other_node->same_as );
 
     # ...and set this node to point to the enlarged pool.
-    $self->set_same_as( $enlarged_pool );
+    $self->same_as( $enlarged_pool );
 }   
 
+sub identical_readings {
+    my $self = shift;
+    my @same = grep { $_ ne $self } @{$self->same_as};
+    return @same;
+}
+
 sub _merge_array_pool {
     my( $pool, $main_pool ) = @_;
     my %poolhash;
@@ -87,25 +148,67 @@ sub _merge_array_pool {
     return $main_pool;
 }
 
-# Much easier to do this with a hash than with an array of Relationship objects,
-# which would be the proper OO method.
+sub has_primary {
+    my $self = shift;
+    my $pool = $self->same_as;
+    return $pool->[0]->name ne $self->name;
+}
+
+sub primary {
+    my $self = shift;
+    return $self->same_as->[0];
+}
 
-sub has_relationship {
-    my( $self, $rel ) = @_;
-    return exists( $self->relationships->{ $rel } );
+sub is_at_position {
+    my $self = shift;
+    return undef unless $self->has_position;
+    return $self->position->is_colocated( @_ );
 }
 
-sub get_relationship {
-    my( $self, $rel ) = @_;
-    if( $self->has_relationship( $rel ) ) {
-       return $self->relationships->{ $rel };
+# Returns all readings that adjoin this one on any path.
+sub neighbor_readings {
+    my( $self, $direction ) = @_;
+    $direction = 'both' unless $direction;
+    my @paths = grep { $_->isa( 'Text::Tradition::Collation::Path' ) } $self->edges;
+    my %connected;
+    foreach my $p ( @paths ) {
+       if( $p->to eq $self ) {
+           next if $direction eq 'forward';
+           $connected{$p->from->name} = $p->from;
+       } else { # $p->from eq $self
+           next if $direction =~ /^back/;
+           $connected{$p->to->name} = $p->to;
+       }
     }
-    return undef;
+    return values( %connected );
+}
+
+# Returns all readings related to the one we've got.
+sub related_readings {
+    my( $self, $colocated ) = @_;
+    my @related;
+    foreach my $e ( $self->edges ) {
+        next unless $e->isa( 'Text::Tradition::Collation::Relationship' );
+        next if $colocated && $e->type eq 'repetition';
+        push( @related, $e->from eq $self ? $e->to : $e->from );
+    }
+    return @related;
+}
+
+## Keep track of which readings are unchanged across witnesses.
+sub is_common {
+    my( $self ) = shift;
+    return $self->get_attribute( 'class' ) eq 'common';
+}
+
+sub make_common {
+    my( $self ) = shift;
+    $self->set_attribute( 'class', 'common' );
 }
 
-sub set_relationship {
-    my( $self, $rel, $value ) = @_;
-    $self->relationships->{ $rel } = $value;
+sub make_variant {
+    my( $self ) = shift;
+    $self->set_attribute( 'class', 'variant' );
 }
 
 no Moose;
@@ -119,15 +222,28 @@ __PACKAGE__->meta->make_immutable;
 # when overriding nodes, we also need ::Anon
 
 package Text::Tradition::Collation::Reading::Anon;
+use Moose;
+use MooseX::NonMoose;
+extends 'Text::Tradition::Collation::Reading';
+extends 'Graph::Easy::Node::Anon';
+no Moose;
+__PACKAGE__->meta->make_immutable;
 
-use base qw/Text::Tradition::Collation::Reading/;
-use base qw/Graph::Easy::Node::Anon/;
+1;
+# use base qw/Text::Tradition::Collation::Reading/;
+# use base qw/Graph::Easy::Node::Anon/;
 
 ######################################################
 # and :::Empty
 
 package Text::Tradition::Collation::Reading::Empty;
+use Moose;
+use MooseX::NonMoose;
+extends 'Graph::Easy::Node::Empty';
+no Moose;
+__PACKAGE__->meta->make_immutable;
 
-use base qw/Text::Tradition::Collation::Reading/;
+1;
+# use base qw/Text::Tradition::Collation::Reading/;
 
 ######################################################