we cannot save coderefs, so stop trying; self parser fixes for new relationship regime
[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' );
448
449
6d381462 450=end testing
451
22222af9 452=cut
453
454sub add_relationship {
414cc046 455 my( $self, $source, $target, $options ) = @_;
456 my $c = $self->collation;
176badfe 457 my $sourceobj = $c->reading( $source );
458 my $targetobj = $c->reading( $target );
359944f7 459 throw( "Adding self relationship at $source" ) if $source eq $target;
176badfe 460 throw( "Cannot set relationship on a meta reading" )
461 if( $sourceobj->is_meta || $targetobj->is_meta );
ca6e6095 462 my $relationship;
24efa55d 463 my $reltype;
c7bd2768 464 my $thispaironly = delete $options->{thispaironly};
414cc046 465 my $droppedcolls = [];
c7bd2768 466 $DB::single = 1 if $source eq 'r796.3' && $target eq 'r796.4';
ca6e6095 467 if( ref( $options ) eq 'Text::Tradition::Collation::Relationship' ) {
468 $relationship = $options;
24efa55d 469 $reltype = $self->type( $relationship->type );
ca6e6095 470 $thispaironly = 1; # If existing rel, set only where asked.
24efa55d 471 # Test the validity
414cc046 472 my( $is_valid, $reason ) = $self->relationship_valid( $source, $target,
24efa55d 473 $relationship->type, $droppedcolls );
ca6e6095 474 unless( $is_valid ) {
475 throw( "Invalid relationship: $reason" );
476 }
24efa55d 477 } else {
478 $reltype = $self->type( $options->{type} );
ca6e6095 479
480 # Try to create the relationship object.
c7bd2768 481 my $rdga = $reltype->regularize( $sourceobj );
482 my $rdgb = $reltype->regularize( $targetobj );
24efa55d 483 $options->{'orig_a'} = $sourceobj;
484 $options->{'orig_b'} = $targetobj;
485 $options->{'reading_a'} = $rdga;
486 $options->{'reading_b'} = $rdgb;
487 if( exists $options->{'scope'} && $options->{'scope'} ne 'local' ) {
0ac5e750 488 # Is there a relationship with this a & b already?
24efa55d 489 if( $rdga eq $rdgb ) {
490 # If we have canonified to the same thing for the relationship
491 # type we want, something is wrong.
492 # NOTE we want to allow this at the local level, as a cheap means
493 # of merging readings in the UI, until we get a better means.
494 throw( "Canonifier returns identical form $rdga for this relationship type" );
495 }
496
f222800e 497 my $otherrel = $self->scoped_relationship( $rdga, $rdgb );
0ac5e750 498 if( $otherrel && $otherrel->type eq $options->{type}
499 && $otherrel->scope eq $options->{scope} ) {
24efa55d 500 # warn "Applying existing scoped relationship for $rdga / $rdgb";
0ac5e750 501 $relationship = $otherrel;
99ab9535 502 } elsif( $otherrel ) {
24efa55d 503 throw( 'Conflicting scoped relationship '
504 . join( '/', $otherrel->type, $otherrel->scope ) . ' vs. '
505 . join( '/', $options->{type}, $options->{scope} )
506 . " for $rdga / $rdgb at $source / $target" );
0ac5e750 507 }
508 }
24efa55d 509 $relationship = $self->create( $options ) unless $relationship;
510 # ... Will throw on error
511
512 # See if the relationship is actually valid here
513 my( $is_valid, $reason ) = $self->relationship_valid( $source, $target,
514 $options->{'type'}, $droppedcolls );
515 unless( $is_valid ) {
516 throw( "Invalid relationship: $reason" );
517 }
22222af9 518 }
ca6e6095 519
22222af9 520
22222af9 521 # Now set the relationship(s).
522 my @pairs_set;
414cc046 523 my $rel = $self->get_relationship( $source, $target );
cc31ebaa 524 my $skip;
414cc046 525 if( $rel && $rel ne $relationship ) {
526 if( $rel->nonlocal ) {
527 throw( "Found conflicting relationship at $source - $target" );
24efa55d 528 } elsif( !$reltype->is_weak ) {
529 # Replace a weak relationship; leave any other sort in place.
414cc046 530 my $r1ann = $rel->has_annotation ? $rel->annotation : '';
531 my $r2ann = $relationship->has_annotation ? $relationship->annotation : '';
532 unless( $rel->type eq $relationship->type && $r1ann eq $r2ann ) {
533 warn sprintf( "Not overriding local relationship %s with global %s "
534 . "set at %s -> %s (%s -> %s)", $rel->type, $relationship->type,
535 $source, $target, $rel->reading_a, $rel->reading_b );
414cc046 536 }
24efa55d 537 $skip = 1;
414cc046 538 }
539 }
cc31ebaa 540 $self->_set_relationship( $relationship, $source, $target ) unless $skip;
414cc046 541 push( @pairs_set, [ $source, $target ] );
542
428bcf0b 543 # Find all the pairs for which we need to set the relationship.
544 if( $relationship->colocated && $relationship->nonlocal && !$thispaironly ) {
545 push( @pairs_set, $self->add_global_relationship( $relationship ) );
22222af9 546 }
414cc046 547 # Finally, restore whatever collations we can, and return.
24efa55d 548 $self->_restore_weak( @$droppedcolls );
63778331 549 return @pairs_set;
22222af9 550}
551
428bcf0b 552=head2 add_global_relationship( $options, $skipvector )
553
554Adds the relationship specified wherever the relevant readings appear together
555in the graph. Options as in add_relationship above.
556
557=cut
558
559sub add_global_relationship {
24efa55d 560 my( $self, $relationship ) = @_;
428bcf0b 561 # Sanity checking
24efa55d 562 my $reltype = $self->type( $relationship->type );
428bcf0b 563 throw( "Relationship passed to add_global is not global" )
564 unless $relationship->nonlocal;
565 throw( "Relationship passed to add_global is not a valid global type" )
24efa55d 566 unless $reltype->is_generalizable;
428bcf0b 567
568 # Apply the relationship wherever it is valid
569 my @pairs_set;
570 foreach my $v ( $self->_find_applicable( $relationship ) ) {
571 my $exists = $self->get_relationship( @$v );
24efa55d 572 my $etype = $exists ? $self->type( $exists->type ) : '';
573 if( $exists && !$etype->is_weak ) {
574 unless( $exists->is_equivalent( $relationship ) ) {
575 throw( "Found conflicting relationship at @$v" );
576 }
428bcf0b 577 } else {
24efa55d 578 my @added;
579 try {
580 @added = $self->add_relationship( @$v, $relationship );
581 } catch {
582 my $reldesc = sprintf( "%s %s -> %s", $relationship->type,
583 $relationship->reading_a, $relationship->reading_b );
584 print STDERR "Global relationship $reldesc not applicable at @$v\n";
585 }
586 push( @pairs_set, @added ) if @added;
428bcf0b 587 }
588 }
589 return @pairs_set;
590}
591
592
9d829138 593=head2 del_scoped_relationship( $reading_a, $reading_b )
594
595Returns the general (document-level or global) relationship that has been defined
596between the two reading strings. Returns undef if there is no general relationship.
597
598=cut
599
600sub del_scoped_relationship {
601 my( $self, $rdga, $rdgb ) = @_;
602 my( $first, $second ) = sort( $rdga, $rdgb );
603 return delete $self->scopedrels->{$first}->{$second};
604}
605
bf6e338d 606sub _find_applicable {
607 my( $self, $rel ) = @_;
608 my $c = $self->collation;
24efa55d 609 my $reltype = $self->type( $rel->type );
bf6e338d 610 my @vectors;
611 my @identical_readings;
c7bd2768 612 @identical_readings = grep { $reltype->regularize( $_ ) eq $rel->reading_a }
24efa55d 613 $c->readings;
bf6e338d 614 foreach my $ir ( @identical_readings ) {
615 my @itarget;
c7bd2768 616 @itarget = grep { $reltype->regularize( $_ ) eq $rel->reading_b }
24efa55d 617 $c->readings_at_rank( $ir->rank );
bf6e338d 618 if( @itarget ) {
24efa55d 619 # Warn if there is more than one hit with no closer link between them.
bf6e338d 620 my $itmain = shift @itarget;
621 if( @itarget ) {
622 my %all_targets;
24efa55d 623 my $bindlevel = $reltype->bindlevel;
bf6e338d 624 map { $all_targets{$_} = 1 } @itarget;
625 map { delete $all_targets{$_} }
24efa55d 626 $self->related_readings( $itmain, sub {
627 $self->type( $_[0]->type )->bindlevel < $bindlevel } );
bf6e338d 628 warn "More than one unrelated reading with text " . $itmain->text
629 . " at rank " . $ir->rank . "!" if keys %all_targets;
630 }
631 push( @vectors, [ $ir->id, $itmain->id ] );
632 }
633 }
634 return @vectors;
635}
636
ee801e17 637=head2 del_relationship( $source, $target )
638
639Removes the relationship between the given readings. If the relationship is
640non-local, removes the relationship everywhere in the graph.
641
642=cut
643
644sub del_relationship {
645 my( $self, $source, $target ) = @_;
646 my $rel = $self->get_relationship( $source, $target );
681893aa 647 return () unless $rel; # Nothing to delete; return an empty set.
24efa55d 648 my $reltype = $self->type( $rel->type );
359944f7 649 my $colo = $rel->colocated;
ee801e17 650 my @vectors = ( [ $source, $target ] );
359944f7 651 $self->_remove_relationship( $colo, $source, $target );
ee801e17 652 if( $rel->nonlocal ) {
653 # Remove the relationship wherever it occurs.
24efa55d 654 my @rel_edges = grep { $self->get_relationship( @$_ ) eq $rel }
ee801e17 655 $self->relationships;
656 foreach my $re ( @rel_edges ) {
359944f7 657 $self->_remove_relationship( $colo, @$re );
ee801e17 658 push( @vectors, $re );
659 }
9d829138 660 $self->del_scoped_relationship( $rel->reading_a, $rel->reading_b );
ee801e17 661 }
662 return @vectors;
663}
664
ca6e6095 665sub _remove_relationship {
359944f7 666 my( $self, $equiv, @vector ) = @_;
ca6e6095 667 $self->graph->delete_edge( @vector );
176badfe 668 $self->_break_equivalence( @vector ) if $equiv;
ca6e6095 669}
670
22222af9 671=head2 relationship_valid( $source, $target, $type )
672
673Checks whether a relationship of type $type may exist between the readings given
674in $source and $target. Returns a tuple of ( status, message ) where status is
675a yes/no boolean and, if the answer is no, message gives the reason why.
676
677=cut
678
679sub relationship_valid {
414cc046 680 my( $self, $source, $target, $rel, $mustdrop ) = @_;
681 $mustdrop = [] unless $mustdrop; # in case we were passed nothing
22222af9 682 my $c = $self->collation;
24efa55d 683 my $reltype = $self->type( $rel );
10943ab0 684 ## Assume validity is okay if we are initializing from scratch.
3579c22b 685 return ( 1, "initializing" ) unless $c->tradition->_initialized;
c7bd2768 686 ## TODO Move this block to relationship type definition when we can save
687 ## coderefs
24efa55d 688 if ( $rel eq 'transposition' || $rel eq 'repetition' ) {
22222af9 689 # Check that the two readings do (for a repetition) or do not (for
690 # a transposition) appear in the same witness.
56772e8c 691 # TODO this might be called before witness paths are set...
22222af9 692 my %seen_wits;
693 map { $seen_wits{$_} = 1 } $c->reading_witnesses( $source );
694 foreach my $w ( $c->reading_witnesses( $target ) ) {
695 if( $seen_wits{$w} ) {
696 return ( 0, "Readings both occur in witness $w" )
697 if $rel eq 'transposition';
698 return ( 1, "ok" ) if $rel eq 'repetition';
d6936dea 699 }
22222af9 700 }
abadc997 701 return ( 0, "Readings occur only in distinct witnesses" )
702 if $rel eq 'repetition';
703 }
24efa55d 704 if ( $reltype->is_colocation ) {
22222af9 705 # Check that linking the source and target in a relationship won't lead
414cc046 706 # to a path loop for any witness.
707 # First, drop/stash any collations that might interfere
708 my $sourceobj = $c->reading( $source );
709 my $targetobj = $c->reading( $target );
710 my $sourcerank = $sourceobj->has_rank ? $sourceobj->rank : -1;
711 my $targetrank = $targetobj->has_rank ? $targetobj->rank : -1;
712 unless( $rel eq 'collated' || $sourcerank == $targetrank ) {
24efa55d 713 push( @$mustdrop, $self->_drop_weak( $source ) );
714 push( @$mustdrop, $self->_drop_weak( $target ) );
359944f7 715 if( $c->end->has_rank ) {
176badfe 716 foreach my $rk ( $sourcerank .. $targetrank ) {
24efa55d 717 map { push( @$mustdrop, $self->_drop_weak( $_->id ) ) }
414cc046 718 $c->readings_at_rank( $rk );
719 }
720 }
a1615ee4 721 }
359944f7 722 unless( $self->test_equivalence( $source, $target ) ) {
24efa55d 723 $self->_restore_weak( @$mustdrop );
414cc046 724 return( 0, "Relationship would create witness loop" );
a1615ee4 725 }
22222af9 726 return ( 1, "ok" );
24efa55d 727 } else {
728 # We also need to check that the readings are not in the same place.
729 # That is, proposing to equate them should cause a witness loop.
730 if( $self->test_equivalence( $source, $target ) ) {
731 return ( 0, "Readings appear to be colocated" );
732 } else {
733 return ( 1, "ok" );
734 }
22222af9 735 }
736}
737
24efa55d 738sub _drop_weak {
778251a6 739 my( $self, $reading ) = @_;
414cc046 740 my @dropped;
778251a6 741 foreach my $n ( $self->graph->neighbors( $reading ) ) {
24efa55d 742 my $nrel = $self->get_relationship( $reading, $n );
743 if( $self->type( $nrel->type )->is_weak ) {
744 push( @dropped, [ $reading, $n, $nrel->type ] );
778251a6 745 $self->del_relationship( $reading, $n );
24efa55d 746 #print STDERR "Dropped weak relationship $reading -> $n\n";
778251a6 747 }
748 }
414cc046 749 return @dropped;
750}
751
24efa55d 752sub _restore_weak {
414cc046 753 my( $self, @vectors ) = @_;
754 foreach my $v ( @vectors ) {
24efa55d 755 my $type = pop @$v;
756 eval {
757 $self->add_relationship( @$v, { 'type' => $type } );
758 #print STDERR "Restored weak relationship @$v\n";
759 }; # if it fails we don't care
414cc046 760 }
778251a6 761}
762
cc31ebaa 763=head2 filter_collations()
764
24efa55d 765Utility function. Removes any redundant weak relationships from the graph.
766A weak relationship is redundant if the readings in question would occupy
cc31ebaa 767the same rank regardless of the existence of the relationship.
768
769=cut
770
24efa55d 771#TODO change name
cc31ebaa 772sub filter_collations {
773 my $self = shift;
774 my $c = $self->collation;
775 foreach my $r ( 1 .. $c->end->rank - 1 ) {
776 my $anchor;
24efa55d 777 my @need_weak;
cc31ebaa 778 foreach my $rdg ( $c->readings_at_rank( $r ) ) {
779 next if $rdg->is_meta;
780 my $ip = 0;
781 foreach my $pred ( $rdg->predecessors ) {
782 if( $pred->rank == $r - 1 ) {
783 $ip = 1;
784 $anchor = $rdg unless( $anchor );
785 last;
786 }
787 }
24efa55d 788 push( @need_weak, $rdg ) unless $ip;
789 $self->_drop_weak( $rdg->id );
cc31ebaa 790 }
791 $anchor
24efa55d 792 # TODO FIX HACK of adding explicit collation type
46e1fe14 793 ? map { $c->add_relationship( $anchor, $_, { 'type' => 'collated' } )
24efa55d 794 unless $c->get_relationship( $anchor, $_ ) } @need_weak
795 : print STDERR "No anchor found at $r\n";
cc31ebaa 796 }
797}
798
7f52eac8 799=head2 related_readings( $reading, $filter )
22222af9 800
801Returns a list of readings that are connected via relationship links to $reading.
7f52eac8 802If $filter is set to a subroutine ref, returns only those related readings where
803$filter( $relationship ) returns a true value.
22222af9 804
805=cut
806
807sub related_readings {
7f52eac8 808 my( $self, $reading, $filter ) = @_;
22222af9 809 my $return_object;
810 if( ref( $reading ) eq 'Text::Tradition::Collation::Reading' ) {
811 $reading = $reading->id;
812 $return_object = 1;
813 }
c84275ff 814 my @answer;
7f52eac8 815 if( $filter ) {
816 # Backwards compat
817 if( $filter eq 'colocated' ) {
818 $filter = sub { $_[0]->colocated };
d002ccb7 819 } elsif( !ref( $filter ) ) {
820 my $type = $filter;
821 $filter = sub { $_[0]->type eq $type };
7f52eac8 822 }
c84275ff 823 my %found = ( $reading => 1 );
824 my $check = [ $reading ];
825 my $iter = 0;
826 while( @$check ) {
c84275ff 827 my $more = [];
828 foreach my $r ( @$check ) {
829 foreach my $nr ( $self->graph->neighbors( $r ) ) {
7f52eac8 830 if( &$filter( $self->get_relationship( $r, $nr ) ) ) {
c84275ff 831 push( @$more, $nr ) unless exists $found{$nr};
832 $found{$nr} = 1;
833 }
834 }
835 }
836 $check = $more;
22222af9 837 }
7f52eac8 838 delete $found{$reading};
c84275ff 839 @answer = keys %found;
840 } else {
841 @answer = $self->graph->all_reachable( $reading );
22222af9 842 }
843 if( $return_object ) {
844 my $c = $self->collation;
c84275ff 845 return map { $c->reading( $_ ) } @answer;
22222af9 846 } else {
c84275ff 847 return @answer;
22222af9 848 }
849}
850
851=head2 merge_readings( $kept, $deleted );
852
853Makes a best-effort merge of the relationship links between the given readings, and
854stops tracking the to-be-deleted reading.
855
856=cut
857
858sub merge_readings {
859 my( $self, $kept, $deleted, $combined ) = @_;
860 foreach my $edge ( $self->graph->edges_at( $deleted ) ) {
861 # Get the pair of kept / rel
862 my @vector = ( $kept );
863 push( @vector, $edge->[0] eq $deleted ? $edge->[1] : $edge->[0] );
864 next if $vector[0] eq $vector[1]; # Don't add a self loop
865
866 # If kept changes its text, drop the relationship.
867 next if $combined;
868
f222800e 869 # If kept / rel already has a relationship, just keep the old
3ae5e2ad 870 my $rel = $self->get_relationship( @vector );
f222800e 871 next if $rel;
22222af9 872
873 # Otherwise, adopt the relationship that would be deleted.
3ae5e2ad 874 $rel = $self->get_relationship( @$edge );
875 $self->_set_relationship( $rel, @vector );
22222af9 876 }
56772e8c 877 $self->_make_equivalence( $deleted, $kept );
22222af9 878}
879
359944f7 880### Equivalence logic
881
882sub _remove_equivalence_node {
883 my( $self, $node ) = @_;
884 my $group = $self->equivalence( $node );
885 my $nodelist = $self->eqreadings( $group );
886 if( @$nodelist == 1 && $nodelist->[0] eq $node ) {
3579c22b 887 $self->equivalence_graph->delete_vertex( $group );
359944f7 888 $self->remove_eqreadings( $group );
3579c22b 889 $self->remove_equivalence( $group );
359944f7 890 } elsif( @$nodelist == 1 ) {
3579c22b 891 throw( "DATA INCONSISTENCY in equivalence graph: " . $nodelist->[0] .
892 " in group that should have only $node" );
359944f7 893 } else {
10e4b1ac 894 my @newlist = grep { $_ ne $node } @$nodelist;
359944f7 895 $self->set_eqreadings( $group, \@newlist );
896 $self->remove_equivalence( $node );
897 }
898}
899
900=head2 add_equivalence_edge
901
176badfe 902Add an edge in the equivalence graph corresponding to $source -> $target in the
903collation. Should only be called by Collation.
359944f7 904
905=cut
906
907sub add_equivalence_edge {
908 my( $self, $source, $target ) = @_;
909 my $seq = $self->equivalence( $source );
910 my $teq = $self->equivalence( $target );
359944f7 911 $self->equivalence_graph->add_edge( $seq, $teq );
912}
913
176badfe 914=head2 delete_equivalence_edge
359944f7 915
176badfe 916Remove an edge in the equivalence graph corresponding to $source -> $target in the
917collation. Should only be called by Collation.
359944f7 918
919=cut
920
921sub delete_equivalence_edge {
922 my( $self, $source, $target ) = @_;
923 my $seq = $self->equivalence( $source );
924 my $teq = $self->equivalence( $target );
359944f7 925 $self->equivalence_graph->delete_edge( $seq, $teq );
926}
927
928sub _is_disconnected {
929 my $self = shift;
930 return( scalar $self->equivalence_graph->predecessorless_vertices > 1
931 || scalar $self->equivalence_graph->successorless_vertices > 1 );
932}
933
176badfe 934# Equate two readings in the equivalence graph
935sub _make_equivalence {
56772e8c 936 my( $self, $source, $target ) = @_;
359944f7 937 # Get the source equivalent readings
938 my $seq = $self->equivalence( $source );
939 my $teq = $self->equivalence( $target );
940 # Nothing to do if they are already equivalent...
941 return if $seq eq $teq;
56772e8c 942 my $sourcepool = $self->eqreadings( $seq );
359944f7 943 # and add them to the target readings.
56772e8c 944 push( @{$self->eqreadings( $teq )}, @$sourcepool );
945 map { $self->set_equivalence( $_, $teq ) } @$sourcepool;
359944f7 946 # Then merge the nodes in the equivalence graph.
947 foreach my $pred ( $self->equivalence_graph->predecessors( $seq ) ) {
56772e8c 948 $self->equivalence_graph->add_edge( $pred, $teq );
359944f7 949 }
950 foreach my $succ ( $self->equivalence_graph->successors( $seq ) ) {
56772e8c 951 $self->equivalence_graph->add_edge( $teq, $succ );
359944f7 952 }
953 $self->equivalence_graph->delete_vertex( $seq );
176badfe 954 # TODO enable this after collation parsing is done
10943ab0 955 throw( "Graph got disconnected making $source / $target equivalence" )
3579c22b 956 if $self->_is_disconnected && $self->collation->tradition->_initialized;
359944f7 957}
958
959=head2 test_equivalence
960
176badfe 961Test whether, if two readings were equated with a 'colocated' relationship,
962the graph would still be valid.
359944f7 963
964=cut
965
966sub test_equivalence {
967 my( $self, $source, $target ) = @_;
968 # Try merging the nodes in the equivalence graph; return a true value if
969 # no cycle is introduced thereby. Restore the original graph first.
970
971 # Keep track of edges we add
972 my %added_pred;
973 my %added_succ;
974 # Get the reading equivalents
975 my $seq = $self->equivalence( $source );
976 my $teq = $self->equivalence( $target );
977 # Maybe this is easy?
978 return 1 if $seq eq $teq;
979
980 # Save the first graph
981 my $checkstr = $self->equivalence_graph->stringify();
982 # Add and save relevant edges
983 foreach my $pred ( $self->equivalence_graph->predecessors( $seq ) ) {
984 if( $self->equivalence_graph->has_edge( $pred, $teq ) ) {
985 $added_pred{$pred} = 0;
986 } else {
987 $self->equivalence_graph->add_edge( $pred, $teq );
988 $added_pred{$pred} = 1;
989 }
990 }
991 foreach my $succ ( $self->equivalence_graph->successors( $seq ) ) {
992 if( $self->equivalence_graph->has_edge( $teq, $succ ) ) {
993 $added_succ{$succ} = 0;
994 } else {
995 $self->equivalence_graph->add_edge( $teq, $succ );
996 $added_succ{$succ} = 1;
997 }
998 }
999 # Delete source equivalent and test
1000 $self->equivalence_graph->delete_vertex( $seq );
1001 my $ret = !$self->equivalence_graph->has_a_cycle;
1002
1003 # Restore what we changed
1004 $self->equivalence_graph->add_vertex( $seq );
1005 foreach my $pred ( keys %added_pred ) {
1006 $self->equivalence_graph->add_edge( $pred, $seq );
1007 $self->equivalence_graph->delete_edge( $pred, $teq ) if $added_pred{$pred};
1008 }
1009 foreach my $succ ( keys %added_succ ) {
1010 $self->equivalence_graph->add_edge( $seq, $succ );
1011 $self->equivalence_graph->delete_edge( $teq, $succ ) if $added_succ{$succ};
1012 }
1013 unless( $self->equivalence_graph->eq( $checkstr ) ) {
c7bd2768 1014 throw( "GRAPH CHANGED after testing" );
359944f7 1015 }
1016 # Return our answer
1017 return $ret;
1018}
1019
176badfe 1020# Unmake an equivalence link between two readings. Should only be called internally.
1021sub _break_equivalence {
359944f7 1022 my( $self, $source, $target ) = @_;
1023
1024 # This is the hard one. Need to reconstruct the equivalence groups without
1025 # the given link.
1026 my( %sng, %tng );
1027 map { $sng{$_} = 1 } $self->_find_equiv_without( $source, $target );
1028 map { $tng{$_} = 1 } $self->_find_equiv_without( $target, $source );
1029 # If these groups intersect, they are still connected; do nothing.
1030 foreach my $el ( keys %tng ) {
10e4b1ac 1031 return if( exists $sng{$el} );
359944f7 1032 }
359944f7 1033 # If they don't intersect, then we split the nodes in the graph and in
1034 # the hashes. First figure out which group has which name
176badfe 1035 my $oldgroup = $self->equivalence( $source ); # same as $target
1036 my $keepsource = $sng{$oldgroup};
1037 my $newgroup = $keepsource ? $target : $source;
359944f7 1038 my( $oldmembers, $newmembers );
176badfe 1039 if( $keepsource ) {
359944f7 1040 $oldmembers = [ keys %sng ];
1041 $newmembers = [ keys %tng ];
1042 } else {
1043 $oldmembers = [ keys %tng ];
1044 $newmembers = [ keys %sng ];
1045 }
1046
1047 # First alter the old group in the hash
1048 $self->set_eqreadings( $oldgroup, $oldmembers );
176badfe 1049 foreach my $el ( @$oldmembers ) {
1050 $self->set_equivalence( $el, $oldgroup );
1051 }
359944f7 1052
1053 # then add the new group back to the hash with its new key
1054 $self->set_eqreadings( $newgroup, $newmembers );
1055 foreach my $el ( @$newmembers ) {
1056 $self->set_equivalence( $el, $newgroup );
1057 }
1058
1059 # Now add the new group back to the equivalence graph
1060 $self->equivalence_graph->add_vertex( $newgroup );
1061 # ...add the appropriate edges to the source group vertext
1062 my $c = $self->collation;
1063 foreach my $rdg ( @$newmembers ) {
1064 foreach my $rp ( $c->sequence->predecessors( $rdg ) ) {
beb47b16 1065 next unless $self->equivalence( $rp );
359944f7 1066 $self->equivalence_graph->add_edge( $self->equivalence( $rp ), $newgroup );
1067 }
1068 foreach my $rs ( $c->sequence->successors( $rdg ) ) {
beb47b16 1069 next unless $self->equivalence( $rs );
359944f7 1070 $self->equivalence_graph->add_edge( $newgroup, $self->equivalence( $rs ) );
1071 }
1072 }
1073
1074 # ...and figure out which edges on the old group vertex to delete.
1075 my( %old_pred, %old_succ );
1076 foreach my $rdg ( @$oldmembers ) {
1077 foreach my $rp ( $c->sequence->predecessors( $rdg ) ) {
beb47b16 1078 next unless $self->equivalence( $rp );
359944f7 1079 $old_pred{$self->equivalence( $rp )} = 1;
1080 }
1081 foreach my $rs ( $c->sequence->successors( $rdg ) ) {
beb47b16 1082 next unless $self->equivalence( $rs );
359944f7 1083 $old_succ{$self->equivalence( $rs )} = 1;
1084 }
1085 }
1086 foreach my $p ( $self->equivalence_graph->predecessors( $oldgroup ) ) {
1087 unless( $old_pred{$p} ) {
1088 $self->equivalence_graph->delete_edge( $p, $oldgroup );
1089 }
1090 }
1091 foreach my $s ( $self->equivalence_graph->successors( $oldgroup ) ) {
1092 unless( $old_succ{$s} ) {
1093 $self->equivalence_graph->delete_edge( $oldgroup, $s );
1094 }
1095 }
176badfe 1096 # TODO enable this after collation parsing is done
10943ab0 1097 throw( "Graph got disconnected breaking $source / $target equivalence" )
3579c22b 1098 if $self->_is_disconnected && $self->collation->tradition->_initialized;
359944f7 1099}
1100
1101sub _find_equiv_without {
1102 my( $self, $first, $second ) = @_;
1103 my %found = ( $first => 1 );
1104 my $check = [ $first ];
1105 my $iter = 0;
1106 while( @$check ) {
1107 my $more = [];
1108 foreach my $r ( @$check ) {
1109 foreach my $nr ( $self->graph->neighbors( $r ) ) {
1110 next if $r eq $second;
1111 if( $self->get_relationship( $r, $nr )->colocated ) {
1112 push( @$more, $nr ) unless exists $found{$nr};
1113 $found{$nr} = 1;
1114 }
1115 }
1116 }
1117 $check = $more;
1118 }
1119 return keys %found;
1120}
1121
e1083e99 1122=head2 rebuild_equivalence
1123
1124(Re)build the equivalence graph from scratch. Dumps the graph, makes a new one,
1125adds all readings and edges, then makes an equivalence for all relationships.
1126
1127=cut
1128
1129sub rebuild_equivalence {
1130 my $self = shift;
1131 my $newgraph = Graph->new();
04482188 1132 # Set this as the new equivalence graph
1133 $self->_reset_equivalence( $newgraph );
1134 # Clear out the data hashes
1135 $self->_clear_equivalence;
1136 $self->_clear_eqreadings;
1137
b6f13859 1138 $self->collation->tradition->_init_done(0);
04482188 1139 # Add the readings
e1083e99 1140 foreach my $r ( $self->collation->readings ) {
04482188 1141 my $rid = $r->id;
1142 $newgraph->add_vertex( $rid );
1143 $self->set_equivalence( $rid, $rid );
1144 $self->set_eqreadings( $rid, [ $rid ] );
e1083e99 1145 }
04482188 1146
1147 # Now add the edges
e1083e99 1148 foreach my $e ( $self->collation->paths ) {
04482188 1149 $self->add_equivalence_edge( @$e );
e1083e99 1150 }
04482188 1151
1152 # Now equate the colocated readings. This does no testing;
1153 # it assumes that all preexisting relationships are valid.
e1083e99 1154 foreach my $rel ( $self->relationships ) {
1155 my $relobj = $self->get_relationship( $rel );
1156 next unless $relobj && $relobj->colocated;
1157 $self->_make_equivalence( @$rel );
1158 }
b6f13859 1159 $self->collation->tradition->_init_done(1);
e1083e99 1160}
1161
56772e8c 1162=head2 equivalence_ranks
1163
1164Rank all vertices in the equivalence graph, and return a hash reference with
1165vertex => rank mapping.
1166
1167=cut
1168
1169sub equivalence_ranks {
1170 my $self = shift;
1171 my $eqstart = $self->equivalence( $self->collation->start );
1172 my $eqranks = { $eqstart => 0 };
1173 my $rankeqs = { 0 => [ $eqstart ] };
1174 my @curr_origin = ( $eqstart );
1175 # A little iterative function.
1176 while( @curr_origin ) {
1177 @curr_origin = $self->_assign_rank( $eqranks, $rankeqs, @curr_origin );
1178 }
1179 return( $eqranks, $rankeqs );
1180}
1181
1182sub _assign_rank {
1183 my( $self, $node_ranks, $rank_nodes, @current_nodes ) = @_;
1184 my $graph = $self->equivalence_graph;
1185 # Look at each of the children of @current_nodes. If all the child's
1186 # parents have a rank, assign it the highest rank + 1 and add it to
1187 # @next_nodes. Otherwise skip it; we will return when the highest-ranked
1188 # parent gets a rank.
1189 my @next_nodes;
1190 foreach my $c ( @current_nodes ) {
1191 warn "Current reading $c has no rank!"
1192 unless exists $node_ranks->{$c};
1193 foreach my $child ( $graph->successors( $c ) ) {
1194 next if exists $node_ranks->{$child};
1195 my $highest_rank = -1;
1196 my $skip = 0;
1197 foreach my $parent ( $graph->predecessors( $child ) ) {
1198 if( exists $node_ranks->{$parent} ) {
1199 $highest_rank = $node_ranks->{$parent}
1200 if $highest_rank <= $node_ranks->{$parent};
1201 } else {
1202 $skip = 1;
1203 last;
1204 }
1205 }
1206 next if $skip;
1207 my $c_rank = $highest_rank + 1;
1208 # print STDERR "Assigning rank $c_rank to node $child \n";
1209 $node_ranks->{$child} = $c_rank if $node_ranks;
1210 push( @{$rank_nodes->{$c_rank}}, $child ) if $rank_nodes;
1211 push( @next_nodes, $child );
1212 }
1213 }
1214 return @next_nodes;
1215}
1216
359944f7 1217### Output logic
1218
027d819c 1219sub _as_graphml {
2626f709 1220 my( $self, $graphml_ns, $xmlroot, $node_hash, $nodeid_key, $edge_keys ) = @_;
c84275ff 1221
1222 my $rgraph = $xmlroot->addNewChild( $graphml_ns, 'graph' );
1223 $rgraph->setAttribute( 'edgedefault', 'directed' );
1224 $rgraph->setAttribute( 'id', 'relationships', );
1225 $rgraph->setAttribute( 'parse.edgeids', 'canonical' );
cc31ebaa 1226 $rgraph->setAttribute( 'parse.edges', 0 );
c84275ff 1227 $rgraph->setAttribute( 'parse.nodeids', 'canonical' );
cc31ebaa 1228 $rgraph->setAttribute( 'parse.nodes', 0 );
c84275ff 1229 $rgraph->setAttribute( 'parse.order', 'nodesfirst' );
1230
1231 # Add the vertices according to their XML IDs
2626f709 1232 my %rdg_lookup = ( reverse %$node_hash );
cc31ebaa 1233 # my @nlist = sort _by_xmlid keys( %rdg_lookup ); ## CAUSES SEGFAULT
826d8773 1234 my @nlist = sort keys( %rdg_lookup );
414cc046 1235 foreach my $n ( @nlist ) {
c84275ff 1236 my $n_el = $rgraph->addNewChild( $graphml_ns, 'node' );
1237 $n_el->setAttribute( 'id', $n );
2626f709 1238 _add_graphml_data( $n_el, $nodeid_key, $rdg_lookup{$n} );
c84275ff 1239 }
cc31ebaa 1240 $rgraph->setAttribute( 'parse.nodes', scalar @nlist );
c84275ff 1241
1242 # Add the relationship edges, with their object information
1243 my $edge_ctr = 0;
1244 foreach my $e ( sort { $a->[0] cmp $b->[0] } $self->graph->edges ) {
1245 # Add an edge and fill in its relationship info.
a30ca502 1246 next unless( exists $node_hash->{$e->[0]} && exists $node_hash->{$e->[1]} );
c84275ff 1247 my $edge_el = $rgraph->addNewChild( $graphml_ns, 'edge' );
1248 $edge_el->setAttribute( 'source', $node_hash->{$e->[0]} );
1249 $edge_el->setAttribute( 'target', $node_hash->{$e->[1]} );
1250 $edge_el->setAttribute( 'id', 'e'.$edge_ctr++ );
1251
3ae5e2ad 1252 my $rel_obj = $self->get_relationship( @$e );
bbd064a9 1253 foreach my $key ( keys %$edge_keys ) {
1254 my $value = $rel_obj->$key;
1255 _add_graphml_data( $edge_el, $edge_keys->{$key}, $value )
1256 if defined $value;
1257 }
c84275ff 1258 }
cc31ebaa 1259 $rgraph->setAttribute( 'parse.edges', $edge_ctr );
c84275ff 1260}
1261
1262sub _by_xmlid {
2626f709 1263 my $tmp_a = $a;
1264 my $tmp_b = $b;
1265 $tmp_a =~ s/\D//g;
1266 $tmp_b =~ s/\D//g;
1267 return $tmp_a <=> $tmp_b;
c84275ff 1268}
1269
1270sub _add_graphml_data {
1271 my( $el, $key, $value ) = @_;
1272 return unless defined $value;
1273 my $data_el = $el->addNewChild( $el->namespaceURI, 'data' );
1274 $data_el->setAttribute( 'key', $key );
1275 $data_el->appendText( $value );
83d5ac3a 1276}
1277
c7bd2768 1278sub dump_segment {
1279 my( $self, $from, $to ) = @_;
1280 open( DUMP, ">debug.svg" ) or die "Could not open debug.svg";
1281 binmode DUMP, ':utf8';
1282 print DUMP $self->collation->as_svg({ from => $from, to => $to, nocalc => 1 });
1283 close DUMP;
1284}
1285
63778331 1286sub throw {
1287 Text::Tradition::Error->throw(
1288 'ident' => 'Relationship error',
1289 'message' => $_[0],
1290 );
1291}
1292
22222af9 1293no Moose;
1294__PACKAGE__->meta->make_immutable;
1295
12961;