Fix graphml output / input format
[scpubgit/stemmatology.git] / lib / Text / Tradition / Collation / Reading.pm
CommitLineData
784877d9 1package Text::Tradition::Collation::Reading;
2
8e1394aa 3use Moose;
784877d9 4use MooseX::NonMoose;
4cdd82f1 5use Text::Tradition::Collation::Position;
784877d9 6
7extends 'Graph::Easy::Node';
8
784877d9 9has 'position' => (
d047cd52 10 is => 'rw',
4cdd82f1 11 isa => 'Text::Tradition::Collation::Position',
4a8828f0 12 predicate => 'has_position',
d047cd52 13 );
910a0a6d 14
15has 'rank' => (
16 is => 'rw',
17 isa => 'Int',
18 predicate => 'has_rank',
19 );
eca16057 20
21has 'is_lacuna' => (
22 is => 'rw',
23 isa => 'Bool',
24 );
784877d9 25
26# This contains an array of reading objects; the array is a pool,
27# shared by the reading objects inside the pool. When a reading is
28# added to the pool, all the same_as attributes should be updated.
29has 'same_as' => (
d047cd52 30 is => 'rw',
31 isa => 'ArrayRef[Text::Tradition::Collation::Reading]',
32 );
784877d9 33
8e1394aa 34# Deal with the non-arg option for Graph::Easy's constructor.
35around BUILDARGS => sub {
36 my $orig = shift;
37 my $class = shift;
38
39 my %args;
40 if( @_ == 1 && ref( $_[0] ) ne 'HASH' ) {
41 return $class->$orig( 'name' => $_[0] );
42 } else {
43 return $class->$orig( @_ );
44 }
45};
46
4cdd82f1 47# Take constructor args as well as a Position argument.
48around position => sub {
49 my $orig = shift;
50 my $self = shift;
51 return $self->$orig() unless @_;
52
53 my @args = @_;
54 unless( @_ == 1 && ref( $_[0] ) eq 'Text::Tradition::Collation::Position' ) {
55 # We have constructor arguments; pass them to Position.
56 @args = ( Text::Tradition::Collation::Position->new( @_ ) );
57 }
58 $self->$orig( @args );
59};
60
94c00c71 61# A lacuna node is also a meta node.
62before is_lacuna => sub {
63 my( $self, $arg ) = @_;
64 if( $arg ) {
65 $self->is_meta( 1 );
66 }
67};
68
d047cd52 69# Initialize the identity pool.
70sub BUILD {
71 my( $self, $args ) = @_;
8e1394aa 72 $self->same_as( [ $self ] );
d047cd52 73}
784877d9 74
e2902068 75sub text {
76 # Wrapper function around 'label' attribute.
77 my $self = shift;
78 if( @_ ) {
eca16057 79 if( defined $_[0] ) {
80 $self->set_attribute( 'label', $_[0] );
81 } else {
82 $self->del_attribute( 'label' );
83 }
e2902068 84 }
9463b0bf 85 return $self->label;
e2902068 86}
87
910a0a6d 88sub witnessed_by {
89 my( $self, $sigil, $backup ) = @_;
90 my @wits = $self->witnesses;
91 return 1 if grep { $_ eq $sigil } @wits;
92 if( $backup ) {
93 return 1 if grep { $_ eq $backup } @wits;
94 }
95 return 0;
96}
97
98sub witnesses {
99 my( $self ) = @_;
100 my @paths = grep { $_->get_attribute( 'class' ) eq 'path' } $self->outgoing;
101 push( @paths, grep { $_->get_attribute( 'class' ) eq 'path' } $self->incoming );
102 my %wits;
103 foreach my $p ( @paths ) {
104 if( $p->has_hidden_witnesses ) {
105 foreach ( @{$p->hidden_witnesses} ) {
106 $wits{$_} = 1;
107 }
108 } else {
109 $wits{$p->label} = 1;
110 }
111 }
112 return keys %wits;
113}
114
784877d9 115sub merge_from {
116 my( $self, $merged_node ) = @_;
117 # Adopt the identity pool of the other node.
118 my @now_identical = grep { $_ ne $merged_node } @{$merged_node->same_as};
119 my $new_pool = _merge_array_pool( \@now_identical, $self->same_as )
120 if @now_identical;
121
910a0a6d 122 # TODO Adopt the relationship attributes and segment memberships of the other node.
784877d9 123}
124
3265b0ce 125## Dealing with transposed readings. These methods are only really
126## applicable if we have a linear collation graph.
127
784877d9 128sub set_identical {
129 my( $self, $other_node ) = @_;
130 my $enlarged_pool = _merge_array_pool( $self->same_as,
131 $other_node->same_as );
132
133 # ...and set this node to point to the enlarged pool.
8e1394aa 134 $self->same_as( $enlarged_pool );
784877d9 135}
136
de51424a 137sub identical_readings {
138 my $self = shift;
139 my @same = grep { $_ ne $self } @{$self->same_as};
140 return @same;
141}
142
784877d9 143sub _merge_array_pool {
144 my( $pool, $main_pool ) = @_;
145 my %poolhash;
146 foreach ( @$main_pool ) {
147 # Note which nodes are already in the main pool so that we
148 # don't re-add them.
149 $poolhash{$_->name} = 1;
150 }
151
152 foreach( @$pool ) {
153 # Add the remaining nodes to the main pool...
154 push( @$main_pool, $_ ) unless $poolhash{$_->name};
155 }
156 return $main_pool;
157}
d047cd52 158
8e1394aa 159sub has_primary {
160 my $self = shift;
161 my $pool = $self->same_as;
df6d9812 162 return $pool->[0]->name ne $self->name;
8e1394aa 163}
164
165sub primary {
166 my $self = shift;
167 return $self->same_as->[0];
168}
169
4cdd82f1 170sub is_at_position {
171 my $self = shift;
172 return undef unless $self->has_position;
173 return $self->position->is_colocated( @_ );
174}
175
94c00c71 176# Looks from the outside like an accessor for a Boolean, but really
177# sets the node's class. Should apply to start, end, and lacunae.
178
179sub is_meta {
180 my $self = shift;
181 my $arg = shift;
182 if( defined $arg && $arg ) {
183 $self->set_attribute( 'class', 'meta' );
184 } elsif ( defined $arg ) {
185 $self->del_attribute( 'class' );
186 }
187 return $self->sub_class eq 'meta';
188}
189
4cdd82f1 190# Returns all readings that adjoin this one on any path.
191sub neighbor_readings {
192 my( $self, $direction ) = @_;
193 $direction = 'both' unless $direction;
194 my @paths = grep { $_->isa( 'Text::Tradition::Collation::Path' ) } $self->edges;
195 my %connected;
196 foreach my $p ( @paths ) {
197 if( $p->to eq $self ) {
198 next if $direction eq 'forward';
199 $connected{$p->from->name} = $p->from;
200 } else { # $p->from eq $self
201 next if $direction =~ /^back/;
202 $connected{$p->to->name} = $p->to;
203 }
204 }
205 return values( %connected );
206}
207
910a0a6d 208# Returns all readings related to the one we've got.
209sub related_readings {
210 my( $self, $colocated ) = @_;
211 my @related;
212 foreach my $e ( $self->edges ) {
213 next unless $e->isa( 'Text::Tradition::Collation::Relationship' );
214 next if $colocated && $e->type eq 'repetition';
215 push( @related, $e->from eq $self ? $e->to : $e->from );
4cdd82f1 216 }
910a0a6d 217 return @related;
4cdd82f1 218}
219
3265b0ce 220## Keep track of which readings are unchanged across witnesses.
4a8828f0 221sub is_common {
222 my( $self ) = shift;
223 return $self->get_attribute( 'class' ) eq 'common';
224}
225
94c00c71 226## TODO Rationalize make_common, is_meta, etc.
4a8828f0 227sub make_common {
228 my( $self ) = shift;
229 $self->set_attribute( 'class', 'common' );
230}
231
232sub make_variant {
233 my( $self ) = shift;
234 $self->set_attribute( 'class', 'variant' );
235}
236
d047cd52 237no Moose;
238__PACKAGE__->meta->make_immutable;
239
2401;
241
242######################################################
243## copied from Graph::Easy::Parser docs
244######################################################
245# when overriding nodes, we also need ::Anon
246
247package Text::Tradition::Collation::Reading::Anon;
021bdbac 248use Moose;
249use MooseX::NonMoose;
250extends 'Text::Tradition::Collation::Reading';
251extends 'Graph::Easy::Node::Anon';
252no Moose;
253__PACKAGE__->meta->make_immutable;
d047cd52 254
021bdbac 2551;
256# use base qw/Text::Tradition::Collation::Reading/;
257# use base qw/Graph::Easy::Node::Anon/;
d047cd52 258
259######################################################
260# and :::Empty
261
262package Text::Tradition::Collation::Reading::Empty;
021bdbac 263use Moose;
264use MooseX::NonMoose;
265extends 'Graph::Easy::Node::Empty';
266no Moose;
267__PACKAGE__->meta->make_immutable;
d047cd52 268
021bdbac 2691;
270# use base qw/Text::Tradition::Collation::Reading/;
d047cd52 271
272######################################################