File Coverage

blib/lib/Schedule/TableImage.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             #---------------------------------------------------------
2             # Documentation is at the end of the file in POD format.
3             #-------------------------------------------------------
4             package Schedule::TableImage;
5              
6 1     1   1102 use strict;
  1         3  
  1         48  
7 1     1   442 use Image::Magick;
  0            
  0            
8             use Text::Wrapper;
9             require Exporter;
10              
11             use fields qw(days hours events width height xoffset yoffset totaldays totalhours daywidth hourheight image max_textlen);
12             use vars qw(%FIELDS $VERSION);
13              
14             $VERSION = '1.13';
15              
16             #-----------------------------
17             # new
18             #------------------------------
19             sub new {
20             my ($invocant) = shift;
21              
22             my $type = ref($invocant) || $invocant;
23             my $self = { @_ };
24              
25             #--- bless ---#
26             bless $self, $type;
27              
28             $self->_init();
29             return $self;
30             }
31              
32             #--------------------------------------------------
33             # get as much info as we can based on text and filename
34             #--------------------------------------------------
35             sub _init {
36             my ($self) = @_;
37              
38             $self->_check_hours();
39             unless ( (defined $self->{days}) && (defined $self->{hours}) ){
40             $self->error("Days and hours must be defined.", "The call to new must include an array of hashes for the days and for the hours");
41             }
42              
43             my @days = @{$self->{days}};
44             my @hours = @{$self->{hours}};
45            
46             $self->{font} = '@/usr/local/share/fonts/ttf/arial.ttf' unless ($self->{font});
47             $self->{pointsize} = '12';
48              
49              
50             $self->_set_text_size();
51             $self->{width} = "500" unless ($self->{width});
52             $self->{height} = "500" unless ($self->{height});
53             $self->{xoffset} = $self->{pt_txt_width} + 1;
54             $self->{yoffset} = $self->{pt_txt_height} + 1 ;
55             $self->{totaldays} = @days + 0 ;
56             $self->{totalhours} = $#hours + 1;
57             $self->{daywidth} = ($self->{width} - $self->{xoffset} - 5) / $self->{totaldays};
58             $self->{hourheight} = ($self->{height} - $self->{yoffset} - 5)/ $self->{totalhours};
59             $self->{minuteheight} = $self->{hourheight} / 60 ;
60             $self->{max_textlen} = $self->_max_textlength($self->{daywidth});
61              
62             $self->{schedule} = {}; # all events keyed by day and start time
63              
64             return;
65             }
66              
67              
68              
69             #-----------------------------------
70             # get size values based on font
71             #------------------------------------
72             sub _set_text_size {
73             my ($self) = @_;
74             my ($x_ppem, $y_ppem, $ascender, $max_advance);
75             my $text = "12:00 PM";
76             my $im = Image::Magick->new();
77             my $rc = $im->Read("label:$text");
78             $self->error("Error finding text size",
79             "Could not create image to read text size: $rc") if $rc;
80            
81             ($x_ppem, $y_ppem, $ascender, $self->{pt_txt_desc},$self->{pt_txt_width}, $self->{pt_txt_height}, $max_advance)
82             = $im->QueryFontMetrics( text=>$text, font=>$self->{font}, pointsize=>$self->{pointsize} );
83             $self->{txt_width} = int $self->{pt_txt_width} / length($text);
84            
85              
86             $im ="";
87              
88             return 1;
89             }
90              
91              
92             #-----------------------------------------------
93             # how many characters can fit in the width given
94             #-----------------------------------------------
95             sub _max_textlength {
96             my ($self, $width) = @_;
97             my $num_chars = int( $width / $self->{txt_width});
98             return $num_chars - 1;
99             }
100              
101             #--------------------------------------------
102             # create image reference
103             #---------------------------------------------
104             sub _setup_image {
105             my ($self, $w, $h) = @_;
106              
107             # some typeing shortcuts
108             my $im = Image::Magick->new(size => "$w".'x'."$h" );
109             my ($rc); #errors
110              
111             $rc = $im->Read('xc:white');
112             $self->error("Error creating schedule", "Could not create image to write text to: $rc") if $rc;
113              
114              
115             $self->{image} = $im;
116              
117             return 1;
118             }
119              
120              
121             #--------------------------------
122             # create schedule background
123             #---------------------------------
124             sub create_schedule {
125             my ($self) = @_;
126             my $text_color= "#000000";
127             my $rc; #errors
128              
129              
130             # do calculations to prepare width of hours and days and prepare events
131             $self->_prepare_schedule();
132             $self->_setup_image($self->{width}, $self->{height}) unless (defined $self->{image});
133              
134             my $im = $self->{image};
135             my ($xoffset, $yoffset) = ($self->{xoffset}, $self->{yoffset});
136              
137             # print "Self is ".Dumper($self);
138             #----- days
139             for (my $i=0;$i<$self->{totaldays};$i++ ) {
140              
141             # create the rectangles for each day
142             my $x1 = $self->{schedule}->{$i}->{startpixels};
143             my $x2 = $self->{schedule}->{$i}->{endpixels};
144             my $y1 = $yoffset;
145             my $y2 = $yoffset + $self->{totalhours}*$self->{hourheight};
146              
147             $rc = $im->Draw(primitive => 'rectangle',
148             points => "$x1, $y1, $x2, $y2",
149             stroke => "$text_color");
150             $self->error("Error creating line", "Could not draw day line at $x1, $y1, $x2, $y2 with $text_color: $rc") if $rc;
151              
152              
153             # add the day labels
154             # put middle of label in middle of column
155             my $textlen = int($self->{txt_width} * length($self->{days}->[$i]->{display}));
156             my $x = $x1 + (($x2 - $x1)/2) - $textlen/2 ;
157              
158             my $y = ($yoffset - 1);
159             $rc = $im->Annotate(text => $self->{days}->[$i]->{display},
160             font => $self->{font},
161             pointsize => $self->{pointsize},
162             fill => $text_color,
163             gravity => 'NorthWest',
164             geometry => "+$x+$y",
165             );
166             $self->error("Error creating day label", "Could not annotate image with text: $rc") if $rc;
167             }
168              
169             #----- hours
170             foreach my $i ( 0..$self->{totalhours} ) {
171              
172             # create the lines for each hour
173             my $y1 = $yoffset + ($i * $self->{hourheight});
174             my $x2 = $self->{width} - $xoffset ;
175             my $y2 = $yoffset + ($i * $self->{hourheight});
176              
177             $rc = $im->Draw(primitive => 'line',
178             points => "$xoffset, $y1,
179             $x2, $y2",
180             stroke => $text_color);
181             $self->error("Error creating line", "Could not draw hour line: $rc") if $rc;
182              
183             # add the hour labels
184              
185             # get middle of text right on hour line
186             my $y = ($i * $self->{hourheight}) + $yoffset + $self->{pointsize}/2 ;
187              
188             # put the text right aligned with cal
189             my $textlen = int($self->{txt_width} * length($self->{hours}->[$i]->{display}));
190             my $x = $xoffset - $textlen - 2*($self->{txt_width});
191              
192             $rc = $im->Annotate(text => $self->{hours}->[$i]->{display},
193             font => $self->{font},
194             pointsize => $self->{pointsize},
195             fill => $text_color,
196             gravity => 'NorthWest',
197             geometry => "+$x+$y",
198             );
199             $self->error("Error annotating hour line", "Could not annotate image with text: $rc") if $rc;
200              
201             }
202              
203             return 1;
204             }
205              
206             #----------------------------
207             # given an array of event hashes
208             # add the events
209             #------------------------------
210             sub add_events {
211             my ($self, $events) = @_;
212             $self->error("Events must be defined.", "The add_events function takes as a parameter an array of events.") unless ($events);
213              
214             my ($fill_color, $text_color) = ("#999999", "#000000");
215             my ($rc); #errors
216             $self->{events} = $events;
217              
218             # create the background and labels
219             $self->create_schedule();
220              
221             my $im = $self->{image};
222            
223             # print out event rectangles
224             foreach my $event ( @{$self->{events}} ) {
225             $fill_color = $event->{fill_color} if (defined $event->{fill_color});
226             $self->_event_coordinates($event);
227              
228             my ($x1, $x2, $y1, $y2) = @{$event->{rectangle}};
229             next unless($x1 && $x2);
230            
231             $rc = $im->Draw(primitive => 'rectangle',
232             points => "$x1, $y1, $x2, $y2",
233             fill => $fill_color,
234             stroke => "#000000");
235             $self->error("Error creating event", "Could not draw rectangle: $rc") if $rc;
236              
237              
238             my $x = $x1 + 1;
239             my $y = $y1 + $self->{yoffset} + 1;
240            
241             # if event size changes, change wrapper size
242             my $title = $self->_wrap_text($event->{title}, $event->{max_textlen});
243             $rc = $im->Annotate(text => $title,
244             font => $self->{font},
245             pointsize => $self->{pointsize},
246             fill => $text_color,
247             gravity => 'NorthWest',
248             geometry => "+$x+$y",
249             );
250             $self->error("Error creating event title", "Could not annotate image with text: $rc") if $rc;
251             }
252             return 1;
253             }
254              
255              
256             #---------------------------------
257             # remove current list of events and schedule
258             #--------------------------------------
259             sub clear_events {
260             my ($self) = @_;
261             $self->{events} = ();
262             $self->{schedule} = {};
263             return 1;
264             }
265              
266            
267              
268             #-----------------------------------------
269             # _prepare_schedule
270             # does all prep work to allow event rects to be calculated
271             #------------------------------------------
272             sub _prepare_schedule {
273             my ($self) = @_;
274              
275             # go through events, find relative positions
276             # start building schedule
277             foreach my $event (@{$self->{events}}) {
278             $self->_event_geometry($event);
279             }
280              
281             # based on the schedule, calculate overlap information
282             my $day;
283             for ( $day=0;$day<$self->{totaldays};$day++ ) {
284              
285             foreach my $hour (sort keys %{$self->{schedule}->{$day}}) {
286             next if ($hour =~ /num_events/i);
287             $self->_calculate_overlap($day, $hour);
288             }
289              
290             if ($day == 0) {
291             $self->{schedule}->{$day}->{startpixels} = $self->{xoffset};
292             }
293             else {
294             $self->{schedule}->{$day}->{startpixels} =
295             $self->{schedule}->{$day-1}->{endpixels};
296             }
297             $self->{schedule}->{$day}->{endpixels} =
298             $self->{schedule}->{$day}->{startpixels} +
299             ( ($self->{schedule}->{$day}->{num_events}+1) * $self->{daywidth} );
300             }
301             $self->{width} = $self->{schedule}->{$day-1}->{endpixels} + $self->{xoffset};
302              
303             }
304              
305              
306             #--------------------------------
307             # remove any empty days
308             #-----------------------------
309             sub _check_hours {
310             my ($self) = @_;
311            
312             my @hours = @{$self->{hours}};
313             for (my $i=0;$i<=@hours;$i++ ) {
314             if ((! $hours[$i] )
315             || (! exists $hours[$i]->{display} )
316             || (! exists $hours[$i]->{value} )
317             || (! defined $hours[$i]->{display} )
318             || ( $hours[$i] eq "" ) ) {
319             splice @hours, $i;
320             }
321             }
322             @{$self->{hours}} = @hours;
323              
324             }
325              
326             #----------------------------------------
327             # get the relative, but not pixel position
328             # of all the events
329             #-----------------------------------------
330             sub _event_geometry {
331             my ($self, $event) = @_;
332             my ($startmin, $endmin) = (0, 0);
333              
334             my $dayindex = $self->_get_index($event->{day_num}, $self->{days} );
335             my $startindex = $self->_get_index($event->{begin_time}, $self->{hours} );
336             my $endindex = $self->_get_index($event->{end_time}, $self->{hours} );
337             if ($startindex == -1) {
338             ($startindex, $startmin) = $self->_get_minute($event->{begin_time}) ;
339             if ($startindex == -1) {
340             $startindex = 0;
341             $startmin = 0;
342             }
343             }
344             if ($endindex == -1) {
345             ($endindex, $endmin) = $self->_get_minute($event->{end_time}) ;
346             $endindex = $self->{totalhours} if ($endindex == -1);
347             }
348             return if ($dayindex == -1);
349              
350              
351             $event->{startindex} = $startindex;
352             $event->{endindex} = $endindex;
353             $event->{startminute} = $startmin;
354             $event->{endminute} = $endmin;
355              
356              
357             # push info into schedule data structure
358             # which allows us later to check for overlap
359             my $hourspan = $startindex;
360             while ($hourspan < $endindex) {
361             # add the event to the place in the schedule
362             push @{$self->{schedule}->{$dayindex}->{$hourspan}}, $event;
363             $hourspan++;
364             }
365             # does not end on that hour... ends after that hour
366             if ($endmin > 0 ){
367             push @{$self->{schedule}->{$dayindex}->{$endindex}}, $event;
368             }
369              
370             }
371              
372              
373             #-----------------------------------
374             # figure out event coordinates
375             # based on the geometry
376             #-----------------------------------
377             sub _event_coordinates {
378             my ($self, $event) = @_;
379              
380             my $dayindex = $event->{day_num} - 1;
381             my $startindex = $event->{startindex};
382             my $endindex = $event->{endindex};
383              
384             my $x2;
385             my $x1 = $self->{schedule}->{$dayindex}->{startpixels} +
386             ( $event->{day_order} * $self->{daywidth} );
387              
388              
389             # if day is stetched, but this event does not overlap
390             # make the day stretch across the whole day
391             if ( ($self->{schedule}->{$dayindex}->{num_events} > 0) &&
392             ($event->{overlap} != 1 ) )
393             {
394             $x2= $self->{schedule}->{$dayindex}->{endpixels};
395            
396             }
397             else {
398             $x2 = $x1 + $self->{daywidth} ;
399             }
400             my $y1 = ( $startindex * $self->{hourheight}) + $self->{yoffset} + $event->{startminute};
401             my $y2 = ( $endindex * $self->{hourheight}) + $self->{yoffset} + $event->{endminute};
402             $event->{rectangle} = [$x1, $x2, $y1, $y2];
403             $event->{max_textlen} = $self->_max_textlength($x2 - $x1);
404              
405             return;
406             }
407              
408              
409              
410             #--------------------------------
411             # modify rectangles or events
412             # to show an overlap
413             #------------------------------------
414             sub _calculate_overlap {
415             my ($self, $daykey, $hourkey) = @_;
416              
417             my @list = @{$self->{schedule}->{$daykey}->{$hourkey}};
418              
419             # if only one event there is no overlap
420             if (@list + 0 == 1) {
421             # default the day_order to 0 if not set
422             unless ( exists $self->{schedule}->{$daykey}->{$hourkey}->[0]->{day_order} )
423             {
424             $self->{schedule}->{$daykey}->{$hourkey}->[0]->{day_order} = 0;
425             }
426             return ;
427             }
428              
429             # the first event of day should be to the leftmost side of day
430             my @eventlist =
431             sort { $a->{startindex} <=> $b->{startindex} } @list;
432              
433             # don't put any two events in same spot
434             my @spots = (0..@eventlist);
435             my @taken_spots;
436             foreach my $e (@eventlist) {
437             next unless ( (exists $e->{day_order}) && ($e->{day_order} !~ /^\s*$/) );
438             push @taken_spots, $e->{day_order};
439             }
440             my $day_order =0;
441              
442            
443             # give an event an unassigned day order
444             foreach my $event (@eventlist) {
445             $event->{overlap} = 1;
446             if ( exists $event->{day_order} ) {
447             next;
448             }
449             while ( grep /^$day_order$/, @taken_spots ) {
450             $day_order++;
451             }
452             $event->{day_order} = $day_order;
453             push @taken_spots, $day_order;
454              
455             }
456              
457            
458             # set maximum num events found so far for this day
459             if ($#taken_spots > $self->{schedule}->{$daykey}->{num_events} ) {
460             $self->{schedule}->{$daykey}->{num_events} = $#taken_spots ;
461             }
462              
463             }
464              
465              
466             #------------------------------------
467             # given a width (for an event)
468             # wrap the text
469             #--------------------------------------
470             sub _wrap_text {
471             my ($self, $text, $width) = @_;
472            
473             my $wrapper = Text::Wrapper->new(columns=>$width, body_start => '');
474             return $wrapper->wrap($text);
475             }
476              
477              
478             #------------------------------
479             # _get_index
480             # takes array & value
481             # returns array index for which the value matches
482             #-----------------------------
483             sub _get_index {
484             my ($self, $value, $array) = @_;
485             for my $i (0.. @$array) {
486             if ( $array->[$i]->{value} eq $value) {
487             return $i;
488             }
489             }
490             return -1;
491             }
492              
493              
494             #---------------------------------------
495             # _get_minute
496             # based on an hhmm time, seperates the minute and hour index
497             #----------------------------------------
498             sub _get_minute {
499             my ($self, $time) = @_;
500             if ($time =~ /(.+)(\d\d)$/ ) {
501             my $hour = $1."00";
502             my $min = $2;
503             my $minpoint = $min * $self->{minuteheight};
504             my $hpoint = $self->_get_index($hour, $self->{hours} );
505             return ($hpoint, $minpoint);
506             }
507              
508             #TODO throw exception
509             #print "time $time does not end with two digits \n";
510              
511             return (-1, -1);
512             }
513              
514              
515              
516              
517             #------------------------------------
518             # write the image file
519             # to specified place
520             #---------------------------------------
521             sub write_image {
522             my ($self, $fp, $qualitymetric) = @_;
523              
524             my $rc;
525              
526             if (defined $qualitymetric) {
527             $rc = $self->{image}->Set('quality'=>'90');
528             }
529             $rc = $self->{image}->Write($fp);
530             $self->error("Error writing image", "Could not write schedule file: $rc") if $rc;
531             return 1;
532             }
533              
534              
535             #---------------------------
536             # the error method
537             #--------------------------
538             sub error {
539             my ($self, $text, $text2) = @_;
540             die "$text \n $text2 \n";
541             }
542              
543             #------------------------------
544             1;
545             __END__