add failing tests for relationship logic, next we fix them
[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(
34 'name' => 'inline',
35 'input' => 'CollateX',
36 'file' => $cxfile,
37 );
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
3ae5e2ad 88=head2 get_relationship
89
90Return the relationship object, if any, that exists between two readings.
91
92=cut
93
94sub get_relationship {
4633f9e4 95 my $self = shift;
96 my @vector;
97 if( @_ == 1 && ref( $_[0] ) eq 'ARRAY' ) {
98 # Dereference the edge arrayref that was passed.
99 my $edge = shift;
100 @vector = @$edge;
101 } else {
102 @vector = @_;
103 }
3ae5e2ad 104 my $relationship;
105 if( $self->graph->has_edge_attribute( @vector, 'object' ) ) {
106 $relationship = $self->graph->get_edge_attribute( @vector, 'object' );
ca6e6095 107 }
3ae5e2ad 108 return $relationship;
109}
110
111sub _set_relationship {
112 my( $self, $relationship, @vector ) = @_;
113 $self->graph->add_edge( @vector );
114 $self->graph->set_edge_attribute( @vector, 'object', $relationship );
115}
a1615ee4 116
22222af9 117=head2 create
118
119Create a new relationship with the given options and return it.
120Warn and return undef if the relationship cannot be created.
121
122=cut
123
124sub create {
125 my( $self, $options ) = @_;
126 # Check to see if a relationship exists between the two given readings
127 my $source = delete $options->{'orig_a'};
128 my $target = delete $options->{'orig_b'};
3ae5e2ad 129 my $rel = $self->get_relationship( $source, $target );
130 if( $rel ) {
3d14b48e 131 if( $rel->type eq 'collated' ) {
132 # Always replace a 'collated' relationship with a more descriptive
133 # one, if asked.
134 $self->del_relationship( $source, $target );
135 } elsif( $rel->type ne $options->{'type'} ) {
63778331 136 throw( "Another relationship of type " . $rel->type
137 . " already exists between $source and $target" );
22222af9 138 } else {
139 return $rel;
140 }
141 }
142
143 # Check to see if a nonlocal relationship is defined for the two readings
144 $rel = $self->scoped_relationship( $options->{'reading_a'},
145 $options->{'reading_b'} );
146 if( $rel && $rel->type eq $options->{'type'} ) {
147 return $rel;
148 } elsif( $rel ) {
63778331 149 throw( sprintf( "Relationship of type %s with scope %s already defined for readings %s and %s", $rel->type, $rel->scope, $options->{'reading_a'}, $options->{'reading_b'} ) );
22222af9 150 } else {
151 $rel = Text::Tradition::Collation::Relationship->new( $options );
152 $self->add_scoped_relationship( $rel ) if $rel->nonlocal;
153 return $rel;
154 }
155}
156
157=head2 add_scoped_relationship( $rel )
158
159Keep track of relationships defined between specific readings that are scoped
160non-locally. Key on whichever reading occurs first alphabetically.
161
162=cut
163
164sub add_scoped_relationship {
165 my( $self, $rel ) = @_;
f222800e 166 my $rdga = $rel->type eq 'orthographic' ? $rel->reading_a : lc( $rel->reading_a );
167 my $rdgb = $rel->type eq 'orthographic' ? $rel->reading_b : lc( $rel->reading_b );
168 my $r = $self->scoped_relationship( $rdga, $rdgb );
22222af9 169 if( $r ) {
170 warn sprintf( "Scoped relationship of type %s already exists between %s and %s",
f222800e 171 $r->type, $rdga, $rdgb );
22222af9 172 return;
173 }
f222800e 174 my( $first, $second ) = sort ( $rdga, $rdgb );
175 $self->scopedrels->{$first}->{$second} = $rel;
22222af9 176}
177
178=head2 scoped_relationship( $reading_a, $reading_b )
179
180Returns the general (document-level or global) relationship that has been defined
181between the two reading strings. Returns undef if there is no general relationship.
182
183=cut
184
185sub scoped_relationship {
186 my( $self, $rdga, $rdgb ) = @_;
187 my( $first, $second ) = sort( $rdga, $rdgb );
188 if( exists $self->scopedrels->{$first}->{$second} ) {
189 return $self->scopedrels->{$first}->{$second};
190 } else {
191 return undef;
192 }
193}
194
195=head2 add_relationship( $self, $source, $sourcetext, $target, $targettext, $opts )
196
197Adds the relationship specified in $opts (see Text::Tradition::Collation::Relationship
198for the possible options) between the readings given in $source and $target. Sets
199up a scoped relationship between $sourcetext and $targettext if the relationship is
200scoped non-locally.
201
202Returns a status boolean and a list of all reading pairs connected by the call to
203add_relationship.
204
6d381462 205=begin testing
206
207use Text::Tradition;
208use TryCatch;
209
210my $t1 = Text::Tradition->new( 'input' => 'Self', 'file' => 't/data/legendfrag.xml' );
211# Test 1: try to equate nodes that are prevented with an intermediate collation
212ok( $t1, "Parsed test fragment file" );
213my $c1 = $t1->collation;
214my $trel = $c1->get_relationship( '9,2', '9,3' );
215is( ref( $trel ), 'Text::Tradition::Collation::Relationship',
216 "Troublesome relationship exists" );
217is( $trel->type, 'collated', "Troublesome relationship is a collation" );
218
219# Try to make the link we want
220try {
221 $c1->add_relationship( '8,6', '10,3', { 'type' => 'orthographic' } );
222 ok( 1, "Added cross-collation relationship as expected" );
223} catch {
224 ok( 0, "Existing collation blocked equivalence relationship" );
225}
226
227try {
228 $c1->calculate_ranks();
229 ok( 1, "Successfully calculated ranks" );
230} catch {
231 ok( 0, "Collation now has a cycle" );
232}
233
234# Test 2: try to equate nodes that are prevented with a real intermediate
235# equivalence
236
237my $t2 = Text::Tradition->new( 'input' => 'Self', 'file' => 't/data/legendfrag.xml' );
238# Test 1: try to equate nodes that are prevented with an intermediate collation
239my $c2 = $t2->collation;
240$c2->add_relationship( '9,2', '9,3', { 'type' => 'lexical' } );
241my $trel2 = $c2->get_relationship( '9,2', '9,3' );
242is( ref( $trel2 ), 'Text::Tradition::Collation::Relationship',
243 "Created blocking relationship" );
244is( $trel2->type, 'lexical', "Blocking relationship is not a collation" );
245# This time the link ought to fail
246try {
247 $c2->add_relationship( '8,6', '10,3', { 'type' => 'orthographic' } );
248 ok( 0, "Existing equivalence blocked crossing relationship" );
249} catch {
250 ok( 1, "Added cross-equivalent bad relationship" );
251}
252
253try {
254 $c2->calculate_ranks();
255 ok( 1, "Successfully calculated ranks" );
256} catch {
257 ok( 0, "Collation now has a cycle" );
258}
259
260=end testing
261
22222af9 262=cut
263
264sub add_relationship {
265 my( $self, $source, $source_rdg, $target, $target_rdg, $options ) = @_;
266
ca6e6095 267 my $relationship;
268 my $thispaironly;
269 if( ref( $options ) eq 'Text::Tradition::Collation::Relationship' ) {
270 $relationship = $options;
271 $thispaironly = 1; # If existing rel, set only where asked.
272 } else {
273 # Check the options
274 $options->{'scope'} = 'local' unless $options->{'scope'};
bf6e338d 275 $options->{'scope'} = 'local' if $options->{'type'} eq 'collated';
8d5c8893 276 $options->{'scope'} = 'local' if $options->{'type'} eq 'transposition';
ca6e6095 277
278 my( $is_valid, $reason ) =
279 $self->relationship_valid( $source, $target, $options->{'type'} );
280 unless( $is_valid ) {
281 throw( "Invalid relationship: $reason" );
282 }
283
284 # Try to create the relationship object.
285 $options->{'reading_a'} = $source_rdg->text;
286 $options->{'reading_b'} = $target_rdg->text;
287 $options->{'orig_a'} = $source;
288 $options->{'orig_b'} = $target;
0ac5e750 289 if( $options->{'scope'} ne 'local' ) {
290 # Is there a relationship with this a & b already?
f222800e 291 # Case-insensitive for non-orthographics.
292 my $rdga = $options->{'type'} eq 'orthographic'
293 ? $options->{'reading_a'} : lc( $options->{'reading_a'} );
294 my $rdgb = $options->{'type'} eq 'orthographic'
295 ? $options->{'reading_b'} : lc( $options->{'reading_b'} );
296 my $otherrel = $self->scoped_relationship( $rdga, $rdgb );
0ac5e750 297 if( $otherrel && $otherrel->type eq $options->{type}
298 && $otherrel->scope eq $options->{scope} ) {
299 warn "Applying existing scoped relationship";
300 $relationship = $otherrel;
301 }
302 }
303 $relationship = $self->create( $options ) unless $relationship; # Will throw on error
22222af9 304 }
ca6e6095 305
22222af9 306
307 # Find all the pairs for which we need to set the relationship.
bf6e338d 308 my @vectors = [ $source, $target ];
ca6e6095 309 if( $relationship->colocated && $relationship->nonlocal && !$thispaironly ) {
bf6e338d 310 push( @vectors, $self->_find_applicable( $relationship ) );
f222800e 311 }
bf6e338d 312
22222af9 313 # Now set the relationship(s).
314 my @pairs_set;
315 foreach my $v ( @vectors ) {
3ae5e2ad 316 my $rel = $self->get_relationship( @$v );
ca6e6095 317 if( $rel && $rel ne $relationship ) {
63778331 318 if( $rel->nonlocal ) {
319 throw( "Found conflicting relationship at @$v" );
a2ed66b2 320 } elsif( $rel->type ne 'collated' ) {
321 # Replace a collation relationship; leave any other sort in place.
13e893dc 322 my $r1ann = $rel->has_annotation ? $rel->annotation : '';
323 my $r2ann = $relationship->has_annotation ? $relationship->annotation : '';
324 unless( $rel->type eq $relationship->type && $r1ann eq $r2ann ) {
325 warn sprintf( "Not overriding local relationship %s with global %s "
326 . "set at %s -> %s (%s -> %s)", $rel->type, $relationship->type,
327 @$v, $rel->reading_a, $rel->reading_b );
328 next;
329 }
63778331 330 }
22222af9 331 }
8d5c8893 332 map { $self->_drop_collations( $_ ) } @$v;
3ae5e2ad 333 $self->_set_relationship( $relationship, @$v );
22222af9 334 push( @pairs_set, $v );
335 }
336
63778331 337 return @pairs_set;
22222af9 338}
339
9d829138 340=head2 del_scoped_relationship( $reading_a, $reading_b )
341
342Returns the general (document-level or global) relationship that has been defined
343between the two reading strings. Returns undef if there is no general relationship.
344
345=cut
346
347sub del_scoped_relationship {
348 my( $self, $rdga, $rdgb ) = @_;
349 my( $first, $second ) = sort( $rdga, $rdgb );
350 return delete $self->scopedrels->{$first}->{$second};
351}
352
bf6e338d 353sub _find_applicable {
354 my( $self, $rel ) = @_;
355 my $c = $self->collation;
356 # TODO Someday we might use a case sensitive language.
357 my $lang = $c->tradition->language;
358 my @vectors;
359 my @identical_readings;
360 if( $rel->type eq 'orthographic' ) {
361 @identical_readings = grep { $_->text eq $rel->reading_a }
362 $c->readings;
363 } else {
364 @identical_readings = grep { lc( $_->text ) eq lc( $rel->reading_a ) }
365 $c->readings;
366 }
367 foreach my $ir ( @identical_readings ) {
368 my @itarget;
369 if( $rel->type eq 'orthographic' ) {
370 @itarget = grep { $_->rank == $ir->rank
371 && $_->text eq $rel->reading_b } $c->readings;
372 } else {
373 @itarget = grep { $_->rank == $ir->rank
374 && lc( $_->text ) eq lc( $rel->reading_b ) } $c->readings;
375 }
376 if( @itarget ) {
377 # Warn if there is more than one hit with no orth link between them.
378 my $itmain = shift @itarget;
379 if( @itarget ) {
380 my %all_targets;
381 map { $all_targets{$_} = 1 } @itarget;
382 map { delete $all_targets{$_} }
383 $self->related_readings( $itmain,
384 sub { $_[0]->type eq 'orthographic' } );
385 warn "More than one unrelated reading with text " . $itmain->text
386 . " at rank " . $ir->rank . "!" if keys %all_targets;
387 }
388 push( @vectors, [ $ir->id, $itmain->id ] );
389 }
390 }
391 return @vectors;
392}
393
ee801e17 394=head2 del_relationship( $source, $target )
395
396Removes the relationship between the given readings. If the relationship is
397non-local, removes the relationship everywhere in the graph.
398
399=cut
400
401sub del_relationship {
402 my( $self, $source, $target ) = @_;
403 my $rel = $self->get_relationship( $source, $target );
681893aa 404 return () unless $rel; # Nothing to delete; return an empty set.
ee801e17 405 my @vectors = ( [ $source, $target ] );
406 $self->_remove_relationship( $source, $target );
407 if( $rel->nonlocal ) {
408 # Remove the relationship wherever it occurs.
9d829138 409 # Remove the relationship wherever it occurs.
ee801e17 410 my @rel_edges = grep { $self->get_relationship( @$_ ) == $rel }
411 $self->relationships;
412 foreach my $re ( @rel_edges ) {
413 $self->_remove_relationship( @$re );
414 push( @vectors, $re );
415 }
9d829138 416 $self->del_scoped_relationship( $rel->reading_a, $rel->reading_b );
ee801e17 417 }
418 return @vectors;
419}
420
ca6e6095 421sub _remove_relationship {
422 my( $self, @vector ) = @_;
423 $self->graph->delete_edge( @vector );
424}
425
22222af9 426=head2 relationship_valid( $source, $target, $type )
427
428Checks whether a relationship of type $type may exist between the readings given
429in $source and $target. Returns a tuple of ( status, message ) where status is
430a yes/no boolean and, if the answer is no, message gives the reason why.
431
432=cut
433
434sub relationship_valid {
435 my( $self, $source, $target, $rel ) = @_;
436 my $c = $self->collation;
437 if ( $rel eq 'transposition' || $rel eq 'repetition' ) {
438 # Check that the two readings do (for a repetition) or do not (for
439 # a transposition) appear in the same witness.
2f39215b 440 # TODO this might be called before witness paths are set...
22222af9 441 my %seen_wits;
442 map { $seen_wits{$_} = 1 } $c->reading_witnesses( $source );
443 foreach my $w ( $c->reading_witnesses( $target ) ) {
444 if( $seen_wits{$w} ) {
445 return ( 0, "Readings both occur in witness $w" )
446 if $rel eq 'transposition';
447 return ( 1, "ok" ) if $rel eq 'repetition';
d6936dea 448 }
22222af9 449 }
450 return $rel eq 'transposition' ? ( 1, "ok" )
451 : ( 0, "Readings occur only in distinct witnesses" );
2f39215b 452 } else {
22222af9 453 # Check that linking the source and target in a relationship won't lead
2f39215b 454 # to a path loop for any witness. If they have the same rank then fine.
455 return( 1, "ok" )
456 if $c->reading( $source )->has_rank
457 && $c->reading( $target )->has_rank
458 && $c->reading( $source )->rank == $c->reading( $target )->rank;
a1615ee4 459
460 # Otherwise, first make a lookup table of all the
22222af9 461 # readings related to either the source or the target.
462 my @proposed_related = ( $source, $target );
778251a6 463 # Drop the collation links of source and target, unless we want to
464 # add a collation relationship.
465 foreach my $r ( ( $source, $target ) ) {
2f39215b 466 $self->_drop_collations( $r ) unless $rel eq 'collated';
778251a6 467 push( @proposed_related, $self->related_readings( $r, 'colocated' ) );
468 }
22222af9 469 my %pr_ids;
470 map { $pr_ids{ $_ } = 1 } @proposed_related;
471
a1615ee4 472 # The cumulative predecessors and successors of the proposed-related readings
473 # should not overlap.
474 my %all_pred;
475 my %all_succ;
22222af9 476 foreach my $pr ( keys %pr_ids ) {
a1615ee4 477 map { $all_pred{$_} = 1 } $c->sequence->all_predecessors( $pr );
478 map { $all_succ{$_} = 1 } $c->sequence->all_successors( $pr );
479 }
480 foreach my $k ( keys %all_pred ) {
2f39215b 481 return( 0, "Relationship would create witness loop" )
482 if exists $all_succ{$k};
a1615ee4 483 }
484 foreach my $k ( keys %pr_ids ) {
2f39215b 485 return( 0, "Relationship would create witness loop" )
486 if exists $all_pred{$k} || exists $all_succ{$k};
a1615ee4 487 }
22222af9 488 return ( 1, "ok" );
489 }
490}
491
778251a6 492sub _drop_collations {
493 my( $self, $reading ) = @_;
494 foreach my $n ( $self->graph->neighbors( $reading ) ) {
495 if( $self->get_relationship( $reading, $n )->type eq 'collated' ) {
496 $self->del_relationship( $reading, $n );
497 }
498 }
499}
500
7f52eac8 501=head2 related_readings( $reading, $filter )
22222af9 502
503Returns a list of readings that are connected via relationship links to $reading.
7f52eac8 504If $filter is set to a subroutine ref, returns only those related readings where
505$filter( $relationship ) returns a true value.
22222af9 506
507=cut
508
509sub related_readings {
7f52eac8 510 my( $self, $reading, $filter ) = @_;
22222af9 511 my $return_object;
512 if( ref( $reading ) eq 'Text::Tradition::Collation::Reading' ) {
513 $reading = $reading->id;
514 $return_object = 1;
515 }
c84275ff 516 my @answer;
7f52eac8 517 if( $filter ) {
518 # Backwards compat
519 if( $filter eq 'colocated' ) {
520 $filter = sub { $_[0]->colocated };
521 }
c84275ff 522 my %found = ( $reading => 1 );
523 my $check = [ $reading ];
524 my $iter = 0;
525 while( @$check ) {
c84275ff 526 my $more = [];
527 foreach my $r ( @$check ) {
528 foreach my $nr ( $self->graph->neighbors( $r ) ) {
7f52eac8 529 if( &$filter( $self->get_relationship( $r, $nr ) ) ) {
c84275ff 530 push( @$more, $nr ) unless exists $found{$nr};
531 $found{$nr} = 1;
532 }
533 }
534 }
535 $check = $more;
22222af9 536 }
7f52eac8 537 delete $found{$reading};
c84275ff 538 @answer = keys %found;
539 } else {
540 @answer = $self->graph->all_reachable( $reading );
22222af9 541 }
542 if( $return_object ) {
543 my $c = $self->collation;
c84275ff 544 return map { $c->reading( $_ ) } @answer;
22222af9 545 } else {
c84275ff 546 return @answer;
22222af9 547 }
548}
549
550=head2 merge_readings( $kept, $deleted );
551
552Makes a best-effort merge of the relationship links between the given readings, and
553stops tracking the to-be-deleted reading.
554
555=cut
556
557sub merge_readings {
558 my( $self, $kept, $deleted, $combined ) = @_;
559 foreach my $edge ( $self->graph->edges_at( $deleted ) ) {
560 # Get the pair of kept / rel
561 my @vector = ( $kept );
562 push( @vector, $edge->[0] eq $deleted ? $edge->[1] : $edge->[0] );
563 next if $vector[0] eq $vector[1]; # Don't add a self loop
564
565 # If kept changes its text, drop the relationship.
566 next if $combined;
567
f222800e 568 # If kept / rel already has a relationship, just keep the old
3ae5e2ad 569 my $rel = $self->get_relationship( @vector );
f222800e 570 next if $rel;
22222af9 571
572 # Otherwise, adopt the relationship that would be deleted.
3ae5e2ad 573 $rel = $self->get_relationship( @$edge );
574 $self->_set_relationship( $rel, @vector );
22222af9 575 }
576 $self->delete_reading( $deleted );
577}
578
027d819c 579sub _as_graphml {
2626f709 580 my( $self, $graphml_ns, $xmlroot, $node_hash, $nodeid_key, $edge_keys ) = @_;
c84275ff 581
582 my $rgraph = $xmlroot->addNewChild( $graphml_ns, 'graph' );
583 $rgraph->setAttribute( 'edgedefault', 'directed' );
584 $rgraph->setAttribute( 'id', 'relationships', );
585 $rgraph->setAttribute( 'parse.edgeids', 'canonical' );
586 $rgraph->setAttribute( 'parse.edges', scalar($self->graph->edges) );
587 $rgraph->setAttribute( 'parse.nodeids', 'canonical' );
588 $rgraph->setAttribute( 'parse.nodes', scalar($self->graph->vertices) );
589 $rgraph->setAttribute( 'parse.order', 'nodesfirst' );
590
591 # Add the vertices according to their XML IDs
2626f709 592 my %rdg_lookup = ( reverse %$node_hash );
593 foreach my $n ( sort _by_xmlid keys( %rdg_lookup ) ) {
c84275ff 594 my $n_el = $rgraph->addNewChild( $graphml_ns, 'node' );
595 $n_el->setAttribute( 'id', $n );
2626f709 596 _add_graphml_data( $n_el, $nodeid_key, $rdg_lookup{$n} );
c84275ff 597 }
598
599 # Add the relationship edges, with their object information
600 my $edge_ctr = 0;
601 foreach my $e ( sort { $a->[0] cmp $b->[0] } $self->graph->edges ) {
602 # Add an edge and fill in its relationship info.
a30ca502 603 next unless( exists $node_hash->{$e->[0]} && exists $node_hash->{$e->[1]} );
c84275ff 604 my $edge_el = $rgraph->addNewChild( $graphml_ns, 'edge' );
605 $edge_el->setAttribute( 'source', $node_hash->{$e->[0]} );
606 $edge_el->setAttribute( 'target', $node_hash->{$e->[1]} );
607 $edge_el->setAttribute( 'id', 'e'.$edge_ctr++ );
608
3ae5e2ad 609 my $rel_obj = $self->get_relationship( @$e );
bbd064a9 610 foreach my $key ( keys %$edge_keys ) {
611 my $value = $rel_obj->$key;
612 _add_graphml_data( $edge_el, $edge_keys->{$key}, $value )
613 if defined $value;
614 }
c84275ff 615 }
616}
617
618sub _by_xmlid {
2626f709 619 my $tmp_a = $a;
620 my $tmp_b = $b;
621 $tmp_a =~ s/\D//g;
622 $tmp_b =~ s/\D//g;
623 return $tmp_a <=> $tmp_b;
c84275ff 624}
625
626sub _add_graphml_data {
627 my( $el, $key, $value ) = @_;
628 return unless defined $value;
629 my $data_el = $el->addNewChild( $el->namespaceURI, 'data' );
630 $data_el->setAttribute( 'key', $key );
631 $data_el->appendText( $value );
83d5ac3a 632}
633
63778331 634sub throw {
635 Text::Tradition::Error->throw(
636 'ident' => 'Relationship error',
637 'message' => $_[0],
638 );
639}
640
22222af9 641no Moose;
642__PACKAGE__->meta->make_immutable;
643
6441;