File Coverage

blib/lib/HTML/Timeline.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package HTML::Timeline;
2              
3             # Author:
4             # Ron Savage
5             #
6             # Note:
7             # \t = 4 spaces || die.
8              
9 1     1   22984 use strict;
  1         3  
  1         43  
10 1     1   6 use warnings;
  1         2  
  1         63  
11              
12             require 5.005_62;
13              
14             require Exporter;
15              
16             # Warning: This list must include format and gedobj, unlike the list in sub new(),
17             # since those 2 special cases are attributes which are not available to the caller.
18              
19 0           use accessors::classic qw/
20             ancestors
21             everyone
22             format
23             gedcom_file
24             gedobj
25             include_spouses
26             missing_as_table
27             output_dir
28             root_person
29             template_dir
30             template_name
31             timeline_height
32             url_for_xml
33             verbose
34             web_page
35             xml_file
36 1     1   332 /;
  0            
37             use Carp;
38             use Gedcom;
39             use Gedcom::Date;
40             use HTML::Template;
41             use Path::Class;
42              
43             our @ISA = qw(Exporter);
44              
45             # Items to export into callers namespace by default. Note: do not export
46             # names by default without a very good reason. Use EXPORT_OK instead.
47             # Do not simply export all your public functions/methods/constants.
48              
49             # This allows declaration use HTML::Timeline ':all';
50             # If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
51             # will save memory.
52             our %EXPORT_TAGS = ( 'all' => [ qw(
53              
54             ) ] );
55              
56             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
57              
58             our @EXPORT = qw(
59              
60             );
61              
62             our $VERSION = '1.08';
63              
64             # -----------------------------------------------
65              
66             sub clean_persons_name
67             {
68             my($self, $name) = @_;
69              
70             # Find /s everwhere (/g) and remove them.
71              
72             $name =~ s|/||g;
73              
74             return $name;
75              
76             } # End of clean_persons_name.
77              
78             # -----------------------------------------------
79              
80             sub generate_xml_file
81             {
82             my($self, $people) = @_;
83             my($missing_message) = 'People excluded because of missing birth dates: ';
84             my($todays_date) = 1900 + (localtime() )[5];
85              
86             # Process each person.
87              
88             my($birth_date);
89             my($death_date);
90             my($earliest_date, $extracted_date);
91             my(@missing);
92             my($name, %notes);
93             my($person);
94             my($result);
95             my(%seen);
96             my(@xml);
97              
98             push @xml, '';
99              
100             for $person (@$people)
101             {
102             $name = $person -> get_value('name');
103              
104             if ($seen{$name})
105             {
106             $self -> log(sprintf($self -> format(), 'Note', "$name appears twice in the input file") );
107              
108             next;
109             }
110              
111             $seen{$name} = 1;
112             $name = $self -> clean_persons_name($name);
113             $birth_date = $person -> get_value('birth date');
114             $death_date = $person -> get_value('death date');
115              
116             # Process birth dates.
117              
118             if (Gedcom::Date -> parse($birth_date) )
119             {
120             $notes{$name} = '';
121              
122             if ($earliest_date && ($birth_date < $earliest_date) )
123             {
124             $earliest_date = $birth_date;
125             }
126             elsif (! $earliest_date)
127             {
128             $earliest_date = $birth_date;
129             }
130             }
131             elsif ($birth_date)
132             {
133             $notes{$name} = "Fuzzy birthdate: $birth_date";
134             ($extracted_date = $birth_date) =~ /(\d{4})/;
135              
136             if ($extracted_date)
137             {
138             $birth_date = $extracted_date;
139             }
140              
141             if ($earliest_date && ($birth_date < $earliest_date) )
142             {
143             $earliest_date = $birth_date;
144             }
145             elsif (! $earliest_date)
146             {
147             $earliest_date = $birth_date;
148             }
149             }
150             else
151             {
152             push @missing,
153             {
154             death_date => $death_date,
155             name => $name,
156             };
157              
158             next;
159             }
160              
161             # Process death dates.
162              
163             if (Gedcom::Date::parse($death_date) )
164             {
165             # James Riley Durbin's death date (FEB 1978) is parseable by ParseDate
166             # but not Similie Timeline, so we only extract the year.
167              
168             if ($name eq 'James Riley Durbin')
169             {
170             ($extracted_date = $death_date) =~ /(\d{4})/;
171              
172             if ($extracted_date)
173             {
174             $death_date = $extracted_date;
175             }
176             }
177             }
178             elsif ($death_date)
179             {
180             ($extracted_date = $death_date) =~ /(\d{4})/;
181              
182             if ($extracted_date)
183             {
184             $death_date = $extracted_date;
185             }
186             }
187              
188             if ($birth_date && $death_date)
189             {
190             push @xml, qq| $notes{$name}|;
191             }
192             elsif ($birth_date)
193             {
194             push @xml, qq| $notes{$name}|;
195             }
196             }
197              
198             if ( ($self -> missing_as_table() == 0) && ($#missing >= 0) )
199             {
200             my($missing) = join(', ', map{$$_{'name'} } @missing);
201              
202             push @xml, qq| $missing|;
203             }
204              
205             push @xml, '';
206              
207             # Write timeline.xml.
208              
209             my($output_dir) = $self -> output_dir();
210             my($output_file_name) = $self -> xml_file();
211              
212             if ($output_dir)
213             {
214             $output_file_name = file($output_dir, $output_file_name);
215             }
216              
217             open(my $fh, "> $output_file_name") || Carp::croak "Can't open(> $output_file_name): $!";
218             print $fh join("\n", @xml), "\n";
219             close $fh;
220              
221             $self -> log(sprintf($self -> format(), 'Created', $output_file_name) );
222              
223             # Write timeline.html.
224              
225             my($template) = HTML::Template -> new(filename => $self -> template_name(), path => $self -> template_dir() );
226             my($url_for_xml) = $self -> url_for_xml();
227             $output_file_name = $self -> xml_file();
228              
229             if ($url_for_xml)
230             {
231             $output_file_name = "$url_for_xml/$output_file_name"; # No Path::Class here.
232             }
233              
234             $template -> param(earliest_date => $earliest_date);
235             $template -> param(missing_as_table => $self -> missing_as_table() );
236             $template -> param(timeline_height => $self -> timeline_height() );
237             $template -> param(xml_file_name => $output_file_name);
238              
239             if ($#missing >= 0)
240             {
241             if ($self -> missing_as_table() == 1)
242             {
243             $template -> param(missing => $missing_message);
244             $template -> param(missing_loop => [map{ { death_date => $$_{'death_date'}, name => $$_{'name'} } } @missing]);
245             }
246             else
247             {
248             $template -> param(todays_date => $todays_date);
249             }
250             }
251              
252             $output_file_name = $self -> web_page();
253              
254             if ($output_dir)
255             {
256             $output_file_name = file($output_dir, $output_file_name);
257             }
258              
259             open(my $fh, "> $output_file_name") || Carp::croak "Can't open(> $output_file_name): $!";
260             print $fh $template -> output();
261             close $fh;
262              
263             $self -> log(sprintf($self -> format(), 'Created', $output_file_name) );
264              
265             } # End of generate_xml_file.
266              
267             # -----------------------------------------------
268              
269             sub get_spouses
270             {
271             my($self, $people) = @_;
272             my($spouses) = [];
273              
274             my($person);
275             my($spouse);
276              
277             for my $person (@$people)
278             {
279             $spouse = $person -> spouse();
280              
281             if ($spouse)
282             {
283             push @$spouses, $spouse;
284             }
285             }
286              
287             return $spouses;
288              
289             } # End of get_spouses.
290              
291             # -----------------------------------------------
292              
293             sub log
294             {
295             my($self, $message) = @_;
296              
297             if ($self -> verbose() )
298             {
299             print STDERR "$message\n";
300             }
301              
302             } # End of log.
303              
304             # -----------------------------------------------
305              
306             sub new
307             {
308             my($class, %arg) = @_;
309             my($self) = bless({}, $class);
310             # Warning: This list must not contain: format or gedobj,
311             # since these are attributes not available to the caller.
312             my(@options) = (qw/
313             ancestors
314             everyone
315             gedcom_file
316             include_spouses
317             missing_as_table
318             output_dir
319             root_person
320             template_dir
321             template_name
322             timeline_height
323             url_for_xml
324             verbose
325             web_page
326             xml_file
327             /);
328              
329             # Set defaults.
330              
331             $self -> ancestors(0);
332             $self -> everyone(0);
333             $self -> format('%-16s: %s'); # Not in the @options array!
334             $self -> gedcom_file('bach.ged');
335             $self -> gedobj(''); # Not in the @options array!
336             $self -> include_spouses(0);
337             $self -> missing_as_table(0);
338             $self -> output_dir('');
339             $self -> root_person('Johann Sebastian Bach');
340             $self -> template_dir('.');
341             $self -> template_name('timeline.tmpl');
342             $self -> timeline_height(500);
343             $self -> url_for_xml('');
344             $self -> verbose(0);
345             $self -> web_page('timeline.html');
346             $self -> xml_file('timeline.xml');
347              
348             # Check ~/.timelinerc for more defaults.
349              
350             my($resource_file_name) = "$ENV{'HOME'}/.timelinerc";
351              
352             if (-e $resource_file_name)
353             {
354             require "Config/IniFiles.pm";
355              
356             my($config) = Config::IniFiles -> new(-file => $resource_file_name);
357             my($section_name) = 'HTML::Timeline';
358              
359             if (! $config -> SectionExists($section_name) )
360             {
361             Carp::croak "Error: Section '$section_name' is missing from $resource_file_name";
362             }
363              
364             my($option);
365             my($value);
366              
367             for $option (@options)
368             {
369             $value = $config -> val($section_name, $option);
370              
371             if (defined $value)
372             {
373             $self -> $option($value);
374             }
375             }
376             }
377              
378             # Process user options.
379              
380             my($attr_name);
381              
382             for $attr_name (@options)
383             {
384             if (exists($arg{'options'}{$attr_name}) )
385             {
386             $self -> $attr_name($arg{'options'}{$attr_name});
387             }
388             }
389              
390             if (! -f $self -> gedcom_file() )
391             {
392             Carp::croak 'Cannot find file: ' . $self -> gedcom_file();
393             }
394              
395             $self -> gedobj
396             (
397             Gedcom -> new
398             (
399             callback => undef,
400             gedcom_file => $self -> gedcom_file(),
401             grammar_version => '5.5',
402             read_only => 1,
403             )
404             );
405              
406             if (! $self -> gedobj() -> validate() )
407             {
408             Carp::croak 'Cannot validate file: ' . $self -> gedcom_file();
409             }
410              
411             $self -> log('Parameters:');
412              
413             for $attr_name (@options)
414             {
415             $self -> log(sprintf($self -> format(), $attr_name, $self -> $attr_name() ) );
416             }
417              
418             $self -> log('-' x 50);
419              
420             return $self;
421              
422             } # End of new.
423              
424             # -----------------------------------------------
425              
426             sub run
427             {
428             my($self) = @_;
429              
430             $self -> log('Processing:');
431              
432             my($root_person) = $self -> gedobj() -> get_individual($self -> root_person() );
433             my($name) = $self -> clean_persons_name($root_person -> name() );
434              
435             my(@people);
436              
437             if ($self -> everyone() == 1)
438             {
439             @people = $self -> gedobj() -> individuals();
440             }
441             else
442             {
443             my($method) = $self -> ancestors() == 1 ? 'ancestors' : 'descendents';
444             @people = $root_person -> $method();
445              
446             $self -> log(sprintf($self -> format(), 'Relationship', $method) );
447              
448             if ($self -> ancestors() == 0)
449             {
450             # If descendents are wanted, check for spouses.
451              
452             if ($self -> include_spouses() == 1)
453             {
454             push @people, @{$self -> get_spouses([$root_person, @people])};
455             }
456             }
457             else
458             {
459             # If ancestors are wanted, check for siblings.
460              
461             push @people, $root_person -> siblings();
462             }
463              
464             unshift @people, $root_person;
465             }
466              
467             $self -> generate_xml_file(\@people);
468             $self -> log('Success');
469              
470             return 0;
471              
472             } # End of run.
473              
474             # -----------------------------------------------
475              
476             1;
477              
478             =head1 NAME
479              
480             HTML::Timeline - Convert a Gedcom file into a Timeline file
481              
482             =head1 Synopsis
483              
484             shell> bin/timeline.pl -h
485              
486             =head1 Description
487              
488             C is a pure Perl module.
489              
490             =head1 Distributions
491              
492             This module is available as a Unix-style distro (*.tgz).
493              
494             See http://savage.net.au/Perl-modules.html for details.
495              
496             See http://savage.net.au/Perl-modules/html/installing-a-module.html for
497             help on unpacking and installing.
498              
499             =head1 Constructor and initialization
500              
501             new(...) returns an object of type C.
502              
503             This is the class contructor.
504              
505             Usage: C<< HTML::Timeline -> new() >>.
506              
507             This method takes a hashref of options.
508              
509             Call C as C<< new({option_1 => value_1, option_2 => value_2, ...}) >>.
510              
511             See the next section for a discussion of the resource file $HOME/.timelinerc,
512             which can be used to override the default values for options.
513              
514             Available options:
515              
516             =over 4
517              
518             =item ancestors
519              
520             If this option is 1, the ancestors of the root_person (see below) are processed.
521              
522             If this option is 0, their descendents are processed.
523              
524             The default is 0.
525              
526             =item everyone
527              
528             If this option is 1, everyone is processed, and the root_person (see below) is ignored.
529              
530             If this option is 0, the root_person is processed.
531              
532             The default is 0.
533              
534             =item gedcom_file
535              
536             This takes the name of your input Gedcom file.
537              
538             The default is bach.ged.
539              
540             =item include_spouses
541              
542             If this option is 1, and descendents are processed, spouses are included.
543              
544             If this option is 0, spouses are ignored.
545              
546             The default is 0.
547              
548             =item missing_as_table
549              
550             If this option is 1, people with missing birthdates are listed under the timeline, in a table.
551              
552             If this option is 0, such people appear on the timeline, with a date (today) as their birthdate.
553              
554             =item output_dir a_dir_name
555              
556             If this option is used, the output HTML and XML files will be created in this directory.
557              
558             =item root_person
559              
560             The name of the person on which to base the timeline.
561              
562             The default is 'Johann Sebastian Bach'.
563              
564             =item template_dir a_dir_name
565              
566             If this option is used, HTML::Template will look in this directory for 'timeline.tmpl'.
567              
568             If this option is not used, the current directory will be used.
569              
570             =item template_name a_file_name
571              
572             If this option is used, HTML::Template will look for a file of this name.
573              
574             If this option is not used, 'timeline.tmpl' will be used.
575              
576             =item url_for_xml a_url
577              
578             If this option is used, it becomes the prefix of the name of the output XML file written into
579             timeline.html.
580              
581             If this option is not used, no prefix is used.
582              
583             =item verbose
584              
585             This takes either a 0 or a 1.
586              
587             Write more or less progress messages to STDERR.
588              
589             The default value is 0.
590              
591             =item web_page a_file_name
592              
593             If this option is used, it specfies the name of the HTML file to write.
594              
595             If this option is not used, 'timeline.html' is written.
596              
597             See the output_dir option for where the file is written.
598              
599             =item xml_file
600              
601             The name of your XML output file.
602              
603             The default value is 'timeline.xml'.
604              
605             Note: The name of the XML file is embedded in timeline.html, at line 28.
606             You will need to edit the latter file if you use a different name for your XML output file.
607              
608             =back
609              
610             =head1 The resource file $HOME/.timelinerc
611              
612             The program looks for a file called $HOME/.timelinerc during execution of the constructor.
613              
614             If this file is present, the module Config::IniFiles is loaded to process it.
615              
616             If the file is absent, Config::IniFiles does not have to be installed.
617              
618             This file must contain the section [HTML::Timeline], after which can follow any number
619             of options, as listed above.
620              
621             The option names in the file do I start with hyphens.
622              
623             If the same option appears two or more times, the I appearence is used to set the value
624             of that option.
625              
626             The values override the defaults listed above.
627              
628             These values are, in turn, overridden by the values passed in to the constructor.
629              
630             This means that command line options passed in to timeline.pl will override the values
631             found in $HOME/.timelinerc.
632              
633             Sample file:
634              
635             [HTML::Timeline]
636             output_dir=/var/www/html
637              
638             =head1 Method: log($message)
639              
640             If C was called as C<< new({verbose => 1}) >>, write the message to STDERR.
641              
642             If C was called as C<< new({verbose => 0}) >> (the default), do nothing.
643              
644             =head1 Method: run()
645              
646             Do everything.
647              
648             See C for an example of how to call C.
649              
650             =head1 See also
651              
652             The C module.
653              
654             =head1 Support
655              
656             Support is via the Gedcom mailing list.
657              
658             Subscribe via perl-gedcom-subscribe@perl.org.
659              
660             =head1 Credits
661              
662             The MIT Simile Timeline project, and others, are at http://code.google.com/p/simile-widgets/.
663              
664             Its original home is at http://simile.mit.edu/timeline.
665              
666             Philip Durbin write the program examples/ged2xml.pl, which Ron Savage converted into a module.
667              
668             Philip also supplied the files examples/bach.* and examples/timeline.html.
669              
670             Ron Savage wrote bin/timeline.pl.
671              
672             examples/timeline.xml is the output of this program, using the default options.
673              
674             =head1 Repository
675              
676             L
677              
678             =head1 Author
679              
680             C was written by Ron Savage Iron@savage.net.auE> in 2008.
681              
682             Home page: http://savage.net.au/index.html
683              
684             =head1 Copyright
685              
686             Australian copyright (c) 2008, Ron Savage.
687              
688             All Programs of mine are 'OSI Certified Open Source Software';
689             you can redistribute them and/or modify them under the terms of
690             The Artistic License, a copy of which is available at:
691             http://www.opensource.org/licenses/index.html
692              
693             =cut