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