Collation library now backed by Graph.pm, tested with CollateX data
[scpubgit/stemmatology.git] / lib / Text / Tradition / Collation / Reading.pm
index be874ec..b77649a 100644 (file)
 package Text::Tradition::Collation::Reading;
 
 use Moose;
-use MooseX::NonMoose;
-use KiokuDB::Set;
-use KiokuDB::Util qw/ weak_set /;
+use overload '""' => \&_stringify;
+use Text::Tradition::Collation;
 
-extends 'Graph::Easy::Node';
+=head1 NAME
 
-has 'rank' => (
-    is => 'rw',
-    isa => 'Int',
-    predicate => 'has_rank',
-    );
+Text::Tradition::Collation::Reading - represents a reading (usually a word) in a collation.
     
-has 'is_lacuna' => (
-    is => 'rw',
-    isa => 'Bool',
-    );
+=head1 DESCRIPTION
 
-# This contains an array of reading objects; the array is a pool,
-# shared by the reading objects inside the pool.  When a reading is
-# added to the pool, all the same_as attributes should be updated.
-has 'same_as' => (
-       does => 'KiokuDB::Set',
-    is => 'rw',
-    );
-    
-has 'is_primary' => (
-       is => 'rw',
-       isa => 'Bool',
-       default => 1,
-       );
+Text::Tradition is a library for representation and analysis of collated
+texts, particularly medieval ones.  A 'reading' refers to a unit of text,
+usually a word, that appears in one or more witnesses (manuscripts) of the
+tradition; the text of a given witness is composed of a set of readings in
+a particular sequence
 
-# Deal with the non-arg option for Graph::Easy's constructor.
-around BUILDARGS => sub {
-       my $orig = shift;
-       my $class = shift;
+=head1 METHODS
 
-       my %args;
-       if( @_ == 1 && ref( $_[0] ) ne 'HASH' ) {
-               return $class->$orig( 'name' => $_[0] );
-       } else {
-               return $class->$orig( @_ );
-       }
-};
+=head2 new
 
-# A lacuna node is also a meta node.
-before is_lacuna => sub {
-       my( $self, $arg ) = @_;
-       if( $arg ) {
-               $self->is_meta( 1 );
-       }
-};
+Creates a new reading in the given collation with the given attributes. 
+Options include:
 
-# Initialize the identity pool. 
-sub BUILD {
-       my( $self, $args ) = @_;
-       my $pool = weak_set( $self );
-       $self->same_as( $pool );
-}
+=over 4
 
-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;
-}
+=item collation - The Text::Tradition::Collation object to which this reading belongs.  Required.
 
-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;
-}
+=item id - A unique identifier for this reading. Required.
 
-sub merge_from {
-       my( $self, $merged_node ) = @_;
-       if( $self eq $merged_node ) {
-               warn "Trying to merge a reading into itself";
-               return;
-       }
-       # Adopt the identity pool of the other node.
-       my @now_identical = grep { $_ ne $merged_node && $_ ne $self  }
-               $merged_node->same_as->members;
-       if( @now_identical ) {
-               _merge_array_pool( $self->same_as, @now_identical );
-       }
-}
+=item text - The word or other text of the reading.
 
-## Dealing with transposed readings.  These methods are only really
-## applicable if we have a linear collation graph.
+=item is_start - The reading is the starting point for the collation.
 
-sub set_identical {
-       my( $self, $other_node ) = @_;
-       if( $self eq $other_node ) {
-               warn "Trying to set a reading identical to itself";
-               return;
-       }
-       # We are no longer primary; turn that off first.
-       my @new_members = grep { $_ ne $other_node } $self->same_as->members;
-       _merge_array_pool( $other_node->same_as, @new_members );
-}      
+=item is_end - The reading is the ending point for the collation.
 
-sub identical_readings {
-       my $self = shift;
-       my @same = grep { $_ ne $self } $self->same_as->members;
-       return @same;
-}
+=item is_lacuna - The 'reading' represents a known gap in the text.
 
-## Helper function - 
-sub _merge_array_pool {
-       my( $pool, @new_members ) = @_;
-       $pool->insert( @new_members );
-       foreach my $n ( @new_members ) {
-               $n->is_primary( 0 );
-               $n->same_as( $pool );
-       }
-}
+=item rank - The sequence number of the reading. This should probably not be set manually.
 
-sub has_primary {
-       my $self = shift;
-       return !$self->is_primary;
-}
+=back
 
-sub primary {
-       my $self = shift;
-       my @p = grep { $_->is_primary } $self->same_as->members;
-       warn "Identity pool for " . $self->name . " has more than one primary"
-               if @p > 1;
-       warn "Identity pool for " . $self->name . " has no primary" unless @p;
-       return shift( @p );
-}
+One of 'text', 'is_start', 'is_end', or 'is_lacuna' is required.
 
-# Looks from the outside like an accessor for a Boolean, but really 
-# sets the node's class.  Should apply to start, end, and lacunae.
+=head2 collation
 
-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';              
-}
+=head2 id
 
-# 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 );
-}
+=head2 text
 
-# 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 );
-       }
-       # 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;
-}
+=head2 is_start
 
-## Keep track of which readings are unchanged across witnesses.
-sub is_common {
-       my( $self ) = shift;
-       return $self->get_attribute( 'class' ) eq 'common';
-}
+=head2 is_end
 
-## TODO Rationalize make_common, is_meta, etc.
-sub make_common {
-       my( $self ) = shift;
-       $self->set_attribute( 'class', 'common' );
-}
+=head2 is_lacuna
 
-sub make_variant {
-       my( $self ) = shift;
-       $self->set_attribute( 'class', 'variant' );
-}
+=head2 rank
 
-no Moose;
-__PACKAGE__->meta->make_immutable;
+Accessor methods for the given attributes.
 
-1;
+=cut
 
-######################################################
-## copied from Graph::Easy::Parser docs
-######################################################
-# when overriding nodes, we also need ::Anon
+has 'collation' => (
+       is => 'ro',
+       isa => 'Text::Tradition::Collation',
+       # required => 1,
+       weak_ref => 1,
+       );
 
-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;
+has 'id' => (
+       is => 'ro',
+       isa => 'Str',
+       required => 1,
+       );
 
-1;
-# use base qw/Text::Tradition::Collation::Reading/;
-# use base qw/Graph::Easy::Node::Anon/;
+has 'text' => (
+       is => 'ro',
+       isa => 'Str',
+       required => 1,
+       );
 
-######################################################
-# and :::Empty
+has 'is_start' => (
+       is => 'ro',
+       isa => 'Bool',
+       default => undef,
+       );
+
+has 'is_end' => (
+       is => 'ro',
+       isa => 'Bool',
+       default => undef,
+       );
+    
+has 'is_lacuna' => (
+    is => 'ro',
+    isa => 'Bool',
+       default => undef,
+    );
+
+has 'rank' => (
+    is => 'rw',
+    isa => 'Int',
+    predicate => 'has_rank',
+    );
+
+
+around BUILDARGS => sub {
+       my $orig = shift;
+       my $class = shift;
+       my $args;
+       if( @_ == 1 ) {
+               $args = shift;
+       } else {
+               $args = { @_ };
+       }
+       
+       # If one of our special booleans is set, we change the text and the
+       # ID to match.
+       
+       if( exists $args->{'is_lacuna'} ) {
+               $args->{'text'} = sprintf( "#LACUNA_%s#", $args->{'id'} );
+       } elsif( exists $args->{'is_start'} ) {
+               $args->{'id'} = '#START#';  # Change the ID to ensure we have only one
+               $args->{'text'} = '#START#';
+               $args->{'rank'} = 0;
+       } elsif( exists $args->{'is_end'} ) {
+               $args->{'id'} = '#END#';        # Change the ID to ensure we have only one
+               $args->{'text'} = '#END#';
+       }
+       
+       $class->$orig( $args );
+};
+
+=head2 is_meta
+
+A meta attribute (ha ha), which should be true if any of our 'special'
+booleans are true.  Implies that the reading does not represent a bit 
+of text found in a witness.
+
+=cut
+
+sub is_meta {
+       my $self = shift;
+       return $self->is_start || $self->is_end || $self->is_lacuna;    
+}
+
+# Some syntactic sugar
+sub related_readings {
+       my $self = shift;
+       return $self->collation->related_readings( $self, @_ );
+}
+
+sub _stringify {
+       my $self = shift;
+       return $self->id;
+}
 
-package Text::Tradition::Collation::Reading::Empty;
-use Moose;
-use MooseX::NonMoose;
-extends 'Graph::Easy::Node::Empty';
 no Moose;
 __PACKAGE__->meta->make_immutable;
 
 1;
-# use base qw/Text::Tradition::Collation::Reading/;
 
-######################################################