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