safer existence check
[scpubgit/stemmatology.git] / base / lib / Text / Tradition / Collation / RelationshipStore.pm
CommitLineData
22222af9 1package Text::Tradition::Collation::RelationshipStore;
2
3use strict;
4use warnings;
24efa55d 5use Safe::Isa;
63778331 6use Text::Tradition::Error;
22222af9 7use Text::Tradition::Collation::Relationship;
24efa55d 8use Text::Tradition::Collation::RelationshipType;
a1615ee4 9use TryCatch;
22222af9 10
11use Moose;
12
13=head1 NAME
14
2626f709 15Text::Tradition::Collation::RelationshipStore - Keeps track of the relationships
16between readings in a given collation
22222af9 17
18=head1 DESCRIPTION
19
20Text::Tradition is a library for representation and analysis of collated
21texts, particularly medieval ones. The RelationshipStore is an internal object
22of the collation, to keep track of the defined relationships (both specific and
23general) between readings.
24
3ae5e2ad 25=begin testing
26
27use Text::Tradition;
ee801e17 28use TryCatch;
3ae5e2ad 29
30use_ok( 'Text::Tradition::Collation::RelationshipStore' );
31
ee801e17 32# Add some relationships, and delete them
33
34my $cxfile = 't/data/Collatex-16.xml';
35my $t = Text::Tradition->new(
56772e8c 36 'name' => 'inline',
37 'input' => 'CollateX',
38 'file' => $cxfile,
39 );
ee801e17 40my $c = $t->collation;
41
f8331a4d 42my @v1 = $c->add_relationship( 'n21', 'n22', { 'type' => 'lexical' } );
ee801e17 43is( scalar @v1, 1, "Added a single relationship" );
44is( $v1[0]->[0], 'n21', "Got correct node 1" );
45is( $v1[0]->[1], 'n22', "Got correct node 2" );
679f17e1 46my @v2 = $c->add_relationship( 'n24', 'n23',
ee801e17 47 { 'type' => 'spelling', 'scope' => 'global' } );
48is( scalar @v2, 2, "Added a global relationship with two instances" );
49@v1 = $c->del_relationship( 'n22', 'n21' );
50is( scalar @v1, 1, "Deleted first relationship" );
679f17e1 51@v2 = $c->del_relationship( 'n12', 'n13' );
ee801e17 52is( scalar @v2, 2, "Deleted second global relationship" );
681893aa 53my @v3 = $c->del_relationship( 'n1', 'n2' );
54is( scalar @v3, 0, "Nothing deleted on non-existent relationship" );
ee801e17 55
3ae5e2ad 56=end testing
57
22222af9 58=head1 METHODS
59
60=head2 new( collation => $collation );
61
62Creates a new relationship store for the given collation.
63
64=cut
65
66has 'collation' => (
67 is => 'ro',
68 isa => 'Text::Tradition::Collation',
69 required => 1,
70 weak_ref => 1,
71 );
24efa55d 72
73=head2 types
74
75Registry of possible relationship types. See RelationshipType for more info.
76
77=cut
78
79has 'relationship_types' => (
80 is => 'ro',
81 traits => ['Hash'],
82 handles => {
83 has_type => 'exists',
84 add_type => 'set',
85 type => 'get',
86 del_type => 'delete'
87 },
88 );
22222af9 89
90has 'scopedrels' => (
91 is => 'ro',
92 isa => 'HashRef[HashRef[Text::Tradition::Collation::Relationship]]',
93 default => sub { {} },
94 );
95
96has 'graph' => (
97 is => 'ro',
98 isa => 'Graph',
99 default => sub { Graph->new( undirected => 1 ) },
100 handles => {
101 relationships => 'edges',
102 add_reading => 'add_vertex',
103 delete_reading => 'delete_vertex',
24efa55d 104 },
22222af9 105 );
106
359944f7 107=head2 equivalence_graph()
108
109Returns an equivalence graph of the collation, in which all readings
110related via a 'colocated' relationship are transformed into a single
111vertex. Can be used to determine the validity of a new relationship.
112
113=cut
114
115has 'equivalence_graph' => (
116 is => 'ro',
117 isa => 'Graph',
118 default => sub { Graph->new() },
e1083e99 119 writer => '_reset_equivalence',
359944f7 120 );
121
122has '_node_equivalences' => (
123 is => 'ro',
124 traits => ['Hash'],
125 handles => {
126 equivalence => 'get',
127 set_equivalence => 'set',
128 remove_equivalence => 'delete',
04482188 129 _clear_equivalence => 'clear',
24efa55d 130 },
359944f7 131 );
132
133has '_equivalence_readings' => (
134 is => 'ro',
135 traits => ['Hash'],
136 handles => {
137 eqreadings => 'get',
138 set_eqreadings => 'set',
139 remove_eqreadings => 'delete',
04482188 140 _clear_eqreadings => 'clear',
24efa55d 141 },
359944f7 142 );
143
24efa55d 144## Build function - here we have our default set of relationship types.
145
146sub BUILD {
147 my $self = shift;
148
149 my $regularize = sub {
150 return $_[0]->can('regularize') ? $_[0]->regularize : $_[0]->text; };
151
152 my @DEFAULT_TYPES = (
153 { name => 'collated', bindlevel => 50, is_weak => 1, is_transitive => 0, is_generalizable => 0 },
154 { name => 'orthographic', bindlevel => 0 },
155 { name => 'spelling', bindlevel => 1, record_sub => $regularize },
156 { name => 'punctuation', bindlevel => 2, record_sub => $regularize },
157 { name => 'grammatical', bindlevel => 2, record_sub => $regularize },
158 { name => 'lexical', bindlevel => 2, record_sub => $regularize },
159 { name => 'transposition', bindlevel => 50, is_colocation => 0, is_transitive => 0 },
160 { name => 'repetition', bindlevel => 50, is_colocation => 0, is_transitive => 0 }
161 );
162
163 foreach my $type ( @DEFAULT_TYPES ) {
164 $self->add_type( $type );
165 }
166}
167
168sub _regular_form {
169}
170
171around add_type => sub {
172 my $orig = shift;
173 my $self = shift;
174 my $new_type;
175 if( @_ == 1 && $_[0]->$_isa( 'Text::Tradition::Collation::RelationshipType' ) ) {
176 $new_type = shift;
177 } else {
178 my %args = @_ == 1 ? %{$_[0]} : @_;
179 $new_type = Text::Tradition::Collation::RelationshipType->new( %args );
180 }
181 $self->$orig( $new_type->name => $new_type );
182 return $new_type;
183};
184
359944f7 185around add_reading => sub {
186 my $orig = shift;
187 my $self = shift;
188
189 $self->equivalence_graph->add_vertex( @_ );
190 $self->set_equivalence( $_[0], $_[0] );
191 $self->set_eqreadings( $_[0], [ $_[0] ] );
192 $self->$orig( @_ );
193};
194
195around delete_reading => sub {
196 my $orig = shift;
197 my $self = shift;
198
359944f7 199 $self->_remove_equivalence_node( @_ );
200 $self->$orig( @_ );
201};
202
3ae5e2ad 203=head2 get_relationship
204
205Return the relationship object, if any, that exists between two readings.
206
207=cut
208
209sub get_relationship {
4633f9e4 210 my $self = shift;
211 my @vector;
212 if( @_ == 1 && ref( $_[0] ) eq 'ARRAY' ) {
213 # Dereference the edge arrayref that was passed.
214 my $edge = shift;
215 @vector = @$edge;
216 } else {
217 @vector = @_;
218 }
3ae5e2ad 219 my $relationship;
220 if( $self->graph->has_edge_attribute( @vector, 'object' ) ) {
221 $relationship = $self->graph->get_edge_attribute( @vector, 'object' );
ca6e6095 222 }
3ae5e2ad 223 return $relationship;
224}
225
226sub _set_relationship {
227 my( $self, $relationship, @vector ) = @_;
228 $self->graph->add_edge( @vector );
229 $self->graph->set_edge_attribute( @vector, 'object', $relationship );
176badfe 230 $self->_make_equivalence( @vector ) if $relationship->colocated;
3ae5e2ad 231}
a1615ee4 232
22222af9 233=head2 create
234
235Create a new relationship with the given options and return it.
236Warn and return undef if the relationship cannot be created.
237
238=cut
239
240sub create {
241 my( $self, $options ) = @_;
242 # Check to see if a relationship exists between the two given readings
243 my $source = delete $options->{'orig_a'};
244 my $target = delete $options->{'orig_b'};
3ae5e2ad 245 my $rel = $self->get_relationship( $source, $target );
246 if( $rel ) {
24efa55d 247 if( $self->type( $rel->type )->is_weak ) {
248 # Always replace a weak relationship with a more descriptive
3d14b48e 249 # one, if asked.
250 $self->del_relationship( $source, $target );
251 } elsif( $rel->type ne $options->{'type'} ) {
63778331 252 throw( "Another relationship of type " . $rel->type
253 . " already exists between $source and $target" );
22222af9 254 } else {
255 return $rel;
256 }
257 }
258
99ab9535 259 $rel = Text::Tradition::Collation::Relationship->new( $options );
24efa55d 260 my $reltype = $self->type( $rel->type );
261 # Validate the options given against the relationship type wanted
262 throw( "Cannot set nonlocal scope on relationship of type " . $reltype->name )
263 if $rel->nonlocal && !$reltype->is_generalizable;
264
99ab9535 265 $self->add_scoped_relationship( $rel ) if $rel->nonlocal;
266 return $rel;
22222af9 267}
268
269=head2 add_scoped_relationship( $rel )
270
271Keep track of relationships defined between specific readings that are scoped
272non-locally. Key on whichever reading occurs first alphabetically.
273
274=cut
275
276sub add_scoped_relationship {
277 my( $self, $rel ) = @_;
24efa55d 278 my $rdga = $rel->reading_a;
279 my $rdgb = $rel->reading_b;
f222800e 280 my $r = $self->scoped_relationship( $rdga, $rdgb );
22222af9 281 if( $r ) {
282 warn sprintf( "Scoped relationship of type %s already exists between %s and %s",
f222800e 283 $r->type, $rdga, $rdgb );
22222af9 284 return;
285 }
f222800e 286 my( $first, $second ) = sort ( $rdga, $rdgb );
287 $self->scopedrels->{$first}->{$second} = $rel;
22222af9 288}
289
290=head2 scoped_relationship( $reading_a, $reading_b )
291
292Returns the general (document-level or global) relationship that has been defined
293between the two reading strings. Returns undef if there is no general relationship.
294
295=cut
296
297sub scoped_relationship {
298 my( $self, $rdga, $rdgb ) = @_;
299 my( $first, $second ) = sort( $rdga, $rdgb );
300 if( exists $self->scopedrels->{$first}->{$second} ) {
301 return $self->scopedrels->{$first}->{$second};
24efa55d 302 }
303 return undef;
22222af9 304}
305
306=head2 add_relationship( $self, $source, $sourcetext, $target, $targettext, $opts )
307
308Adds the relationship specified in $opts (see Text::Tradition::Collation::Relationship
309for the possible options) between the readings given in $source and $target. Sets
310up a scoped relationship between $sourcetext and $targettext if the relationship is
311scoped non-locally.
312
313Returns a status boolean and a list of all reading pairs connected by the call to
314add_relationship.
315
6d381462 316=begin testing
317
56772e8c 318use Test::Warn;
6d381462 319use Text::Tradition;
320use TryCatch;
321
56772e8c 322my $t1;
e92d4229 323warnings_exist {
56772e8c 324 $t1 = Text::Tradition->new( 'input' => 'Self', 'file' => 't/data/legendfrag.xml' );
e92d4229 325} [qr/Cannot set relationship on a meta reading/],
56772e8c 326 "Got expected relationship drop warning on parse";
327
176badfe 328# Test 1.1: try to equate nodes that are prevented with an intermediate collation
6d381462 329ok( $t1, "Parsed test fragment file" );
330my $c1 = $t1->collation;
10e4b1ac 331my $trel = $c1->get_relationship( 'r9.2', 'r9.3' );
6d381462 332is( ref( $trel ), 'Text::Tradition::Collation::Relationship',
333 "Troublesome relationship exists" );
334is( $trel->type, 'collated', "Troublesome relationship is a collation" );
335
336# Try to make the link we want
337try {
10e4b1ac 338 $c1->add_relationship( 'r8.6', 'r10.3', { 'type' => 'orthographic' } );
6d381462 339 ok( 1, "Added cross-collation relationship as expected" );
176badfe 340} catch( Text::Tradition::Error $e ) {
341 ok( 0, "Existing collation blocked equivalence relationship: " . $e->message );
6d381462 342}
343
344try {
345 $c1->calculate_ranks();
346 ok( 1, "Successfully calculated ranks" );
176badfe 347} catch ( Text::Tradition::Error $e ) {
348 ok( 0, "Collation now has a cycle: " . $e->message );
6d381462 349}
350
176badfe 351# Test 1.2: attempt merge of an identical reading
359944f7 352try {
10e4b1ac 353 $c1->merge_readings( 'r9.3', 'r11.5' );
359944f7 354 ok( 1, "Successfully merged reading 'pontifex'" );
355} catch ( Text::Tradition::Error $e ) {
356 ok( 0, "Merge of mergeable readings failed: $e->message" );
357
358}
359
176badfe 360# Test 1.3: attempt relationship with a meta reading (should fail)
361try {
10e4b1ac 362 $c1->add_relationship( 'r8.1', 'r9.2', { 'type' => 'collated' } );
176badfe 363 ok( 0, "Allowed a meta-reading to be used in a relationship" );
364} catch ( Text::Tradition::Error $e ) {
365 is( $e->message, 'Cannot set relationship on a meta reading',
366 "Relationship link prevented for a meta reading" );
367}
368
beb47b16 369# Test 1.4: try to break a relationship near a meta reading
370$c1->add_relationship( 'r7.6', 'r7.3', { type => 'orthographic' } );
371try {
372 $c1->del_relationship( 'r7.6', 'r7.7' );
373 $c1->del_relationship( 'r7.6', 'r7.3' );
374 ok( 1, "Relationship broken with a meta reading as neighbor" );
375} catch {
376 ok( 0, "Relationship deletion failed with a meta reading as neighbor" );
377}
378
176badfe 379# Test 2.1: try to equate nodes that are prevented with a real intermediate
6d381462 380# equivalence
56772e8c 381my $t2;
e92d4229 382warnings_exist {
56772e8c 383 $t2 = Text::Tradition->new( 'input' => 'Self', 'file' => 't/data/legendfrag.xml' );
e92d4229 384} [qr/Cannot set relationship on a meta reading/],
56772e8c 385 "Got expected relationship drop warning on parse";
6d381462 386my $c2 = $t2->collation;
10e4b1ac 387$c2->add_relationship( 'r9.2', 'r9.3', { 'type' => 'lexical' } );
388my $trel2 = $c2->get_relationship( 'r9.2', 'r9.3' );
6d381462 389is( ref( $trel2 ), 'Text::Tradition::Collation::Relationship',
390 "Created blocking relationship" );
391is( $trel2->type, 'lexical', "Blocking relationship is not a collation" );
392# This time the link ought to fail
393try {
10e4b1ac 394 $c2->add_relationship( 'r8.6', 'r10.3', { 'type' => 'orthographic' } );
414cc046 395 ok( 0, "Added cross-equivalent bad relationship" );
176badfe 396} catch ( Text::Tradition::Error $e ) {
397 like( $e->message, qr/witness loop/,
398 "Existing equivalence blocked crossing relationship" );
6d381462 399}
400
401try {
402 $c2->calculate_ranks();
403 ok( 1, "Successfully calculated ranks" );
176badfe 404} catch ( Text::Tradition::Error $e ) {
405 ok( 0, "Collation now has a cycle: " . $e->message );
6d381462 406}
407
176badfe 408# Test 3.1: make a straightforward pair of transpositions.
cc31ebaa 409my $t3 = Text::Tradition->new( 'input' => 'Self', 'file' => 't/data/lf2.xml' );
410# Test 1: try to equate nodes that are prevented with an intermediate collation
411my $c3 = $t3->collation;
412try {
10e4b1ac 413 $c3->add_relationship( 'r36.4', 'r38.3', { 'type' => 'transposition' } );
cc31ebaa 414 ok( 1, "Added straightforward transposition" );
176badfe 415} catch ( Text::Tradition::Error $e ) {
416 ok( 0, "Failed to add normal transposition: " . $e->message );
cc31ebaa 417}
418try {
10e4b1ac 419 $c3->add_relationship( 'r36.3', 'r38.2', { 'type' => 'transposition' } );
cc31ebaa 420 ok( 1, "Added straightforward transposition complement" );
176badfe 421} catch ( Text::Tradition::Error $e ) {
422 ok( 0, "Failed to add normal transposition complement: " . $e->message );
cc31ebaa 423}
424
176badfe 425# Test 3.2: try to make a transposition that could be a parallel.
cc31ebaa 426try {
10e4b1ac 427 $c3->add_relationship( 'r28.2', 'r29.2', { 'type' => 'transposition' } );
cc31ebaa 428 ok( 0, "Added bad colocated transposition" );
176badfe 429} catch ( Text::Tradition::Error $e ) {
430 like( $e->message, qr/Readings appear to be colocated/,
431 "Prevented bad colocated transposition" );
cc31ebaa 432}
433
176badfe 434# Test 3.3: make the parallel, and then make the transposition again.
cc31ebaa 435try {
10e4b1ac 436 $c3->add_relationship( 'r28.3', 'r29.3', { 'type' => 'orthographic' } );
cc31ebaa 437 ok( 1, "Equated identical readings for transposition" );
176badfe 438} catch ( Text::Tradition::Error $e ) {
439 ok( 0, "Failed to equate identical readings: " . $e->message );
cc31ebaa 440}
441try {
10e4b1ac 442 $c3->add_relationship( 'r28.2', 'r29.2', { 'type' => 'transposition' } );
cc31ebaa 443 ok( 1, "Added straightforward transposition complement" );
176badfe 444} catch ( Text::Tradition::Error $e ) {
445 ok( 0, "Failed to add normal transposition complement: " . $e->message );
cc31ebaa 446}
447
6d381462 448=end testing
449
22222af9 450=cut
451
452sub add_relationship {
414cc046 453 my( $self, $source, $target, $options ) = @_;
454 my $c = $self->collation;
176badfe 455 my $sourceobj = $c->reading( $source );
456 my $targetobj = $c->reading( $target );
359944f7 457 throw( "Adding self relationship at $source" ) if $source eq $target;
176badfe 458 throw( "Cannot set relationship on a meta reading" )
459 if( $sourceobj->is_meta || $targetobj->is_meta );
ca6e6095 460 my $relationship;
24efa55d 461 my $reltype;
ca6e6095 462 my $thispaironly;
414cc046 463 my $droppedcolls = [];
ca6e6095 464 if( ref( $options ) eq 'Text::Tradition::Collation::Relationship' ) {
465 $relationship = $options;
24efa55d 466 $reltype = $self->type( $relationship->type );
ca6e6095 467 $thispaironly = 1; # If existing rel, set only where asked.
24efa55d 468 # Test the validity
414cc046 469 my( $is_valid, $reason ) = $self->relationship_valid( $source, $target,
24efa55d 470 $relationship->type, $droppedcolls );
ca6e6095 471 unless( $is_valid ) {
472 throw( "Invalid relationship: $reason" );
473 }
24efa55d 474 } else {
475 $reltype = $self->type( $options->{type} );
ca6e6095 476
477 # Try to create the relationship object.
24efa55d 478 my $rdga = $reltype->record_sub->( $sourceobj );
479 my $rdgb = $reltype->record_sub->( $targetobj );
480 $options->{'orig_a'} = $sourceobj;
481 $options->{'orig_b'} = $targetobj;
482 $options->{'reading_a'} = $rdga;
483 $options->{'reading_b'} = $rdgb;
484 if( exists $options->{'scope'} && $options->{'scope'} ne 'local' ) {
0ac5e750 485 # Is there a relationship with this a & b already?
24efa55d 486 if( $rdga eq $rdgb ) {
487 # If we have canonified to the same thing for the relationship
488 # type we want, something is wrong.
489 # NOTE we want to allow this at the local level, as a cheap means
490 # of merging readings in the UI, until we get a better means.
491 throw( "Canonifier returns identical form $rdga for this relationship type" );
492 }
493
f222800e 494 my $otherrel = $self->scoped_relationship( $rdga, $rdgb );
0ac5e750 495 if( $otherrel && $otherrel->type eq $options->{type}
496 && $otherrel->scope eq $options->{scope} ) {
24efa55d 497 # warn "Applying existing scoped relationship for $rdga / $rdgb";
0ac5e750 498 $relationship = $otherrel;
99ab9535 499 } elsif( $otherrel ) {
24efa55d 500 throw( 'Conflicting scoped relationship '
501 . join( '/', $otherrel->type, $otherrel->scope ) . ' vs. '
502 . join( '/', $options->{type}, $options->{scope} )
503 . " for $rdga / $rdgb at $source / $target" );
0ac5e750 504 }
505 }
24efa55d 506 $relationship = $self->create( $options ) unless $relationship;
507 # ... Will throw on error
508
509 # See if the relationship is actually valid here
510 my( $is_valid, $reason ) = $self->relationship_valid( $source, $target,
511 $options->{'type'}, $droppedcolls );
512 unless( $is_valid ) {
513 throw( "Invalid relationship: $reason" );
514 }
22222af9 515 }
ca6e6095 516
22222af9 517
22222af9 518 # Now set the relationship(s).
519 my @pairs_set;
414cc046 520 my $rel = $self->get_relationship( $source, $target );
cc31ebaa 521 my $skip;
414cc046 522 if( $rel && $rel ne $relationship ) {
523 if( $rel->nonlocal ) {
524 throw( "Found conflicting relationship at $source - $target" );
24efa55d 525 } elsif( !$reltype->is_weak ) {
526 # Replace a weak relationship; leave any other sort in place.
414cc046 527 my $r1ann = $rel->has_annotation ? $rel->annotation : '';
528 my $r2ann = $relationship->has_annotation ? $relationship->annotation : '';
529 unless( $rel->type eq $relationship->type && $r1ann eq $r2ann ) {
530 warn sprintf( "Not overriding local relationship %s with global %s "
531 . "set at %s -> %s (%s -> %s)", $rel->type, $relationship->type,
532 $source, $target, $rel->reading_a, $rel->reading_b );
414cc046 533 }
24efa55d 534 $skip = 1;
414cc046 535 }
536 }
cc31ebaa 537 $self->_set_relationship( $relationship, $source, $target ) unless $skip;
414cc046 538 push( @pairs_set, [ $source, $target ] );
539
428bcf0b 540 # Find all the pairs for which we need to set the relationship.
541 if( $relationship->colocated && $relationship->nonlocal && !$thispaironly ) {
542 push( @pairs_set, $self->add_global_relationship( $relationship ) );
22222af9 543 }
414cc046 544 # Finally, restore whatever collations we can, and return.
24efa55d 545 $self->_restore_weak( @$droppedcolls );
63778331 546 return @pairs_set;
22222af9 547}
548
428bcf0b 549=head2 add_global_relationship( $options, $skipvector )
550
551Adds the relationship specified wherever the relevant readings appear together
552in the graph. Options as in add_relationship above.
553
554=cut
555
556sub add_global_relationship {
24efa55d 557 my( $self, $relationship ) = @_;
428bcf0b 558 # Sanity checking
24efa55d 559 my $reltype = $self->type( $relationship->type );
428bcf0b 560 throw( "Relationship passed to add_global is not global" )
561 unless $relationship->nonlocal;
562 throw( "Relationship passed to add_global is not a valid global type" )
24efa55d 563 unless $reltype->is_generalizable;
428bcf0b 564
565 # Apply the relationship wherever it is valid
566 my @pairs_set;
567 foreach my $v ( $self->_find_applicable( $relationship ) ) {
568 my $exists = $self->get_relationship( @$v );
24efa55d 569 my $etype = $exists ? $self->type( $exists->type ) : '';
570 if( $exists && !$etype->is_weak ) {
571 unless( $exists->is_equivalent( $relationship ) ) {
572 throw( "Found conflicting relationship at @$v" );
573 }
428bcf0b 574 } else {
24efa55d 575 my @added;
576 try {
577 @added = $self->add_relationship( @$v, $relationship );
578 } catch {
579 my $reldesc = sprintf( "%s %s -> %s", $relationship->type,
580 $relationship->reading_a, $relationship->reading_b );
581 print STDERR "Global relationship $reldesc not applicable at @$v\n";
582 }
583 push( @pairs_set, @added ) if @added;
428bcf0b 584 }
585 }
586 return @pairs_set;
587}
588
589
9d829138 590=head2 del_scoped_relationship( $reading_a, $reading_b )
591
592Returns the general (document-level or global) relationship that has been defined
593between the two reading strings. Returns undef if there is no general relationship.
594
595=cut
596
597sub del_scoped_relationship {
598 my( $self, $rdga, $rdgb ) = @_;
599 my( $first, $second ) = sort( $rdga, $rdgb );
600 return delete $self->scopedrels->{$first}->{$second};
601}
602
bf6e338d 603sub _find_applicable {
604 my( $self, $rel ) = @_;
605 my $c = $self->collation;
24efa55d 606 my $reltype = $self->type( $rel->type );
bf6e338d 607 my @vectors;
608 my @identical_readings;
24efa55d 609 @identical_readings = grep { $reltype->record_sub->( $_ ) eq $rel->reading_a }
610 $c->readings;
bf6e338d 611 foreach my $ir ( @identical_readings ) {
612 my @itarget;
24efa55d 613 @itarget = grep { $reltype->record_sub->( $_ ) eq $rel->reading_b }
614 $c->readings_at_rank( $ir->rank );
bf6e338d 615 if( @itarget ) {
24efa55d 616 # Warn if there is more than one hit with no closer link between them.
bf6e338d 617 my $itmain = shift @itarget;
618 if( @itarget ) {
619 my %all_targets;
24efa55d 620 my $bindlevel = $reltype->bindlevel;
bf6e338d 621 map { $all_targets{$_} = 1 } @itarget;
622 map { delete $all_targets{$_} }
24efa55d 623 $self->related_readings( $itmain, sub {
624 $self->type( $_[0]->type )->bindlevel < $bindlevel } );
bf6e338d 625 warn "More than one unrelated reading with text " . $itmain->text
626 . " at rank " . $ir->rank . "!" if keys %all_targets;
627 }
628 push( @vectors, [ $ir->id, $itmain->id ] );
629 }
630 }
631 return @vectors;
632}
633
ee801e17 634=head2 del_relationship( $source, $target )
635
636Removes the relationship between the given readings. If the relationship is
637non-local, removes the relationship everywhere in the graph.
638
639=cut
640
641sub del_relationship {
642 my( $self, $source, $target ) = @_;
643 my $rel = $self->get_relationship( $source, $target );
681893aa 644 return () unless $rel; # Nothing to delete; return an empty set.
24efa55d 645 my $reltype = $self->type( $rel->type );
359944f7 646 my $colo = $rel->colocated;
ee801e17 647 my @vectors = ( [ $source, $target ] );
359944f7 648 $self->_remove_relationship( $colo, $source, $target );
ee801e17 649 if( $rel->nonlocal ) {
650 # Remove the relationship wherever it occurs.
24efa55d 651 my @rel_edges = grep { $self->get_relationship( @$_ ) eq $rel }
ee801e17 652 $self->relationships;
653 foreach my $re ( @rel_edges ) {
359944f7 654 $self->_remove_relationship( $colo, @$re );
ee801e17 655 push( @vectors, $re );
656 }
9d829138 657 $self->del_scoped_relationship( $rel->reading_a, $rel->reading_b );
ee801e17 658 }
659 return @vectors;
660}
661
ca6e6095 662sub _remove_relationship {
359944f7 663 my( $self, $equiv, @vector ) = @_;
ca6e6095 664 $self->graph->delete_edge( @vector );
176badfe 665 $self->_break_equivalence( @vector ) if $equiv;
ca6e6095 666}
667
22222af9 668=head2 relationship_valid( $source, $target, $type )
669
670Checks whether a relationship of type $type may exist between the readings given
671in $source and $target. Returns a tuple of ( status, message ) where status is
672a yes/no boolean and, if the answer is no, message gives the reason why.
673
674=cut
675
676sub relationship_valid {
414cc046 677 my( $self, $source, $target, $rel, $mustdrop ) = @_;
678 $mustdrop = [] unless $mustdrop; # in case we were passed nothing
22222af9 679 my $c = $self->collation;
24efa55d 680 my $reltype = $self->type( $rel );
10943ab0 681 ## Assume validity is okay if we are initializing from scratch.
3579c22b 682 return ( 1, "initializing" ) unless $c->tradition->_initialized;
24efa55d 683 ## TODO Move this block to relationship type definition
684 if ( $rel eq 'transposition' || $rel eq 'repetition' ) {
22222af9 685 # Check that the two readings do (for a repetition) or do not (for
686 # a transposition) appear in the same witness.
56772e8c 687 # TODO this might be called before witness paths are set...
22222af9 688 my %seen_wits;
689 map { $seen_wits{$_} = 1 } $c->reading_witnesses( $source );
690 foreach my $w ( $c->reading_witnesses( $target ) ) {
691 if( $seen_wits{$w} ) {
692 return ( 0, "Readings both occur in witness $w" )
693 if $rel eq 'transposition';
694 return ( 1, "ok" ) if $rel eq 'repetition';
d6936dea 695 }
22222af9 696 }
abadc997 697 return ( 0, "Readings occur only in distinct witnesses" )
698 if $rel eq 'repetition';
699 }
24efa55d 700 if ( $reltype->is_colocation ) {
22222af9 701 # Check that linking the source and target in a relationship won't lead
414cc046 702 # to a path loop for any witness.
703 # First, drop/stash any collations that might interfere
704 my $sourceobj = $c->reading( $source );
705 my $targetobj = $c->reading( $target );
706 my $sourcerank = $sourceobj->has_rank ? $sourceobj->rank : -1;
707 my $targetrank = $targetobj->has_rank ? $targetobj->rank : -1;
708 unless( $rel eq 'collated' || $sourcerank == $targetrank ) {
24efa55d 709 push( @$mustdrop, $self->_drop_weak( $source ) );
710 push( @$mustdrop, $self->_drop_weak( $target ) );
359944f7 711 if( $c->end->has_rank ) {
176badfe 712 foreach my $rk ( $sourcerank .. $targetrank ) {
24efa55d 713 map { push( @$mustdrop, $self->_drop_weak( $_->id ) ) }
414cc046 714 $c->readings_at_rank( $rk );
715 }
716 }
a1615ee4 717 }
359944f7 718 unless( $self->test_equivalence( $source, $target ) ) {
24efa55d 719 $self->_restore_weak( @$mustdrop );
414cc046 720 return( 0, "Relationship would create witness loop" );
a1615ee4 721 }
22222af9 722 return ( 1, "ok" );
24efa55d 723 } else {
724 # We also need to check that the readings are not in the same place.
725 # That is, proposing to equate them should cause a witness loop.
726 if( $self->test_equivalence( $source, $target ) ) {
727 return ( 0, "Readings appear to be colocated" );
728 } else {
729 return ( 1, "ok" );
730 }
22222af9 731 }
732}
733
24efa55d 734sub _drop_weak {
778251a6 735 my( $self, $reading ) = @_;
414cc046 736 my @dropped;
778251a6 737 foreach my $n ( $self->graph->neighbors( $reading ) ) {
24efa55d 738 my $nrel = $self->get_relationship( $reading, $n );
739 if( $self->type( $nrel->type )->is_weak ) {
740 push( @dropped, [ $reading, $n, $nrel->type ] );
778251a6 741 $self->del_relationship( $reading, $n );
24efa55d 742 #print STDERR "Dropped weak relationship $reading -> $n\n";
778251a6 743 }
744 }
414cc046 745 return @dropped;
746}
747
24efa55d 748sub _restore_weak {
414cc046 749 my( $self, @vectors ) = @_;
750 foreach my $v ( @vectors ) {
24efa55d 751 my $type = pop @$v;
752 eval {
753 $self->add_relationship( @$v, { 'type' => $type } );
754 #print STDERR "Restored weak relationship @$v\n";
755 }; # if it fails we don't care
414cc046 756 }
778251a6 757}
758
cc31ebaa 759=head2 filter_collations()
760
24efa55d 761Utility function. Removes any redundant weak relationships from the graph.
762A weak relationship is redundant if the readings in question would occupy
cc31ebaa 763the same rank regardless of the existence of the relationship.
764
765=cut
766
24efa55d 767#TODO change name
cc31ebaa 768sub filter_collations {
769 my $self = shift;
770 my $c = $self->collation;
771 foreach my $r ( 1 .. $c->end->rank - 1 ) {
772 my $anchor;
24efa55d 773 my @need_weak;
cc31ebaa 774 foreach my $rdg ( $c->readings_at_rank( $r ) ) {
775 next if $rdg->is_meta;
776 my $ip = 0;
777 foreach my $pred ( $rdg->predecessors ) {
778 if( $pred->rank == $r - 1 ) {
779 $ip = 1;
780 $anchor = $rdg unless( $anchor );
781 last;
782 }
783 }
24efa55d 784 push( @need_weak, $rdg ) unless $ip;
785 $self->_drop_weak( $rdg->id );
cc31ebaa 786 }
787 $anchor
24efa55d 788 # TODO FIX HACK of adding explicit collation type
46e1fe14 789 ? map { $c->add_relationship( $anchor, $_, { 'type' => 'collated' } )
24efa55d 790 unless $c->get_relationship( $anchor, $_ ) } @need_weak
791 : print STDERR "No anchor found at $r\n";
cc31ebaa 792 }
793}
794
7f52eac8 795=head2 related_readings( $reading, $filter )
22222af9 796
797Returns a list of readings that are connected via relationship links to $reading.
7f52eac8 798If $filter is set to a subroutine ref, returns only those related readings where
799$filter( $relationship ) returns a true value.
22222af9 800
801=cut
802
803sub related_readings {
7f52eac8 804 my( $self, $reading, $filter ) = @_;
22222af9 805 my $return_object;
806 if( ref( $reading ) eq 'Text::Tradition::Collation::Reading' ) {
807 $reading = $reading->id;
808 $return_object = 1;
809 }
c84275ff 810 my @answer;
7f52eac8 811 if( $filter ) {
812 # Backwards compat
813 if( $filter eq 'colocated' ) {
814 $filter = sub { $_[0]->colocated };
d002ccb7 815 } elsif( !ref( $filter ) ) {
816 my $type = $filter;
817 $filter = sub { $_[0]->type eq $type };
7f52eac8 818 }
c84275ff 819 my %found = ( $reading => 1 );
820 my $check = [ $reading ];
821 my $iter = 0;
822 while( @$check ) {
c84275ff 823 my $more = [];
824 foreach my $r ( @$check ) {
825 foreach my $nr ( $self->graph->neighbors( $r ) ) {
7f52eac8 826 if( &$filter( $self->get_relationship( $r, $nr ) ) ) {
c84275ff 827 push( @$more, $nr ) unless exists $found{$nr};
828 $found{$nr} = 1;
829 }
830 }
831 }
832 $check = $more;
22222af9 833 }
7f52eac8 834 delete $found{$reading};
c84275ff 835 @answer = keys %found;
836 } else {
837 @answer = $self->graph->all_reachable( $reading );
22222af9 838 }
839 if( $return_object ) {
840 my $c = $self->collation;
c84275ff 841 return map { $c->reading( $_ ) } @answer;
22222af9 842 } else {
c84275ff 843 return @answer;
22222af9 844 }
845}
846
847=head2 merge_readings( $kept, $deleted );
848
849Makes a best-effort merge of the relationship links between the given readings, and
850stops tracking the to-be-deleted reading.
851
852=cut
853
854sub merge_readings {
855 my( $self, $kept, $deleted, $combined ) = @_;
856 foreach my $edge ( $self->graph->edges_at( $deleted ) ) {
857 # Get the pair of kept / rel
858 my @vector = ( $kept );
859 push( @vector, $edge->[0] eq $deleted ? $edge->[1] : $edge->[0] );
860 next if $vector[0] eq $vector[1]; # Don't add a self loop
861
862 # If kept changes its text, drop the relationship.
863 next if $combined;
864
f222800e 865 # If kept / rel already has a relationship, just keep the old
3ae5e2ad 866 my $rel = $self->get_relationship( @vector );
f222800e 867 next if $rel;
22222af9 868
869 # Otherwise, adopt the relationship that would be deleted.
3ae5e2ad 870 $rel = $self->get_relationship( @$edge );
871 $self->_set_relationship( $rel, @vector );
22222af9 872 }
56772e8c 873 $self->_make_equivalence( $deleted, $kept );
22222af9 874}
875
359944f7 876### Equivalence logic
877
878sub _remove_equivalence_node {
879 my( $self, $node ) = @_;
880 my $group = $self->equivalence( $node );
881 my $nodelist = $self->eqreadings( $group );
882 if( @$nodelist == 1 && $nodelist->[0] eq $node ) {
3579c22b 883 $self->equivalence_graph->delete_vertex( $group );
359944f7 884 $self->remove_eqreadings( $group );
3579c22b 885 $self->remove_equivalence( $group );
359944f7 886 } elsif( @$nodelist == 1 ) {
3579c22b 887 throw( "DATA INCONSISTENCY in equivalence graph: " . $nodelist->[0] .
888 " in group that should have only $node" );
359944f7 889 } else {
10e4b1ac 890 my @newlist = grep { $_ ne $node } @$nodelist;
359944f7 891 $self->set_eqreadings( $group, \@newlist );
892 $self->remove_equivalence( $node );
893 }
894}
895
896=head2 add_equivalence_edge
897
176badfe 898Add an edge in the equivalence graph corresponding to $source -> $target in the
899collation. Should only be called by Collation.
359944f7 900
901=cut
902
903sub add_equivalence_edge {
904 my( $self, $source, $target ) = @_;
905 my $seq = $self->equivalence( $source );
906 my $teq = $self->equivalence( $target );
359944f7 907 $self->equivalence_graph->add_edge( $seq, $teq );
908}
909
176badfe 910=head2 delete_equivalence_edge
359944f7 911
176badfe 912Remove an edge in the equivalence graph corresponding to $source -> $target in the
913collation. Should only be called by Collation.
359944f7 914
915=cut
916
917sub delete_equivalence_edge {
918 my( $self, $source, $target ) = @_;
919 my $seq = $self->equivalence( $source );
920 my $teq = $self->equivalence( $target );
359944f7 921 $self->equivalence_graph->delete_edge( $seq, $teq );
922}
923
924sub _is_disconnected {
925 my $self = shift;
926 return( scalar $self->equivalence_graph->predecessorless_vertices > 1
927 || scalar $self->equivalence_graph->successorless_vertices > 1 );
928}
929
176badfe 930# Equate two readings in the equivalence graph
931sub _make_equivalence {
56772e8c 932 my( $self, $source, $target ) = @_;
359944f7 933 # Get the source equivalent readings
934 my $seq = $self->equivalence( $source );
935 my $teq = $self->equivalence( $target );
936 # Nothing to do if they are already equivalent...
937 return if $seq eq $teq;
56772e8c 938 my $sourcepool = $self->eqreadings( $seq );
359944f7 939 # and add them to the target readings.
56772e8c 940 push( @{$self->eqreadings( $teq )}, @$sourcepool );
941 map { $self->set_equivalence( $_, $teq ) } @$sourcepool;
359944f7 942 # Then merge the nodes in the equivalence graph.
943 foreach my $pred ( $self->equivalence_graph->predecessors( $seq ) ) {
56772e8c 944 $self->equivalence_graph->add_edge( $pred, $teq );
359944f7 945 }
946 foreach my $succ ( $self->equivalence_graph->successors( $seq ) ) {
56772e8c 947 $self->equivalence_graph->add_edge( $teq, $succ );
359944f7 948 }
949 $self->equivalence_graph->delete_vertex( $seq );
176badfe 950 # TODO enable this after collation parsing is done
10943ab0 951 throw( "Graph got disconnected making $source / $target equivalence" )
3579c22b 952 if $self->_is_disconnected && $self->collation->tradition->_initialized;
359944f7 953}
954
955=head2 test_equivalence
956
176badfe 957Test whether, if two readings were equated with a 'colocated' relationship,
958the graph would still be valid.
359944f7 959
960=cut
961
962sub test_equivalence {
963 my( $self, $source, $target ) = @_;
964 # Try merging the nodes in the equivalence graph; return a true value if
965 # no cycle is introduced thereby. Restore the original graph first.
966
967 # Keep track of edges we add
968 my %added_pred;
969 my %added_succ;
970 # Get the reading equivalents
971 my $seq = $self->equivalence( $source );
972 my $teq = $self->equivalence( $target );
973 # Maybe this is easy?
974 return 1 if $seq eq $teq;
975
976 # Save the first graph
977 my $checkstr = $self->equivalence_graph->stringify();
978 # Add and save relevant edges
979 foreach my $pred ( $self->equivalence_graph->predecessors( $seq ) ) {
980 if( $self->equivalence_graph->has_edge( $pred, $teq ) ) {
981 $added_pred{$pred} = 0;
982 } else {
983 $self->equivalence_graph->add_edge( $pred, $teq );
984 $added_pred{$pred} = 1;
985 }
986 }
987 foreach my $succ ( $self->equivalence_graph->successors( $seq ) ) {
988 if( $self->equivalence_graph->has_edge( $teq, $succ ) ) {
989 $added_succ{$succ} = 0;
990 } else {
991 $self->equivalence_graph->add_edge( $teq, $succ );
992 $added_succ{$succ} = 1;
993 }
994 }
995 # Delete source equivalent and test
996 $self->equivalence_graph->delete_vertex( $seq );
997 my $ret = !$self->equivalence_graph->has_a_cycle;
998
999 # Restore what we changed
1000 $self->equivalence_graph->add_vertex( $seq );
1001 foreach my $pred ( keys %added_pred ) {
1002 $self->equivalence_graph->add_edge( $pred, $seq );
1003 $self->equivalence_graph->delete_edge( $pred, $teq ) if $added_pred{$pred};
1004 }
1005 foreach my $succ ( keys %added_succ ) {
1006 $self->equivalence_graph->add_edge( $seq, $succ );
1007 $self->equivalence_graph->delete_edge( $teq, $succ ) if $added_succ{$succ};
1008 }
1009 unless( $self->equivalence_graph->eq( $checkstr ) ) {
1010 warn "GRAPH CHANGED after testing";
1011 }
1012 # Return our answer
1013 return $ret;
1014}
1015
176badfe 1016# Unmake an equivalence link between two readings. Should only be called internally.
1017sub _break_equivalence {
359944f7 1018 my( $self, $source, $target ) = @_;
1019
1020 # This is the hard one. Need to reconstruct the equivalence groups without
1021 # the given link.
1022 my( %sng, %tng );
1023 map { $sng{$_} = 1 } $self->_find_equiv_without( $source, $target );
1024 map { $tng{$_} = 1 } $self->_find_equiv_without( $target, $source );
1025 # If these groups intersect, they are still connected; do nothing.
1026 foreach my $el ( keys %tng ) {
10e4b1ac 1027 return if( exists $sng{$el} );
359944f7 1028 }
359944f7 1029 # If they don't intersect, then we split the nodes in the graph and in
1030 # the hashes. First figure out which group has which name
176badfe 1031 my $oldgroup = $self->equivalence( $source ); # same as $target
1032 my $keepsource = $sng{$oldgroup};
1033 my $newgroup = $keepsource ? $target : $source;
359944f7 1034 my( $oldmembers, $newmembers );
176badfe 1035 if( $keepsource ) {
359944f7 1036 $oldmembers = [ keys %sng ];
1037 $newmembers = [ keys %tng ];
1038 } else {
1039 $oldmembers = [ keys %tng ];
1040 $newmembers = [ keys %sng ];
1041 }
1042
1043 # First alter the old group in the hash
1044 $self->set_eqreadings( $oldgroup, $oldmembers );
176badfe 1045 foreach my $el ( @$oldmembers ) {
1046 $self->set_equivalence( $el, $oldgroup );
1047 }
359944f7 1048
1049 # then add the new group back to the hash with its new key
1050 $self->set_eqreadings( $newgroup, $newmembers );
1051 foreach my $el ( @$newmembers ) {
1052 $self->set_equivalence( $el, $newgroup );
1053 }
1054
1055 # Now add the new group back to the equivalence graph
1056 $self->equivalence_graph->add_vertex( $newgroup );
1057 # ...add the appropriate edges to the source group vertext
1058 my $c = $self->collation;
1059 foreach my $rdg ( @$newmembers ) {
1060 foreach my $rp ( $c->sequence->predecessors( $rdg ) ) {
beb47b16 1061 next unless $self->equivalence( $rp );
359944f7 1062 $self->equivalence_graph->add_edge( $self->equivalence( $rp ), $newgroup );
1063 }
1064 foreach my $rs ( $c->sequence->successors( $rdg ) ) {
beb47b16 1065 next unless $self->equivalence( $rs );
359944f7 1066 $self->equivalence_graph->add_edge( $newgroup, $self->equivalence( $rs ) );
1067 }
1068 }
1069
1070 # ...and figure out which edges on the old group vertex to delete.
1071 my( %old_pred, %old_succ );
1072 foreach my $rdg ( @$oldmembers ) {
1073 foreach my $rp ( $c->sequence->predecessors( $rdg ) ) {
beb47b16 1074 next unless $self->equivalence( $rp );
359944f7 1075 $old_pred{$self->equivalence( $rp )} = 1;
1076 }
1077 foreach my $rs ( $c->sequence->successors( $rdg ) ) {
beb47b16 1078 next unless $self->equivalence( $rs );
359944f7 1079 $old_succ{$self->equivalence( $rs )} = 1;
1080 }
1081 }
1082 foreach my $p ( $self->equivalence_graph->predecessors( $oldgroup ) ) {
1083 unless( $old_pred{$p} ) {
1084 $self->equivalence_graph->delete_edge( $p, $oldgroup );
1085 }
1086 }
1087 foreach my $s ( $self->equivalence_graph->successors( $oldgroup ) ) {
1088 unless( $old_succ{$s} ) {
1089 $self->equivalence_graph->delete_edge( $oldgroup, $s );
1090 }
1091 }
176badfe 1092 # TODO enable this after collation parsing is done
10943ab0 1093 throw( "Graph got disconnected breaking $source / $target equivalence" )
3579c22b 1094 if $self->_is_disconnected && $self->collation->tradition->_initialized;
359944f7 1095}
1096
1097sub _find_equiv_without {
1098 my( $self, $first, $second ) = @_;
1099 my %found = ( $first => 1 );
1100 my $check = [ $first ];
1101 my $iter = 0;
1102 while( @$check ) {
1103 my $more = [];
1104 foreach my $r ( @$check ) {
1105 foreach my $nr ( $self->graph->neighbors( $r ) ) {
1106 next if $r eq $second;
1107 if( $self->get_relationship( $r, $nr )->colocated ) {
1108 push( @$more, $nr ) unless exists $found{$nr};
1109 $found{$nr} = 1;
1110 }
1111 }
1112 }
1113 $check = $more;
1114 }
1115 return keys %found;
1116}
1117
e1083e99 1118=head2 rebuild_equivalence
1119
1120(Re)build the equivalence graph from scratch. Dumps the graph, makes a new one,
1121adds all readings and edges, then makes an equivalence for all relationships.
1122
1123=cut
1124
1125sub rebuild_equivalence {
1126 my $self = shift;
1127 my $newgraph = Graph->new();
04482188 1128 # Set this as the new equivalence graph
1129 $self->_reset_equivalence( $newgraph );
1130 # Clear out the data hashes
1131 $self->_clear_equivalence;
1132 $self->_clear_eqreadings;
1133
b6f13859 1134 $self->collation->tradition->_init_done(0);
04482188 1135 # Add the readings
e1083e99 1136 foreach my $r ( $self->collation->readings ) {
04482188 1137 my $rid = $r->id;
1138 $newgraph->add_vertex( $rid );
1139 $self->set_equivalence( $rid, $rid );
1140 $self->set_eqreadings( $rid, [ $rid ] );
e1083e99 1141 }
04482188 1142
1143 # Now add the edges
e1083e99 1144 foreach my $e ( $self->collation->paths ) {
04482188 1145 $self->add_equivalence_edge( @$e );
e1083e99 1146 }
04482188 1147
1148 # Now equate the colocated readings. This does no testing;
1149 # it assumes that all preexisting relationships are valid.
e1083e99 1150 foreach my $rel ( $self->relationships ) {
1151 my $relobj = $self->get_relationship( $rel );
1152 next unless $relobj && $relobj->colocated;
1153 $self->_make_equivalence( @$rel );
1154 }
b6f13859 1155 $self->collation->tradition->_init_done(1);
e1083e99 1156}
1157
56772e8c 1158=head2 equivalence_ranks
1159
1160Rank all vertices in the equivalence graph, and return a hash reference with
1161vertex => rank mapping.
1162
1163=cut
1164
1165sub equivalence_ranks {
1166 my $self = shift;
1167 my $eqstart = $self->equivalence( $self->collation->start );
1168 my $eqranks = { $eqstart => 0 };
1169 my $rankeqs = { 0 => [ $eqstart ] };
1170 my @curr_origin = ( $eqstart );
1171 # A little iterative function.
1172 while( @curr_origin ) {
1173 @curr_origin = $self->_assign_rank( $eqranks, $rankeqs, @curr_origin );
1174 }
1175 return( $eqranks, $rankeqs );
1176}
1177
1178sub _assign_rank {
1179 my( $self, $node_ranks, $rank_nodes, @current_nodes ) = @_;
1180 my $graph = $self->equivalence_graph;
1181 # Look at each of the children of @current_nodes. If all the child's
1182 # parents have a rank, assign it the highest rank + 1 and add it to
1183 # @next_nodes. Otherwise skip it; we will return when the highest-ranked
1184 # parent gets a rank.
1185 my @next_nodes;
1186 foreach my $c ( @current_nodes ) {
1187 warn "Current reading $c has no rank!"
1188 unless exists $node_ranks->{$c};
1189 foreach my $child ( $graph->successors( $c ) ) {
1190 next if exists $node_ranks->{$child};
1191 my $highest_rank = -1;
1192 my $skip = 0;
1193 foreach my $parent ( $graph->predecessors( $child ) ) {
1194 if( exists $node_ranks->{$parent} ) {
1195 $highest_rank = $node_ranks->{$parent}
1196 if $highest_rank <= $node_ranks->{$parent};
1197 } else {
1198 $skip = 1;
1199 last;
1200 }
1201 }
1202 next if $skip;
1203 my $c_rank = $highest_rank + 1;
1204 # print STDERR "Assigning rank $c_rank to node $child \n";
1205 $node_ranks->{$child} = $c_rank if $node_ranks;
1206 push( @{$rank_nodes->{$c_rank}}, $child ) if $rank_nodes;
1207 push( @next_nodes, $child );
1208 }
1209 }
1210 return @next_nodes;
1211}
1212
359944f7 1213### Output logic
1214
027d819c 1215sub _as_graphml {
2626f709 1216 my( $self, $graphml_ns, $xmlroot, $node_hash, $nodeid_key, $edge_keys ) = @_;
c84275ff 1217
1218 my $rgraph = $xmlroot->addNewChild( $graphml_ns, 'graph' );
1219 $rgraph->setAttribute( 'edgedefault', 'directed' );
1220 $rgraph->setAttribute( 'id', 'relationships', );
1221 $rgraph->setAttribute( 'parse.edgeids', 'canonical' );
cc31ebaa 1222 $rgraph->setAttribute( 'parse.edges', 0 );
c84275ff 1223 $rgraph->setAttribute( 'parse.nodeids', 'canonical' );
cc31ebaa 1224 $rgraph->setAttribute( 'parse.nodes', 0 );
c84275ff 1225 $rgraph->setAttribute( 'parse.order', 'nodesfirst' );
1226
1227 # Add the vertices according to their XML IDs
2626f709 1228 my %rdg_lookup = ( reverse %$node_hash );
cc31ebaa 1229 # my @nlist = sort _by_xmlid keys( %rdg_lookup ); ## CAUSES SEGFAULT
826d8773 1230 my @nlist = sort keys( %rdg_lookup );
414cc046 1231 foreach my $n ( @nlist ) {
c84275ff 1232 my $n_el = $rgraph->addNewChild( $graphml_ns, 'node' );
1233 $n_el->setAttribute( 'id', $n );
2626f709 1234 _add_graphml_data( $n_el, $nodeid_key, $rdg_lookup{$n} );
c84275ff 1235 }
cc31ebaa 1236 $rgraph->setAttribute( 'parse.nodes', scalar @nlist );
c84275ff 1237
1238 # Add the relationship edges, with their object information
1239 my $edge_ctr = 0;
1240 foreach my $e ( sort { $a->[0] cmp $b->[0] } $self->graph->edges ) {
1241 # Add an edge and fill in its relationship info.
a30ca502 1242 next unless( exists $node_hash->{$e->[0]} && exists $node_hash->{$e->[1]} );
c84275ff 1243 my $edge_el = $rgraph->addNewChild( $graphml_ns, 'edge' );
1244 $edge_el->setAttribute( 'source', $node_hash->{$e->[0]} );
1245 $edge_el->setAttribute( 'target', $node_hash->{$e->[1]} );
1246 $edge_el->setAttribute( 'id', 'e'.$edge_ctr++ );
1247
3ae5e2ad 1248 my $rel_obj = $self->get_relationship( @$e );
bbd064a9 1249 foreach my $key ( keys %$edge_keys ) {
1250 my $value = $rel_obj->$key;
1251 _add_graphml_data( $edge_el, $edge_keys->{$key}, $value )
1252 if defined $value;
1253 }
c84275ff 1254 }
cc31ebaa 1255 $rgraph->setAttribute( 'parse.edges', $edge_ctr );
c84275ff 1256}
1257
1258sub _by_xmlid {
2626f709 1259 my $tmp_a = $a;
1260 my $tmp_b = $b;
1261 $tmp_a =~ s/\D//g;
1262 $tmp_b =~ s/\D//g;
1263 return $tmp_a <=> $tmp_b;
c84275ff 1264}
1265
1266sub _add_graphml_data {
1267 my( $el, $key, $value ) = @_;
1268 return unless defined $value;
1269 my $data_el = $el->addNewChild( $el->namespaceURI, 'data' );
1270 $data_el->setAttribute( 'key', $key );
1271 $data_el->appendText( $value );
83d5ac3a 1272}
1273
63778331 1274sub throw {
1275 Text::Tradition::Error->throw(
1276 'ident' => 'Relationship error',
1277 'message' => $_[0],
1278 );
1279}
1280
22222af9 1281no Moose;
1282__PACKAGE__->meta->make_immutable;
1283
12841;