File Coverage

blib/lib/Graph/Timeline.pm
Criterion Covered Total %
statement 158 164 96.3
branch 84 94 89.3
condition 9 18 50.0
subroutine 15 15 100.0
pod 5 5 100.0
total 271 296 91.5


line stmt bran cond sub pod time code
1             package Graph::Timeline;
2              
3 1     1   1133 use strict;
  1         2  
  1         49  
4 1     1   5 use warnings;
  1         2  
  1         51  
5              
6 1     1   880 use Date::Calc;
  1         36482  
  1         2313  
7              
8             our $VERSION = '1.5';
9              
10             sub new {
11 2     2 1 827 my ($class) = @_;
12              
13 2 100       20 die "Timeline->new() takes no arguments" if scalar(@_) != 1;
14              
15 1         2 my $self = {};
16              
17 1         3 $self->{_pool} = ();
18              
19 1         5 return bless $self, $class;
20             }
21              
22             sub add_interval {
23 15 100   15 1 5099 die "Timeline->add_interval() expected HASH as parameter" unless scalar(@_) % 2 == 1;
24              
25 14         50 my ( $self, %data ) = @_;
26              
27 14         41 %data = $self->_lowercase_keys(%data);
28 14         44 $self->_required_keys( 'add_interval', \%data, (qw/start end label/) );
29 11         29 $self->_valid_keys( 'add_interval', \%data, (qw/start end label group id url/) );
30              
31 7         9 $data{type} = 'interval';
32              
33 7         19 $self->_add_to_pool(%data);
34             }
35              
36             sub add_point {
37 8 100   8 1 3575 die "Timeline->add_point() expected HASH as parameter" unless scalar(@_) % 2 == 1;
38              
39 7         19 my ( $self, %data ) = @_;
40              
41 7         21 %data = $self->_lowercase_keys(%data);
42 7         30 $self->_required_keys( 'add_point', \%data, (qw/start label/) );
43 5         17 $self->_valid_keys( 'add_point', \%data, (qw/start label group id/) );
44              
45 1         2 $data{type} = 'point';
46 1         3 $data{end} = $data{start};
47              
48 1         6 $self->_add_to_pool(%data);
49             }
50              
51             sub window {
52 17     17 1 20226 my ( $self, %data ) = @_;
53              
54             # Default values for our parameters
55              
56 17         24 $self->{_window_start} = undef;
57 17         22 $self->{_window_end} = undef;
58              
59 17         18 $self->{_window_start_in} = undef;
60 17         18 $self->{_window_end_in} = undef;
61              
62 17         15 $self->{_window_span} = undef;
63              
64 17         18 $self->{_window_callback} = undef;
65              
66 17         47 %data = $self->_lowercase_keys(%data);
67 17         54 $self->_valid_keys( 'window', \%data, (qw/start end start_in end_in span callback/) );
68              
69             # Additional validation
70              
71 14 100       32 if ( $data{span} ) {
72 4 100 100     29 die "Timeline->window() 'span' can only be defined with a 'start' and 'end'" unless $data{start} and $data{end};
73             }
74              
75 12 100       24 if ( $data{callback} ) {
76 2 100       10 die "Timeline->window() 'callback' can only be a CODE reference" unless ref( $data{callback} ) eq 'CODE';
77             }
78              
79 11         21 foreach my $key ( keys %data ) {
80 24         62 $self->{"_window_$key"} = $data{$key};
81             }
82             }
83              
84             sub data {
85 13     13 1 726 my ($self) = @_;
86              
87 13 100       33 die "Timeline->data() takes no arguments" if scalar(@_) != 1;
88              
89             # Set the start and end, this make things easier
90              
91 12 100       28 my $start = ( $self->{_window_start} ? $self->{_window_start} : '0000/00/00T00:00:00' );
92 12 100       22 my $end = ( $self->{_window_end} ? $self->{_window_end} : '9999/99/99T23:59:59' );
93              
94 12         13 my @results;
95              
96 12 100       23 if ( $self->{_window_start} ) {
97 7         7 my $x;
98 7         14 $x->{start} = $self->{_window_start};
99 7         12 $x->{start_start} = $self->{_window_start};
100 7         10 $x->{start_end} = $self->{_window_start};
101              
102 7         10 $x->{end} = $self->{_window_start};
103 7         10 $x->{end_start} = $self->{_window_start};
104 7         10 $x->{end_end} = $self->{_window_start};
105              
106 7         7 $x->{type} = 'marker';
107              
108 7         10 push( @results, $x );
109             }
110              
111 12 100       25 if ( $self->{_window_end} ) {
112 7         6 my $x;
113 7         12 $x->{start} = $self->{_window_end};
114 7         9 $x->{start_start} = $self->{_window_end};
115 7         10 $x->{start_end} = $self->{_window_end};
116              
117 7         9 $x->{end} = $self->{_window_end};
118 7         10 $x->{end_start} = $self->{_window_end};
119 7         8 $x->{end_end} = $self->{_window_end};
120              
121 7         9 $x->{type} = 'marker';
122              
123 7         9 push( @results, $x );
124             }
125              
126 12         13 foreach my $record ( @{ $self->{_pool} } ) {
  12         21  
127 85 100       249 if ( $record->{start} lt $start ) {
    100          
128 15 100       30 if ( $record->{end} lt $start ) {
    100          
129 7         10 next;
130             }
131             elsif ( $record->{end} lt $end ) {
132 3 100       8 next unless $self->{_window_end_in};
133             }
134             else {
135 5 100       12 next unless $self->{_window_span};
136             }
137             }
138             elsif ( $record->{start} lt $end ) {
139 56 100       102 if ( $record->{end} gt $end ) {
140 2 100       6 next unless $self->{_window_start_in};
141             }
142             }
143             else {
144 14         16 next;
145             }
146              
147 57 100       93 if ( $self->{_window_callback} ) {
148 7 100       6 next unless &{ $self->{_window_callback} }($record);
  7         16  
149             }
150              
151 54 100       104 if ( $record->{start} lt $start ) {
152 2         4 $record->{start} = $start;
153 2         4 $record->{start_start} = $start;
154 2         3 $record->{start_end} = $start;
155             }
156              
157 54 100       88 if ( $record->{end} gt $end ) {
158 2         5 $record->{end} = $end;
159 2         3 $record->{end_start} = $end;
160 2         3 $record->{end_end} = $end;
161             }
162              
163 54         67 push( @results, $record );
164             }
165              
166 12         47 return @results;
167             }
168              
169             sub _add_to_pool {
170 8     8   21 my ( $self, %data ) = @_;
171              
172 8         23 my @newpool;
173 8         10 my $todo = 1;
174              
175 8         25 %data = $self->_set_range( 'start', %data );
176 8         28 %data = $self->_set_range( 'end', %data );
177              
178 8 50       33 $data{group} = '--unknown--' unless $data{group};
179              
180 8         8 foreach my $record ( @{ $self->{_pool} } ) {
  8         18  
181 28 100 100     83 if ( $todo and $record->{start} gt $data{start} ) {
182 5         6 push @newpool, \%data;
183 5         6 $todo = undef;
184             }
185 28         40 push @newpool, $record;
186             }
187              
188 8 100       20 push @newpool, \%data if $todo;
189              
190 8         30 $self->{_pool} = \@newpool;
191             }
192              
193             sub _valid_keys {
194 33     33   73 my ( $self, $caller, $data, @keys ) = @_;
195              
196 33         36 my @testkeys = keys %{$data};
  33         74  
197 33         47 my %validkeys = map { $_ => $_ } @keys;
  188         321  
198              
199 33         58 foreach my $key (@testkeys) {
200 90 100       230 die "Timeline->$caller() invalid key '$key' passed as data" unless $validkeys{$key};
201             }
202              
203 30         45 foreach my $key ( (qw/start end/) ) {
204 55 100       144 if ( $data->{$key} ) {
205 44 100       95 $data->{$key} = $self->_today() if $data->{$key} eq 'present';
206 44 100       78 die "Timeline->$caller() invalid date for '$key'" unless $self->_date_valid( $data->{$key} );
207             }
208             }
209              
210 23 100 100     120 if ( $data->{start} and $data->{end} ) {
211 14 100       67 die "Timeline->$caller() 'start' and 'end' are in the wrong order" if $data->{start} gt $data->{end};
212             }
213             }
214              
215             sub _date_valid {
216 44     44   61 my ( $self, $date ) = @_;
217              
218 44         92 my ( $date_part, $time_part ) = split( 'T', $date );
219 44         142 my ( $year, $month, $day ) = split( '[\/-]', $date_part );
220              
221             ## Check the date first
222              
223 44 100       83 $month = '01' unless $month;
224 44 100       59 $day = '01' unless $day;
225              
226 44 100       209 return unless $year =~ m/^\d+$/;
227 39 100       101 return unless $month =~ m/^\d+$/;
228 38 100       91 return unless $day =~ m/^\d+$/;
229              
230 37         32 my $valid;
231 37         37 eval { $valid = Date::Calc::check_date( $year, $month, $day ); };
  37         86  
232              
233 37 50       495 return unless $valid;
234              
235             ## Check the optional time part
236              
237 37 50       57 if ($time_part) {
238 0         0 my ( $hours, $minutes, $seconds ) = split( ':', $time_part );
239              
240 0 0 0     0 return unless 0 <= $hours and $hours <= 23;
241 0 0 0     0 return unless 0 <= $minutes and $minutes <= 59;
242 0 0 0     0 return unless 0 <= $seconds and $seconds <= 59;
243             }
244              
245 37         114 return 1;
246             }
247              
248             sub _required_keys {
249 21     21   47 my ( $self, $caller, $data, @keys ) = @_;
250              
251 21         32 foreach my $key (@keys) {
252 52 100       165 die "Timeline->$caller() missing key '$key'" unless $data->{$key};
253             }
254             }
255              
256             sub _lowercase_keys {
257 38     38   83 my ( $self, %data ) = @_;
258              
259 38         64 my %newdata = map { lc($_) => $data{$_} } keys %data;
  106         261  
260              
261 38         195 return %newdata;
262             }
263              
264             sub _today {
265 1     1   237 my ( $year, $month, $day ) = ( localtime() )[ 5, 4, 3 ];
266              
267 1         3 $year += 1900;
268 1         2 $month += 1;
269              
270 1         7 return sprintf( "%4d/%02d/%02d", $year, $month, $day );
271             }
272              
273             sub _set_range {
274 16     16   37 my ( $self, $label, %record ) = @_;
275              
276 16         32 my ( $date_part, $time_part ) = split( 'T', $record{$label} );
277 16         50 my ( $year, $month, $day ) = split( '[\/-]', $date_part );
278              
279 16 100       32 if ($day) {
    100          
280 14         23 $record{"${label}_start"} = $date_part;
281 14         25 $record{"${label}_end"} = $date_part;
282             }
283             elsif ($month) {
284 1         4 $record{"${label}_start"} = "$year/$month/01";
285 1         5 $record{"${label}_end"} = "$year/$month/" . Date::Calc::Days_in_Month( $year, $month );
286             }
287             else {
288 1         3 $record{"${label}_start"} = "$year/01/01";
289 1         2 $record{"${label}_end"} = "$year/12/31";
290             }
291              
292 16 50       39 if($time_part) {
293 0         0 $record{"${label}_start"} .= "T" . $time_part;
294 0         0 $record{"${label}_end"} .= "T" . $time_part;
295             }
296             else {
297 16         21 $record{"${label}_start"} .= "T00:00:00";
298 16         26 $record{"${label}_end"} .= "T23:59:59";
299             }
300              
301 16         110 return %record;
302             }
303              
304             1;
305              
306             =head1 NAME
307              
308             Graph::Timeline - Render timeline data
309              
310             =head1 VERSION
311              
312             This document refers to verion 1.5 of Graph::Timeline, released September 29, 2009
313              
314             =head1 SYNOPSIS
315              
316             This class takes a list of events and processes them so that they can be rendered in
317             various graphical formats by subclasses of this class.
318              
319             =head1 DESCRIPTION
320              
321             =head2 Overview
322              
323             The purpose of this class is to organise the data that will be used to render a timeline. Events fall into two types.
324             Intervals, which has a start and an end. For example Albert Einstein was born on 1879/03/14 and died on 1955/04/18, this would be
325             stored as an interval. His works were publicly burned by the Nazi's on 1933/05/10 for being 'of un-German spirit', I guess
326             being Jewish didn't help either. So this event would be marked as a point.
327              
328             You feed events into the class using add_interval( ) and add_point( ), then use window( ) to select which events you want to
329             render and then call data( ) to get the relevant events. This last bit will be done in the subclass.
330              
331             =head2 Constructors and initialisation
332              
333             =over 4
334              
335             =item new( )
336              
337             The constructor takes no arguments and just initialises a few basic variables.
338              
339             =back
340              
341             =head2 Public methods
342              
343             =over 4
344              
345             =item add_interval( HASH )
346              
347             Inserts an event that has a start and an end point into the list at the corrct position. The
348             hash contains the following keys, some of which are required.
349              
350             =over 4
351              
352             =item start [ REQUIRED ]
353              
354             The start date for the interval in the for 'YYYY/MM/DD' or the word 'present' which will be converted into todays date.
355             Dates in the format YYYY will be taken to span YYYY/01/01 until YYYY/12/31 and dates of the format YYYY/MM will span
356             YYYY/MM/01 until YYYY/MM/xx where xx is the last day of MM in YYYY.
357              
358             =item end [ REQUIRED ]
359              
360             The start end for the interval in the for 'YYYY/MM/DD' or the word 'present' which will be converted into todays date.
361             Dates in the format YYYY will be taken to span YYYY/01/01 until YYYY/12/31 and dates of the format YYYY/MM will span
362             YYYY/MM/01 until YYYY/MM/xx where xx is the last day of MM in YYYY.
363              
364             =item label [ REQUIRED ]
365              
366             The text string that will be displayed when the event is rendered
367              
368             =item id [ OPTIONAL ]
369              
370             A unique id for the render, Graph::Timeline does not validate this field for uniqueness
371              
372             =item group [ OPTIONAL ]
373              
374             A string is used to group related events together, Graph::Timeline does not validate this field
375              
376             =back
377              
378             =item add_point( HASH )
379              
380             The same as add_interval( ) except that the event occurs on just one day and therefore does not require an end date. Interval and point events are rendered differently.
381              
382             =item window( HASH )
383              
384             Set up the data to be selected from the event pool. To reset the defaults just call without any parameters.
385              
386             =over 4
387              
388             =item start
389              
390             Select only record that start on or after this date. Takes a valid date or the word 'present' which is
391             translated to the current date.
392              
393             =item end
394              
395             Select only record that end on or before this date. Takes a valid date or the word 'present' which is
396             translated to the current date.
397              
398             =item start_in
399              
400             If end is set then include records that start before the end date but ends after the end date.
401              
402             =item end_in
403              
404             If start is set then include records that start before the start date but end after the start date.
405              
406             =item span
407              
408             If start and end are both set then additionally report events that start before the start date and end
409             after the end date.
410              
411             =item callback
412              
413             A code reference to provide additionaly custom filtering. The callback will be passed a hash reference with the
414             following keys: start, end, label, group, id and type ('interval' and 'point').
415              
416             =back
417              
418             =item data( )
419              
420             This returns a list of the events from the pool that got passed the parameters from the window( ) method.
421              
422             =back
423              
424             =head2 Private methods
425              
426             =over 4
427              
428             =item _add_to_pool
429              
430             Used to add the event into the pool which is sorted by start date
431              
432             =item _valid_keys
433              
434             Validate that the keys supplied in the hash are valid
435              
436             =item _date_valid
437              
438             Validate a date
439              
440             =item _required_keys
441              
442             Check that the required keys have been supplied in the hash
443              
444             =item _lowercase_keys
445              
446             Lowercase the keys in a hash
447              
448             =item _today
449              
450             Return todays date for use with the 'present' word
451              
452             =item _set_range
453              
454             Set the xxx_start and xxx_end values from the xxx data
455              
456             =back
457              
458             =head1 ENVIRONMENT
459              
460             None
461              
462             =head1 DIAGNOSTICS
463              
464             =over 4
465              
466             =item Timeline->new() takes no arguments
467              
468             When the constructor is initialised it requires no arguments. This message is given if
469             some arguments were supplied.
470              
471             =item Timeline->add_interval() expected HASH as parameter
472              
473             The parameter is a hash describing an event
474              
475             =item Timeline->add_point() expected HASH as parameter
476              
477             The parameter is a hash describing an event
478              
479             =item Timeline->window() 'span' can only be defined with a 'start' and 'end'
480              
481             To define 'span' then you must also define 'start' and 'end'
482              
483             =item Timeline->window() 'callback' can only be a CODE reference
484              
485             You must pass a code reference for the callback
486              
487             =item Timeline->data() takes no arguments
488              
489             When the method is called it requires no arguments. This message is given if
490             some arguments were supplied.
491              
492             =item Timeline->add_interval() invalid key '...' passed as data
493              
494             The only valid keys are 'start', 'end', 'label', 'group' and 'id'. Something else was supplied.
495              
496             =item Timeline->add_interval() invalid date for '...'
497              
498             The date supplied for '...' is invalid
499              
500             =item Timeline->add_interval() 'start' and 'end' are in the wrong order
501              
502             The values for 'start' and 'end' are in the wrong order
503              
504             =item Timeline->add_interval() missing key '...'
505              
506             A required key was not supplied. Required keys are 'start', 'end' and 'label'
507              
508             =item Timeline->add_point() invalid key '...' passed as data
509              
510             The only valid keys are 'start', 'label', 'group' and 'id'. Something else was supplied.
511              
512             =item Timeline->add_point() invalid date for '...'
513              
514             The date supplied for '...' is invalid
515              
516             =item Timeline->add_point() missing key '...'
517              
518             A required key was not supplied. Required keys are 'start' and 'label'
519              
520             =item Timeline->window() invalid key '...' passed as data
521              
522             The only valid keys are 'start', 'end', 'start_in', 'end_in', 'span' and 'callback'. Something else was supplied.
523              
524             =item Timeline->window() invalid date for '...'
525              
526             The date supplied for '...' is invalid
527              
528             =item Timeline->window() 'start' and 'end' are in the wrong order
529              
530             The values for 'start' and 'end' are in the wrong order
531              
532             =back
533              
534             =head1 BUGS
535              
536             None
537              
538             =head1 FILES
539              
540             See the Timeline.t file in the test directory
541              
542             =head1 SEE ALSO
543              
544             Graph::Timeline::GD - Use GD to render the timeline
545              
546             =head1 AUTHORS
547              
548             Peter Hickman (peterhi@ntlworld.com)
549              
550             =head1 COPYRIGHT
551              
552             Copyright (c) 2003, Peter Hickman. All rights reserved.
553              
554             This module is free software. It may be used, redistributed and/or
555             modified under the same terms as Perl itself.