File Coverage

blib/lib/Mail/Thread/Arc.pm
Criterion Covered Total %
statement 12 101 11.8
branch 0 16 0.0
condition n/a
subroutine 4 18 22.2
pod 12 13 92.3
total 28 148 18.9


line stmt bran cond sub pod time code
1 1     1   838 use strict;
  1         3  
  1         42  
2             package Mail::Thread::Arc;
3 1     1   1019 use SVG;
  1         20041  
  1         7  
4 1     1   1840 use Date::Parse qw( str2time );
  1         9548  
  1         95  
5 1     1   10 use base qw( Class::Accessor::Chained::Fast );
  1         2  
  1         1237  
6             __PACKAGE__->mk_accessors(qw( messages message_offsets selected_message
7             width height svg ));
8              
9             our $VERSION = '0.22';
10              
11             =head1 NAME
12              
13             Mail::Thread::Arc - Generates a Thread Arc reperesentation of a thread
14              
15             =head1 SYNOPSIS
16              
17             my $threader = Mail::Thread->new( @messages );
18             my $arc = Mail::Thread::Arc->new;
19              
20             $threader->thread;
21              
22             my $i;
23             for my $thread ($threader->rootset) {
24             ++$i;
25             my $svg = $arc->render( $thread );
26             write_file( "thread_$i.svg", $svg->xmlify );
27             }
28              
29             =head1 DESCRIPTION
30              
31             Mail::Thread::Arc takes a Mail::Thread::Container and generates an
32             image of the Thread Arc. Thread Arcs are described in the
33             documentation for IBM's remail project.
34              
35             http://www.research.ibm.com/remail/
36              
37             =head1 METHODS
38              
39             =head2 new
40              
41             Generic constructor, inherited from L
42              
43             =head2 render( $root_container, %options )
44              
45             The main method.
46              
47             Renders the thread tree as a thread arc. Returns an SVG object.
48              
49             =cut
50              
51             sub render {
52 0     0 1   my $self = shift;
53 0           my $root = shift;
54              
55             # extract just the containers with messages
56 0           my @messages;
57             $root->iterate_down(
58             sub {
59 0     0     my $container = shift;
60 0 0         push @messages, $container if $container->message;
61 0           } );
62              
63             # sort on date
64 0           @messages = sort {
65 0           $self->date_of( $a ) <=> $self->date_of( $b )
66             } @messages;
67              
68 0           $self->messages( \@messages );
69              
70 0           $self->width( ( @messages + 1 ) * $self->message_radius * 3 );
71 0           $self->height( $self->maximum_arc_height * 2 + $self->message_radius * 6 );
72 0           $self->svg( SVG->new( width => $self->width, height => $self->height, onload => "init(evt)" ) );
73              
74             {
75             # assign the numbers needed to compute X
76 0           my $i;
  0            
77 0           $self->message_offsets( { map { $_ => ++$i } @messages } );
  0            
78             }
79 0           $self->svg->script->CDATA( $self->javascript_chunk );
80              
81 0           $self->draw_arc( $_->parent, $_ ) for @messages;
82 0           $self->draw_message( $_ ) for @messages;
83              
84 0           return $self->svg;
85             }
86              
87              
88             =head2 draw_message( $message )
89              
90             Draw the message on the SVG canvas.
91              
92             =cut
93              
94             sub draw_message {
95 0     0 1   my ($self, $message) = @_;
96              
97 0           my $i = $self->message_offsets->{$message} - 1;
98              
99 0           my $link = $self->make_link($message);
100 0           my $root = $self->svg;
101 0 0         $root = $root->anchor( -href=> $link ) if defined $link;
102              
103 0           my $group = $root->group(
104             id => "road$i",
105             onmouseover => "set_group_color( $i, 'blue' )",
106             $self->message_style( $message ),
107             );
108              
109 0           $group->title->cdata( $message->header('from') );
110 0           $group->desc->cdata( "Date: " . $message->header('date') );
111              
112 0           $group->circle(
113             cx => $self->message_x( $message ),
114             cy => $self->message_y,
115             r => $self->message_radius,
116             );
117            
118             }
119              
120              
121             =head2 make_link( $message )
122              
123             Return an URI based on the message. By default returns
124             undef meaning that that the message is not a link.
125              
126             However if this module is subclassed then a meaningful
127             URI can be returned where appropriate.
128              
129             =cut
130              
131             sub make_link {
132 0     0 1   my ($self, $message) = @_;
133 0           return;
134             }
135              
136              
137             =head2 draw_arc( $from, $to )
138              
139             draws an arc between two messages
140              
141             =cut
142              
143             sub draw_arc {
144 0     0 1   my ($self, $from, $to) = @_;
145              
146 0 0         return unless $from;
147              
148 0           my $distance = $self->message_x( $to ) - $self->message_x( $from );
149 0           my $radius = $distance/ 2;
150              
151 0           my $top = $self->thread_generation( $to ) % 2;
152 0           my $x = $self->message_x( $from );
153 0 0         my $y = $self->message_y + ( $top ? -$self->message_radius : $self->message_radius);
154              
155 0           my %offsets = %{ $self->message_offsets };
  0            
156              
157 0           my $path;
158 0 0         if ($radius > $self->maximum_arc_height) { # uh oh - trickyness
159 0           my $max = $self->maximum_arc_height;
160             # to Y - the relative part of the first curve
161 0 0         my $toy = $top ? -$max : $max;
162 0           my $toy2 = -$toy;
163 0           my $x2 = $self->message_x( $to ) - $max;
164 0           my $y2 = $y + $toy;
165              
166 0           $path = join(' ',
167             "M $x,$y", #start the path
168             "a$max,$max 0 0,$top $max,$toy", # arc up
169             "L $x2,$y2", # line across
170             "a$max,$max 0 0,$top $max,$toy2", # arc down
171             );
172             }
173             else {
174 0           $path = "M $x,$y a$radius,$radius 0 1,$top $distance,0";
175             }
176              
177 0           my $group = $self->svg->group(
178             id => "road$offsets{ $from }-$offsets{ $to }",
179             $self->arc_style( $from, $to )
180             );
181 0           $group->path( d => $path );
182             }
183              
184             =head2 message_radius
185              
186             The radius of the message circles. The most magic of all the magic
187             numbers.
188              
189             =cut
190              
191 0     0 1   sub message_radius { 5 }
192              
193             =head2 message_style( $container )
194              
195             Returns the style hash for the message circle.
196              
197             =cut
198              
199             sub message_style {
200 0     0 1   my ($self, $message) = @_;
201              
202             return (
203 0           stroke => 'red',
204             fill => 'white',
205             'stroke-width' => $self->message_radius / 4,
206             );
207             }
208              
209             =head2 maximum_arc_height
210              
211             the maximum height of an arc. default is 17 message radii
212              
213             =cut
214              
215             sub maximum_arc_height {
216 0     0 1   my $self = shift;
217 0           return $self->message_radius * 17
218             }
219              
220             =head2 arc_style( $from, $to )
221              
222             Returns the style hash for the connecting arc,
223              
224             =cut
225              
226             sub arc_style {
227 0     0 1   my ($self, $from, $to) = @_;
228              
229             return (
230 0           stroke => 'black',
231             fill => 'none',
232             'stroke-width' => $self->message_radius / 4,
233             );
234             }
235              
236             =head2 message_x( $container )
237              
238             returns the X co-ordinate for a message
239              
240             =cut
241              
242             sub message_x {
243 0     0 1   my ($self, $message) = @_;
244 0           return $self->message_offsets->{ $message } * $self->message_radius * 3;
245             }
246              
247             =head2 message_y
248              
249             returns the Y co-ordinate for a message (expected to be constant for
250             all messages)
251              
252             =cut
253              
254             sub message_y {
255 0     0 1   my $self = shift;
256 0           return $self->height / 2;
257             }
258              
259             =head2 thread_generation( $message )
260              
261             returns the thread generation of the container.
262              
263             =cut
264              
265             sub thread_generation {
266 0     0 1   my ($self, $container) = @_;
267              
268 0           my $count = 0;
269 0           while ($container->parent) {
270 0           ++$count;
271 0           $container = $container->parent;
272             }
273              
274 0           return $count;
275             }
276              
277             =head2 date_of( $container )
278              
279             The date the message was sent, in epoch seconds
280              
281             =cut
282              
283             sub date_of {
284 0     0 1   my ($self, $container) = @_;
285 0           return str2time $container->header( 'date' );
286             }
287              
288             sub javascript_chunk {
289 0     0 0   my $self = shift;
290 0           my $js = <<'END';
291             /* lifted from http://roasp.com/tutorial/tutorial5.shtml */
292             var SVGDoc;
293             var groups = new Array();
294             var last_group;
295              
296             function init(e) {
297             SVGDoc = e.getTarget().getOwnerDocument();
298             END
299              
300             # just what groups do we manage?
301 0           my %offset = %{ $self->message_offsets };
  0            
302 0           for my $id (sort { $a <=> $b } values %offset) {
  0            
303 0           my $group = $id - 1;
304 0           my $message = $self->messages->[$group];
305 0           my @path = $id - 1;
306 0           while ($message) {
307 0 0         last unless $message->parent;
308 0           push @path, $offset{ $message->parent } - 1,
309             "'$offset{ $message->parent }-$offset{ $message }'";
310 0           $message = $message->parent;
311             }
312              
313 0           $js .= " append_group( ". join(', ', @path ) . " ); // road $group\n"
314             }
315              
316 0 0         if (my $m = $self->selected_message) {
317 0           my $group = $offset{ $m } - 1;
318 0           $js .= " set_group_color( $group, 'blue' ); // select group $group\n"
319             }
320              
321 0           $js .= <<'END';
322             }
323              
324             function append_group() {
325             var roads = new Array();
326             for (var i = 0; i < arguments.length; i++) {
327             var index = arguments[i];
328             var road = SVGDoc.getElementById("road" + index);
329             roads[roads.length] = road;
330             }
331             groups[groups.length] = roads;
332             }
333              
334             function set_group_color(group_index, color) {
335             if ( last_group != null ) {
336             _set_group_color(last_group, "black");
337             }
338             _set_group_color(group_index, color);
339             last_group = group_index;
340             }
341              
342             function _set_group_color(group_index, color) {
343             var roads = groups[group_index];
344              
345             for (var i = 0; i < roads.length; i++) {
346             var road = roads[i];
347              
348             //alert( "Setting " + road + " to " + color );
349             // set the color
350             road.setAttribute("stroke", color);
351             // pop to top - seems to blow errors on circles
352             // road.getParentNode.appendChild(road);
353             }
354             }
355             END
356              
357 0           return $js;
358             }
359              
360              
361             1;
362             __END__