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