fix bugs to do with reading relationships
[scpubgit/stemmatology.git] / lib / Text / Tradition / Collation / Reading.pm
index 146eadf..74e40b5 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,14 +31,6 @@ 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;
@@ -45,12 +44,74 @@ around BUILDARGS => sub {
     }
 };
 
+# 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 );
+};
+
+# A lacuna node is also a meta node.
+before is_lacuna => sub {
+       my( $self, $arg ) = @_;
+       if( $arg ) {
+               $self->is_meta( 1 );
+       }
+};
+
 # Initialize the identity pool. 
 sub BUILD {
     my( $self, $args ) = @_;
     $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 {
     my( $self, $merged_node ) = @_;
     # Adopt the identity pool of the other node.
@@ -58,23 +119,12 @@ 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, 
@@ -84,6 +134,12 @@ sub set_identical {
     $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;
@@ -103,7 +159,7 @@ sub _merge_array_pool {
 sub has_primary {
     my $self = shift;
     my $pool = $self->same_as;
-    return $pool->[0]->name eq $self->name;
+    return $pool->[0]->name ne $self->name;
 }
 
 sub primary {
@@ -111,25 +167,82 @@ sub primary {
     return $self->same_as->[0];
 }
 
-# Much easier to do this with a hash than with an array of Relationship objects,
-# which would be the proper OO method.
+sub is_at_position {
+    my $self = shift;
+    return undef unless $self->has_position;
+    return $self->position->is_colocated( @_ );
+}
+
+# Looks from the outside like an accessor for a Boolean, but really 
+# sets the node's class.  Should apply to start, end, and lacunae.
+
+sub is_meta {
+       my $self = shift;
+       my $arg = shift;
+       if( defined $arg && $arg ) {
+               $self->set_attribute( 'class', 'meta' );
+       } elsif ( defined $arg ) {
+               $self->del_attribute( 'class' );
+       }
+       return $self->sub_class eq 'meta';      
+}
 
-sub has_relationship {
-    my( $self, $rel ) = @_;
-    return exists( $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 values( %connected );
 }
 
-sub get_relationship {
-    my( $self, $rel ) = @_;
-    if( $self->has_relationship( $rel ) ) {
-       return $self->relationships->{ $rel };
+# Returns all readings related to the one we've got.
+sub related_readings {
+    my( $self, $colocated, $queried ) = @_;
+    $queried = { $self->name => 1 } unless $queried;
+    my @related;
+    # Get the nodes directly related to this one
+    foreach my $e ( $self->edges ) {
+        next unless $e->isa( 'Text::Tradition::Collation::Relationship' );
+        next if $colocated && $e->type eq 'repetition';
+        my $n = $e->from eq $self ? $e->to : $e->from;
+        next if $queried->{$n->name};
+        push( @related, $n );
     }
-    return undef;
+    # Now query those nodes for their relations, recursively
+    map { $queried->{$_->name} = 1 } @related;
+    my @also_related;
+    foreach ( @related ) {
+        push( @also_related, $_->related_readings( $colocated, $queried ) );
+    }
+    push( @related, @also_related );
+    return @related;
+}
+
+## Keep track of which readings are unchanged across witnesses.
+sub is_common {
+    my( $self ) = shift;
+    return $self->get_attribute( 'class' ) eq 'common';
+}
+
+## TODO Rationalize make_common, is_meta, etc.
+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;