1##########################################################################
2#
3#	File:	Project/Gantt/TimeSpan.pm
4#
5#	Author:	Alexander Westholm
6#
7#	Purpose: This class is a visual representation of a timespan on
8#		a Gantt chart. It is used to display both sub-projects,
9#		and the tasks they contain.
10#
11#	Client:	CPAN
12#
13#	CVS: $Id: TimeSpan.pm,v 1.4 2004/08/03 17:56:52 awestholm Exp $
14#
15##########################################################################
16package Project::Gantt::TimeSpan;
17use strict;
18use warnings;
19use Project::Gantt::Globals;
20
21##########################################################################
22#
23#	Method:	new(%opts)
24#
25#	Purpose: Constructor. Takes as parameters the Task or Gantt object
26#		it is going to display, as well as the Class::Date object
27#		representing the beginning of the chart. In addition, the
28#		Image::Magick canvas is passed in, along with a skin object
29#		to customize the colors of this TimeSpan.
30#
31##########################################################################
32sub new {
33	my $cls	= shift;
34	my %ops	= @_;
35	die "Must provide proper args to TimeSpan!" if(not($ops{task} and $ops{rootStr}));
36	return bless {
37		task	=>	$ops{task},
38		rootStr	=>	$ops{rootStr},
39		canvas	=>	$ops{canvas},
40		skin	=>	$ops{skin},
41		beginX	=>	205,
42	}, $cls;
43}
44
45##########################################################################
46#
47#	Method:	Display(mode, height)
48#
49#	Purpose: Calls _writeBar passing its parameters along. Simply a
50#		placeholder at this point, but exists incase some
51#		preprocessing is necessary at a later date.
52#
53##########################################################################
54sub display {
55	my $me	= shift;
56	my $mod	= shift;
57	my $hgt	= shift;
58	$me->_writeBar($mod, $hgt);
59}
60
61##########################################################################
62#
63#	Method:	_writeBar(mode, height)
64#
65#	Purpose: This method calculates the distance from the beginning of
66#		the graph at which to begin drawing this TimeSpan, as well
67#		as how many pixels in width it should be. It then calls
68#		either _drawSubProj or _drawTask depending on whether
69#		the task object passed to the constructor is a
70#		Project::Gantt instance or a Project::Gantt::Task
71#		instance.
72#
73##########################################################################
74sub _writeBar {
75	my $me		= shift;
76	my $mode	= shift;
77	my $height	= shift;
78	my $tsk		= $me->{task};
79	my $rootStart	= $me->{rootStr};
80	my $taskStart	= $tsk->getStartDate();
81	my $taskEnd	= $tsk->getEndDate();
82	my $startX	= $me->{beginX};
83	my $dif		= $taskStart-$rootStart;
84
85	# calculate starting X coordinate based on number of units away from start it is
86	if($mode eq 'hours'){
87		$startX += $dif->hour * $DAYSIZE;
88		$startX += (($rootStart->min / 59) * $DAYSIZE);
89	}elsif($mode eq 'months'){
90		$startX	+= $me->_getMonthPixels($rootStart->month_begin, $taskStart);
91	}else{
92		$startX += $dif->day * $DAYSIZE;
93		$startX += (($rootStart->hour / 23) * $DAYSIZE);
94	}
95	my $endX	= $startX;
96	my $edif	= $taskEnd-$taskStart;
97	# range variable indicates whether or not space filled by this bar is less than 15 pixels or not
98	# this is because 15 pixels are required for the diamond shape... if less than, a rectangle
99	# is used
100	my $range	= 0;
101
102	# calculate ending X coordinate based on number of units within this bar
103	if($mode eq 'hours'){
104		$endX	+= $edif->hour * $DAYSIZE;
105		$range	= $edif->hour;
106	}elsif($mode eq 'months'){
107		my $tmp	= $me->_getMonthPixels($taskStart, $taskEnd);
108		$endX	+= $tmp;
109		$range	= $tmp / $DAYSIZE;
110	}else{
111		$endX	+= $edif->day * $DAYSIZE;
112		$range	= $edif->day;
113	}
114	if($startX == $endX){
115		die "Incorrect date range!";
116	}
117
118	$me->_drawSubProj($startX, $height, $endX, $range) if $tsk->isa("Project::Gantt");
119	$me->_drawTask($startX, $height, $endX, $range) if $tsk->isa("Project::Gantt::Task");
120}
121
122##########################################################################
123#
124#	Method:	_getMonthPixels(start, end)
125#
126#	Purpose: Given the start and end of a TimeSpan, as passed in, this
127#		method approximately calculates the number of pixels
128#		that it should take up on the Gantt chart. There are some
129#		minor errors occasionally, as this calculation is based on
130#		the number of seconds in the task divided by the number of
131#		seconds in the year. Since not every month has the same
132#		number of seconds, minor miscalculations will occur.
133#
134##########################################################################
135sub _getMonthPixels {
136	my $me		= shift;
137	my $birth	= shift;
138	my $death	= shift;
139	my $pixelsPerYr	= 12 * 60;
140	my $secsInYear	= (((60*60)*24)*365);
141	my $secsInSpan	= ($death - $birth)->sec;
142	my $percentage	= $secsInSpan / $secsInYear;
143	return $percentage * $pixelsPerYr;
144}
145
146##########################################################################
147#
148#	Method:	_drawTask(startX, startY, endX, range)
149#
150#	Purpose: Given the starting coordinates, and ending X coordinate
151#		of a TimeSpan, uses Image::Magick to draw the span on the
152#		chart using whatever Skin scheme is in effect. Range is
153#		an indication of whether the span takes up more than
154#		15 pixels or not. If so, the span is drawn as a diamond,
155#		if not, as a rectangle.
156#
157##########################################################################
158sub _drawTask {
159	my $me		= shift;
160	my $startX	= shift;
161	my $startY	= shift;
162	my $endX	= shift;
163	my $range	= shift;
164	my $canvas	= $me->{canvas};
165	my $leadY	= $startY+8.5;
166	my $bottom	= $startY+13.5;
167	$startY		+= 3.5;
168	my $leadX	= $startX+7.5;
169	my $trailX	= $endX-7.5;
170	# if has space for full diamond
171	if($range >= 1){
172		$canvas->Draw(
173			fill		=>	$me->{skin}->itemFill(),
174			stroke		=>	$me->{skin}->itemFill(),
175			primitive	=>	'polygon',
176			points		=>	"${startX}, $leadY ${leadX}, $bottom ${leadX}, $startY");
177		$canvas->Draw(
178			fill		=>	$me->{skin}->itemFill(),
179			stroke		=>	$me->{skin}->itemFill(),
180			primitive	=>	'polygon',
181			points		=>	"${trailX}, $bottom ${trailX}, $startY ${endX}, $leadY");
182		# if space between diamond edges, fill in
183		if($leadX != $trailX){
184			$canvas->Draw(
185				fill		=>	$me->{skin}->itemFill(),
186				stroke		=>	$me->{skin}->itemFill(),
187				primitive	=>	'rectangle',
188				points		=>	"${leadX}, $startY ${trailX}, $bottom");
189		}
190	# not enough space for full diamond, use rectangle
191	}else{
192		$canvas->Draw(
193			fill		=>	$me->{skin}->itemFill(),
194			stroke		=>	$me->{skin}->itemFill(),
195			primitive	=>	'rectangle',
196			points		=>	"${startX}, $startY ${endX}, $bottom");
197	}
198}
199
200##########################################################################
201#
202#	Method:	_drawSubProj(startX, startY, endX, range)
203#
204#	Purpose: Same as above, except draws a bracket instead of a
205#		diamond, indicating a containment relationship.
206#
207##########################################################################
208sub _drawSubProj {
209	my $me		= shift;
210	my $startX	= shift;
211	my $startY	= shift;
212	my $endX	= shift;
213	my $range	= shift;
214	my $canvas	= $me->{canvas};
215	my $edgeTop	= $startY+7;
216	my $edgeBot	= $startY+17;
217	my $innerBot	= $startY+10;
218	my $polyX	= $startX+7.5;
219	my $endPolyX	= $endX-7.5;
220	#if enough space for full bracket
221	if($range >= 1){
222		$canvas->Draw(
223			fill		=>	$me->{skin}->containerFill(),
224			stroke		=>	$me->{skin}->containerStroke(),
225			primitive	=>	'polygon',
226			points		=>	"${startX}, $edgeBot ${startX}, $edgeTop ${polyX}, $startY ${polyX}, $innerBot");
227		$canvas->Draw(
228			fill		=>	$me->{skin}->containerFill(),
229			stroke		=>	$me->{skin}->containerStroke(),
230			primitive	=>	'polygon',
231			points		=>	"${endPolyX}, $innerBot ${endPolyX}, $startY ${endX}, $edgeTop ${endX}, $edgeBot");
232		# if space between bracket ends, fill in
233		if($polyX != $endPolyX){
234			$canvas->Draw(
235				fill		=>	$me->{skin}->containerFill(),
236				stroke		=>	$me->{skin}->containerStroke(),
237				primitive	=>	'rectangle',
238				points		=>	"${polyX}, $startY ${endPolyX}, $innerBot");
239		}
240	# not enough space for full bracket, use rectangle
241	}else{
242		$canvas->Draw(
243			fill		=>	$me->{skin}->containerFill(),
244			stroke		=>	$me->{skin}->containerStroke(),
245			primitive	=>	'rectangle',
246			points		=>	"${startX}, $startY ${endX}, $edgeBot");
247	}
248}
249
2501;
251