File Coverage

blib/lib/Tree/Family.pm
Criterion Covered Total %
statement 301 306 98.3
branch 79 96 82.2
condition 27 45 60.0
subroutine 38 38 100.0
pod 10 12 83.3
total 455 497 91.5


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Tree::Family - Represent and visualize a family tree.
4              
5             =cut
6              
7             =head1 SYNOPSIS
8              
9             use Tree::Family;
10              
11             my $tree = Tree::Family->new(filename => '/tmp/mytree.dmp');
12              
13             my $person = Tree::Family::Person->new(name => 'Fred');
14             my $nother = Tree::Family::Person->new(name => 'Wilma');
15              
16             $person->spouse($nother);
17              
18             $tree->add_person($person);
19             $tree->add_person($nother);
20              
21             for ($tree->people) {
22             print $_->name;
23             }
24              
25             my $dot_file = $tree->as_dot;
26              
27             =head1 DESCRIPTION
28              
29             Use this module to represent spousal and parental relationships
30             among a group of people, and generate a graphviz "dot"
31             file to visualize them.
32              
33             =head1 FUNCTIONS
34              
35             =cut
36              
37             package Tree::Family;
38 14     14   488711 use Data::Dumper;
  14         230848  
  14         1980  
39             $Data::Dumper::Sortkeys = 1; # makes diffing easier
40 14     14   131 use warnings;
  14         27  
  14         455  
41 14     14   75 use strict;
  14         32  
  14         551  
42 14     14   18469 use List::MoreUtils qw(first_index last_index uniq);
  14         23280  
  14         1691  
43 14     14   24923 use Algorithm::Permute;
  14         55838  
  14         920  
44 14     14   12316 use Clone qw(clone);
  14         13724  
  14         1993  
45 14     14   13124 use YAML::XS qw/Dump Load LoadFile/;
  14         66356  
  14         76999  
46              
47             our $VERSION = '0.02';
48             our $urlBase = 'http://localhost/';
49             our $GraphHeader = <<'';
50             graph family {
51             edge [ style=solid ];
52             node [ shape=box style=bold, color="black", fontsize="18", fontname="Times-Roman" ];
53             ranksep=2.0
54              
55             our $GraphFooter = <<'';
56             }
57              
58             our $bottomInvisibleEdges = ''; # populated and used below.
59             our $topInvisibleEdges = ''; # populated and used below.
60              
61 5990     5990 0 66041 sub debug($) {
62             # print STDERR "@_";
63             }
64              
65             =head2 new
66              
67             my $tree = Tree::Family->new(filename => '/tmp/foobarfamily.dmp');
68              
69             =cut
70              
71             sub new {
72 27     27 1 13222 my ($class,%args) = @_;
73 27 50       133 my $filename = $args{filename} or die "missing filename";
74 27         160 return bless {
75             filename => $filename,
76             }, $class;
77             }
78              
79             sub _init {
80 896     896   1190 my $self = shift;
81 896 100       3595 return if exists($self->{people});
82             # $self->{people} will be a hash from ids to T:F:Person objects
83 27 100 66     1238 if (-e $self->{filename} && -s $self->{filename}) {
84 15         45 my $filename = $self->{filename};
85 15         94 $self->{people} = LoadFile $filename;
86             } else {
87 12         40 $self->{people} = {};
88             }
89 27         8281 $self->{people} = { map { $_ => $self->{people}{$_}->Toast } keys %{ $self->{people} } };
  119         464  
  27         149  
90 27 50       396 die "error reading $self->{filename}, got (".ref($self->{people}).") error: [$!] [$@]" unless ref($self->{people}) eq 'HASH';
91             }
92              
93             #
94             # Assign numeric generations
95             #
96             sub _set_generations {
97 41     41   99 my $self = shift;
98 41         88 my %args = @_;
99 41         66 our $haveSet;
100 41 100 33     279 return if !$args{force} && $haveSet;
101 12         101 $haveSet = 1;
102 12         78 Tree::Family::Person->_clear_generations;
103 12         28 for my $person (values %{ $self->{people} }) {
  12         140  
104 751 100       2529 next if $person->generation;
105 12         65 $person->_set_all_generations(100);
106             }
107             }
108              
109             =head2 write
110              
111             Write the family tree to a file
112              
113             $tree->write
114              
115             =cut
116              
117             sub write {
118 16     16 1 136 my $self = shift;
119 16         50 $self->_init;
120 16         67 $self->_set_generations;
121 16         112 Tree::Family::Person->_clear_all_partners;
122 16         100 Tree::Family::Person->_set_all_partners;
123 16         372 my $filename = $self->{filename};
124 16         488 my $tmpfile = $filename."-tmp-".$$.time.(rand 1);
125 16         39 my %write = map { $_ => $self->{people}{$_}->Freeze } keys %{ $self->{people} };
  773         3387  
  16         284  
126 16 50       3625 open FP, ">$tmpfile" or die "Couldn't write to $tmpfile : $!";
127 16         36322 print FP Dump( \%write );
128 16         3614 close FP;
129 16 50       1715 rename $tmpfile, $filename or die "Couldn't rename $tmpfile to $filename : $!";
130 16         1982 return 1;
131             }
132              
133             =head2 add_person
134              
135             Add a person to the tree
136              
137             $tree->add_person($joe);
138              
139             $joe should be a Tree::Family::Person object.
140              
141             =cut
142              
143             sub add_person {
144 753     753 1 3891 my $self = shift;
145 753         1492 $self->_init;
146 753         1187 my $person = shift;
147 753         2457 $self->{people}{$person->get('id')} = $person;
148             }
149              
150             =head2 delete_person
151              
152             Delete a person
153              
154             $tree->delete_person($joe)
155              
156             =cut
157              
158             sub delete_person {
159 4     4 1 11 my $self = shift;
160 4         14 $self->_init;
161 4         5 my $person = shift;
162 4         16 $person->dad(undef);
163 4         15 $person->mom(undef);
164 4         14 $person->spouse(undef);
165 4         19 for ($person->partners) {
166 0         0 $person->_delete_partner($_);
167             }
168 4         20 for ($person->kids) {
169 2         12 $person->delete_kid($_);
170             }
171 4         24 delete $self->{people}->{$person->id};
172 4         23 $person->_delete_self;
173             }
174              
175             =head2 people
176              
177             Get a list of all the people in the tree
178              
179             =cut
180              
181             sub people {
182 26     26 1 493 my $self = shift;
183 26         92 $self->_init;
184 26         42 return values %{ $self->{people} };
  26         626  
185             }
186              
187             =head2 find
188              
189             Find a person, specifying keys and values to search for.
190              
191             $tree->find(id => 'sam');
192              
193             $tree->find(first_name => 'joe', last_name => 'dimaggio');
194              
195             =cut
196              
197             sub find {
198 61     61 1 37021 my ($class,%args) = @_;
199 61         188 shift->_init;
200 61         367 Tree::Family::Person->find(%args);
201             }
202              
203             =head2 min_generation
204              
205             The numeric smallest generation.
206              
207             =cut
208              
209             sub min_generation {
210 12     12 1 633 my $self = shift;
211 12         57 $self->_init;
212 12         55 $self->_set_generations;
213 12         98 Tree::Family::Person->min_generation;
214             }
215              
216             =head2 max_generation
217              
218             The numeric highest generation.
219              
220             =cut
221              
222             sub max_generation {
223 13     13 1 740 my $self = shift;
224 13         41 $self->_init;
225 13         44 $self->_set_generations;
226 13         75 Tree::Family::Person->max_generation;
227             }
228              
229             =head2 write_dotfile
230              
231             Write out a .dot file (graphviz format).
232              
233             $tree->write("output.dot");
234              
235             =cut
236              
237             sub write_dotfile {
238 9     9 1 5077 my ($self,$filename) = @_;
239 9 50       44 die "missing filename" unless $filename;
240 9         168 my $tmpfile = $filename."-tmp-".$$.time.(rand 1);
241 9 50       1161 open FP, ">$tmpfile" or die "Couldn't write to $tmpfile : $!";
242 9         52 print FP $self->as_dot;
243 9 50       975 close FP or die "couldn't close FP : $!";
244 9 50       1207 rename $tmpfile, $filename or die "Couldn't rename $tmpfile to $filename : $!";
245 9         48 return 1;
246             }
247              
248             #
249             # _add_person_and_all_ascendants
250             #
251             # Add a person and all their ascendants to the .dot output
252             #
253             sub _add_person_and_all_ascendants {
254 1470     1470   3434 my ($class,$person,$person2subgraph,$people_written,$subgraph_written,$all_subgraphs,$person2subgraphpeople) = @_;
255 1470         1791 my $output = '';
256 1470 50 33     5846 die "no person id " if defined($person) && !defined($person->id);
257 1470 100       4918 return $output if $people_written->{$person->id};
258 740         2200 debug "adding person and all ascendants for ".$person->first_name."\n";
259              
260             # Find the subgraph containing dad (and hence mom), and then call ourself
261             # recursively for every person in that subgraph.
262 740         1195 my $people;
263 740 100       2003 $people = $person2subgraphpeople->{ $person->mom->id } if $person->mom;
264 740 100 66     2289 $people ||= $person2subgraphpeople->{ $person->dad->id } if $person->dad;
265 740 100 66     2676 if ($people && @$people) {
266 361         1049 debug "Found subgraph for parents of ".$person->first_name." : ".
267             (join ',', map $_->first_name, @$people)."\n"
268             } else {
269 379         1094 debug "No ascendants for ".$person->first_name."\n";
270             }
271             # annoying dot hacks to untangle the generation above us.
272 740 100 66     2709 if ($person->spouse() &&
      66        
      66        
      66        
273             ($person->mom && $person->dad) &&
274             ($person->spouse->mom && $person->spouse->dad)) {
275             # TODO also for partners (not spouse?)
276 340         1051 my $parent_node = _kid_node($person->mom,$person->dad);
277 340         554 my $edges;
278 340 100       1056 if (had_kids($person,$person->spouse)) {
279 308         944 $edges = _kid_node($person,$person->spouse())." -- $parent_node [style=invis];\n";
280             } else {
281 32         148 $edges = $person->id." -- $parent_node [style=invis];\n";
282 32         111 $edges .= $person->spouse()->id." -- $parent_node [style=invis];\n";
283             }
284              
285 340 100 66     1199 if ($person->mom->spouse() || $person->dad->spouse()) {
286 338         748 $bottomInvisibleEdges .= $edges;
287             } else {
288 2         7 $topInvisibleEdges .= $edges;
289             }
290             }
291            
292 740 100       1154 for (@{ $people || [] }) {
  740         3057  
293 730         1919 $output .= $class->_add_person_and_all_ascendants($_,$person2subgraph,$people_written,$subgraph_written,$all_subgraphs,$person2subgraphpeople);
294             }
295 740         1920 $output .= $class->_person_node($person)."\n";
296 740         2665 my $subgraph_index = $person2subgraph->{$person->id};
297 740 100       2664 $output .= $all_subgraphs->[$subgraph_index] unless $subgraph_written->{$subgraph_index};
298 740         1494 $subgraph_written->{$subgraph_index} = 1;
299 740         1984 $people_written->{$person->id} = 1;
300 740         5360 return $output;
301             }
302              
303             =head2 as_dot
304              
305             Return the text for a .dot graphviz file
306              
307             print $tree->as_dot
308              
309             =cut
310              
311             sub as_dot {
312 11     11 1 1430 my $class = shift;
313 11         50 debug "as dot called\n";
314 11         44 $class->_init;
315 4331 50 33     15549 my @people = sort {
316 11         49 warn "generation for $a or $b not set" unless defined($a->get('generation')) && defined($b->get('generation'));
317 4331         12351 $a->get('generation') <=> $b->get('generation') } $class->people;
318 11         54 my $output;
319              
320             # Make subgraphs for people with partners/spouses
321             my %person2subgraph; # map from person id to the dot text
322 0         0 my @all_subgraphs;
323 0         0 my %generation_subgraphs; # keys are generations, values are arrays of arrays of people who are in a subgraph.
324 0         0 my %person2subgraphpeople; # map from person id to an array of people in the subgraph
325 11         34 for my $person (@people) {
326 740 100       2571 next if $person2subgraph{$person->get('id')};
327 383         961 my @together = $class->_partner_and_marriage_group($person);
328 383         1422 debug "doing subgraph for : ".(join ',', map $_->first_name, @together)."\n";
329 383 50       984 next unless @together > 0;
330 383         1486 $person2subgraph{$_->get('id')} = scalar(@all_subgraphs) for @together;
331 383         1268 push @all_subgraphs, $class->_partner_subgraph(\@together);
332 383         1431 debug "best ordering : ".(join ',', map $_->first_name, @together)."\n";
333 383         1537 $person2subgraphpeople{$_->get('id')} = \@together for @together;
334             }
335            
336             # People
337 11         27 my %people_written; # keeps track of people who have been written
338             my %subgraph_written; # ids of subgraphs that have been written
339 0         0 my %people_by_generation;
340 11         30 for (@people) {
341 740         1010 push @{ $people_by_generation{$_->get('generation')} }, $_;
  740         2168  
342             }
343             # starting with the bottom-most generation, do depth-first traversals to add
344             # all ascendants and their partner subgraphs.
345             # This also builds $bottomInvisibleEdges. If this isn't on the
346             # bottom of the graph, dot segfaults.
347             # maybe on the top? TODO
348             # if it isn't on the top, they're in the wrong place
349 11         37 $bottomInvisibleEdges = '';
350 11         92 for (sort {$b <=> $a } keys %people_by_generation) {
  21         107  
351 27         60 my $this_generation = $people_by_generation{$_};
352 27 50       73 next unless $this_generation;
353 27         107 debug "adding generation $_\n";
354 27         57 for my $person (@$this_generation) {
355 740         2227 debug "starting generation with person ".$person->first_name."\n";
356 740         2037 $output .= $class->_add_person_and_all_ascendants($person,\%person2subgraph,\%people_written,\%subgraph_written,\@all_subgraphs,\%person2subgraphpeople);
357             }
358             }
359 11 50       129 die "unwritten subgraphs, should not happen" if grep {!$_} values %subgraph_written;
  373         583  
360              
361             # Parent edges
362 11         32 for my $person (@people) {
363 740         1977 my $parent_key = join '_', map $_->get('id'), grep defined, ($person->dad,$person->mom);
364 740 100       1741 next unless $parent_key;
365 361         1001 $output .= "$parent_key -- ".$person->get('id')." // Parents of ".$person->get('id')."\n";
366             }
367            
368             # Generations
369 11         66 my $min_generation = $class->min_generation;
370 11         199 my $max_generation = $class->max_generation;
371 11         94 $output .= "/* generations : ".$min_generation." to ".$max_generation." */\n";
372 11         35 my @generation_nodes;
373 11         20 my $i = 0;
374 11         58 for my $g ($min_generation .. $max_generation) {
375 27         70 my $generation_node = "generation_".(++$i);
376 27         58 push @generation_nodes, $generation_node;
377 27         92 my @this = $class->find(generation => $g);
378 27 100       150 my $which = $g==$min_generation ? 'source' : $g==$max_generation ? 'sink' : 'same';
    100          
379 27         136 $output .= "{rank=$which; $generation_node ".
380             (join ' ',map $_->get('id'), @this)."}\n";
381             }
382             # Now add an invisible edge between the first member of each generation.
383 11         27 my $generation_edges;
384 11         39 $generation_edges .= join "--", @generation_nodes;
385 11         39 $generation_edges .= "[style=invis];\n";
386 11         112 for (@generation_nodes) {
387 27         156 $generation_edges .= qq{$_ [label="" style=invis];\n};
388             }
389              
390 11         2175 return join "\n",$GraphHeader,$topInvisibleEdges,$generation_edges,$output,$bottomInvisibleEdges,$GraphFooter;
391             }
392              
393             # All people who are connected to a given person via marriage or partnership
394             # ...and all people connected to those people, etc.
395             sub _partner_and_marriage_group {
396 386     386   608 my ($class, $person ) = @_;
397 386         724 my @all = ($person);
398 386         1148 my @add_me = $person->partners_and_spouse;
399 386         1340 debug "partners and spouse for ".$person->id." : ".@add_me."\n";
400             #debug (join ',',map $_->id, @add_me)."\n";
401 386         1118 while (@add_me) {
402 410         745 push @all, @add_me;
403 410         791 my @just_added = @add_me;
404 410         580 @add_me = ();
405 410         675 for (@just_added) {
406 443         1245 for my $p ($_->partners_and_spouse) {
407 489 100       725 next if grep { $p eq $_ } @all;
  1298         6118  
408 62         241 push @add_me, $p;
409             }
410             }
411             }
412 386         659 my %uniq = map { ( $_->get('id') => $_ ) } @all;
  829         2370  
413 386         1889 return values %uniq;
414             }
415              
416             #
417             # _remove_duplicates
418             #
419             # Given a list of pairs of people, return a list of
420             # unique unordered pairs. e.g. given
421             # ( [a,b], [b,a], [c,d] )
422             # return ( [a,b], [c,d] )
423             # where a,b,c,d are person objects.
424             #
425             sub _remove_duplicates {
426 368     368   610 my @edges = @_;
427 368         437 my @ret;
428             my %h;
429 368         609 for my $e (@edges) {
430 772 100       2449 next if $h{$e->[0]->id,$e->[1]->id}++;
431 384 50       1301 next if $h{$e->[1]->id,$e->[0]->id}++;
432 384         771 push @ret, $e;
433             }
434 368         1505 return @ret;
435             }
436              
437             #
438             # _distance
439             #
440             # a metric on a list of ordered pairs :
441             #
442             # The sum of the difference between the first and last positions of each
443             # unique element, e.g.
444             #
445             # ( [a,b], [b,c], [c,d] ) == a -- b b -- c c -- d
446             # 0 1 2 3 4 5
447             # 0-0 (a) + 2-1 (b) + 4-3 (c) + 5-5 (d) == 2
448             #
449             # ( [a,b], [c,d], [b,c] ) == a -- b c -- d b -- c
450             # 0 1 2 3 4 5
451             # 0-0 (a) + (4-2) b + (5-2) c + 3-3 (d) = 5
452             #
453             # a,b,c,d are Tree::Family::Person objects
454             #
455             sub _distance {
456 192     192   434 my @edges = @_;
457 192         843 my @flattenned = map @$_, @edges;
458 192         348 my %seen;
459 192         273 my $distance = 0;
460 192         266 for my $m (@flattenned) {
461 960 100       3043 next if $seen{$m->id}++;
462 672     1712   3593 my $first = first_index { $_->id eq $m->id } @flattenned;
  1712         8017  
463 672     1712   5317 my $last = last_index { $_->id eq $m->id } @flattenned;
  1712         5431  
464 672         2737 $distance += ($last - $first);
465             }
466 192         790 return $distance;
467             }
468              
469             #
470             #_are_married
471             #
472             #_are_married($joe,$sue)
473             #
474             #returns true iff $joe and $sue are married
475             #
476             sub _are_married {
477 1332     1332   1647 my ($a,$b) = @_;
478 1332   100     3946 return ($a->spouse() && $b->spouse() && $a->spouse->id eq $b->id);
479             }
480              
481             #
482             # return --- or -+- for two people depending on whether they
483             # are married or not.
484             #
485             sub _ascii_pair {
486 948     948   1698 my ($a,$b) = @_;
487 948 100       1821 if (_are_married($a,$b)) {
488 681         1963 return join '-+-', $a->id, $b->id;
489             }
490 267         881 return join '---', $a->id, $b->id;
491             }
492              
493             #
494             # parameters : an array ref of pairs of people
495             # returns : nothing, but puts 'em in a decent order, to minimize the
496             # distance between elements of the pairs.
497             #
498             # e.g. given ( [d,c], [a,b], [b,c] )
499             # the best ordering would be one of
500             # ( [a,b], [b,c], [c,d] )
501             # ( [d,c], [c,b], [b,a] )
502             # since then they could appear like so:
503             # a -- b -- c -- d
504             #
505             sub _find_best_ordering {
506 368     368   632 my @pairs = @_;
507 368         1232 debug "-- finding best ordering of ".@pairs." marriages/partnerships\n";
508 368 100       1410 return @pairs unless @pairs > 1;
509 14         24 my $min_distance;
510 14         41 my @best = @pairs;
511 14         155 my $i = Algorithm::Permute->new(\@pairs);
512 14         70 my @m = $i->next;
513 14         28 do {
514 36         159 debug "-- starting with permutation : ".(join ' ', map _ascii_pair(@$_), @m)."\n";
515             # flip the order of each possible edge
516 36         246 for my $b (0..(2**(@m)-1)) {
517 192         630 debug "-- b is $b\n";
518 192         4344 my $m = clone \@m;
519 192         309 my $k = 0;
520 192         449 for (@$m) {
521 480 100       1738 $_ = [$_->[1],$_->[0]] if $b & (1 << $k++);
522             }
523 192         451 my $d = _distance(@$m);
524 192         552 debug " -- distance for ".(join ' ', map _ascii_pair(@$_), @$m)." : $d\n";
525 192 100 100     2413 if (!defined($min_distance) || $d < $min_distance) {
526 32         55 $min_distance = $d;
527 32         183 @best = @$m;
528             }
529             }
530 36         407 @m = $i->next;
531             } until (!@m);
532 14         67 debug "-- best distance : $min_distance\n";
533 14         146 return @best;
534             }
535              
536             #
537             # make a subgraph of people who are partners (i.e. married or had kids together)
538             # also rearranges @people
539             #
540             sub _partner_subgraph {
541 383     383   570 my ($class,$people) = @_;
542 383         792 my @people = @$people;
543 383 100       858 return '' if @$people==1;
544 368         414 my @marriages;
545             my @parentships;
546 368         587 for my $p (@people) {
547 752 100       2212 push @marriages, [ $p, $p->spouse() ] if $p->spouse;
548 752         2453 push @parentships, [ $p, $_ ] for $p->partners;
549             }
550 368         897 my @cluster = (@marriages, @parentships);
551 368         791 @cluster = _remove_duplicates(@cluster);
552 368         813 @cluster = _find_best_ordering(@cluster);
553              
554 368         1079 my $best = join ' ', map _ascii_pair(@$_), @cluster;
555 368         1118 debug "** best ordering : $best\n";
556              
557 368         1255 my $graph_name = join '_and_', map $_->get('id'), @people;
558 368         994 my $output = "subgraph cluster_$graph_name {\n /* $best */\ncolor=white;\n";
559              
560 368         633 for my $e (@cluster) {
561 384 100       817 if (_are_married(@$e)) {
562 365         1015 $output .= $class->_marriage_subgraph(@$e);
563             } else {
564 19         89 $output .= $class->_parent_edge(@$e);
565             }
566             }
567              
568 368 50 33     2302 return "" unless $output && $output =~ /\w/;
569 368         1881 return $output." } \n";
570             }
571              
572             #
573             # intersect two array refs
574             #
575             sub _intersection {
576             # probably a little slow compared to perldoc -q intersect, but can we use objects as hash keys?
577 705     705   1059 my ($a,$b) = @_;
578 705         811 my @i;
579 705         1253 for my $x (@$b) {
580 687 50       1439 die "undefs in intersection" unless defined $x;
581 687 100       1164 push @i, $x if grep { $_ eq $x } @$a;
  782         3693  
582             }
583 705         2453 return @i;
584             }
585              
586             #
587             # node from which a kid comes; a --+-- b
588             # |
589             # kid
590             # the "+" is the kid node.
591             #
592             sub _kid_node {
593 996     996   2000 my ($a,$b) = @_;
594 996 50 33     4402 die "no kid node for single parents" unless ($a && $b);
595 996 100       2772 ($a,$b) = ($b,$a) if $b->get('gender') eq 'm';
596 996         3106 return join '_',$a->get('id'),$b->get('id');
597             }
598              
599             #
600             sub had_kids {
601 705     705 0 973 my ($a,$b) = @_;
602 705         2041 my $x = [map $_->id, $a->kids ];
603 705         2522 my $y = [map $_->id, $b->kids ];
604 705         3210 debug "intersecting ".Dumper($x,$y);
605 705         8054 my @i = _intersection($x,$y);
606 705         2269 debug "number of kids shared by ".$a->id." and ".$b->id." is ".@i."\n";
607 705         3099 return (@i > 0);
608             }
609              
610             sub _marriage_subgraph {
611 365     365   552 my ($class,$x,$y) = @_;
612 365         1171 my ($one,$two) = map $_->get('id'), ($x,$y);
613 365         537 my $graph;
614             my %k;
615 365 100       723 if (had_kids($x,$y)) {
616 329         663 my $kid_node = _kid_node($x,$y);
617 329         1016 $graph = "$one -- $kid_node -- $two; rank=same;$one $two $kid_node;";
618 329         622 $graph .= qq+\n$kid_node [label="",width=.01,height=.01]+;
619             } else {
620 36         111 $graph = "$one -- $two; rank=same;$one $two;";
621             }
622 365         2459 return "subgraph marriage_${one}_${two} {\nedge [style=bold]; $graph }\n",
623             }
624              
625             sub _parent_edge {
626             # Draw an edge between two people who had a kid together
627 19     19   40 my ($class,$x,$y) = @_;
628 19         81 my ($one,$two) = map $_->get('id'), ($x,$y);
629 19         88 my $kid_node = _kid_node($x,$y);
630 19         170 return join "\n",
631             "edge [style=dotted]; $one -- $kid_node -- $two { rank=same;$one $two $kid_node }",
632             "$kid_node [ shape=point ]";
633             }
634              
635             sub _person_node {
636 740     740   1121 my ($class, $person) = @_;
637 740         927 our $urlBase;
638 740 100       1923 return $person->id . " ["
639             . ($person->get('gender') eq 'm' ? 'color="#093AB5"' : 'color="#C666B8"')
640             . ' label = "'
641             . $class->_person_label($person)
642             . qq|" href="$urlBase?id=|
643             . $person->id . '"];';
644             }
645              
646             sub _person_label {
647 740     740   2037 my ($class,$p) = @_;
648 740         1942 return join ' ', grep defined($_), $p->get('first_name'), $p->get('last_name');
649             }
650              
651             sub DESTROY {
652 27     27   19159 %Tree::Family::Person::globalHash = ();
653             }
654              
655             =head1 SEE ALSO
656              
657             Tree::Family::Person
658             family.cgi (in this distribution)
659              
660             =head1 AUTHOR
661              
662             Brian Duggan, C<< >>
663              
664             =head1 BUGS
665              
666             graphviz uses a lot of heuristics to create a nice layout. This package
667             attempts to micro-manage the contents of the dot file in order to produce
668             a nice layout, while still letting graphviz do the brunt of the work.
669             This approach doesn't always produce optimal results. Patches welcome.
670              
671             =head1 COPYRIGHT & LICENSE
672              
673             Copyright 2006 Brian Duggan, all rights reserved.
674              
675             This program is free software; you can redistribute it and/or modify it
676             under the same terms as Perl itself.
677              
678             =cut
679              
680             1;