Subversion Repositories Applications.papyrus

Rev

Blame | Last modification | View Log | RSS feed

<?php
/*=======================================================================
// File:        JPGRAPH_GANTT.PHP
// Description: JpGraph Gantt plot extension
// Created:     2001-11-12
// Author:      Johan Persson (johanp@aditus.nu)
// Ver:         $Id: jpgraph_gantt.php,v 1.1 2004/06/15 10:13:19 jpm Exp $
//
// License:     This code is released under QPL 
// Copyright (c) 2002 Johan Persson
//========================================================================
*/
 
// Scale Header types
DEFINE("GANTT_HDAY",1);
DEFINE("GANTT_HWEEK",2);
DEFINE("GANTT_HMONTH",4);
DEFINE("GANTT_HYEAR",8);

// Bar patterns
DEFINE("GANTT_RDIAG",BAND_RDIAG);       // Right diagonal lines
DEFINE("GANTT_LDIAG",BAND_LDIAG); // Left diagonal lines
DEFINE("GANTT_SOLID",BAND_SOLID); // Solid one color
DEFINE("GANTT_VLINE",BAND_VLINE); // Vertical lines
DEFINE("GANTT_HLINE",BAND_HLINE);  // Horizontal lines
DEFINE("GANTT_3DPLANE",BAND_3DPLANE);  // "3D" Plane
DEFINE("GANTT_HVCROSS",BAND_HVCROSS);  // Vertical/Hor crosses
DEFINE("GANTT_DIAGCROSS",BAND_DIAGCROSS); // Diagonal crosses

// Conversion constant
DEFINE("SECPERDAY",3600*24);

// Locales. ONLY KEPT FOR BACKWARDS COMPATIBILITY
// You should use the proper locale strings directly 
// from now on. 
DEFINE("LOCALE_EN","en_UK");
DEFINE("LOCALE_SV","sv_SE");

// Layout of bars
DEFINE("GANTT_EVEN",1);
DEFINE("GANTT_FROMTOP",2);

// Styles for week header
DEFINE("WEEKSTYLE_WNBR",0);
DEFINE("WEEKSTYLE_FIRSTDAY",1);
DEFINE("WEEKSTYLE_FIRSTDAY2",2);
DEFINE("WEEKSTYLE_FIRSTDAYWNBR",3);
DEFINE("WEEKSTYLE_FIRSTDAY2WNBR",4);

// Styles for month header
DEFINE("MONTHSTYLE_SHORTNAME",0);
DEFINE("MONTHSTYLE_LONGNAME",1);
DEFINE("MONTHSTYLE_LONGNAMEYEAR2",2);
DEFINE("MONTHSTYLE_SHORTNAMEYEAR2",3);
DEFINE("MONTHSTYLE_LONGNAMEYEAR4",4);
DEFINE("MONTHSTYLE_SHORTNAMEYEAR4",5);


// Types of constrain links
DEFINE('CONSTRAIN_STARTSTART',0);
DEFINE('CONSTRAIN_STARTEND',1);
DEFINE('CONSTRAIN_ENDSTART',2);
DEFINE('CONSTRAIN_ENDEND',3);

// Arrow direction for constrain links
DEFINE('ARROW_DOWN',0);
DEFINE('ARROW_UP',1);
DEFINE('ARROW_LEFT',2);
DEFINE('ARROW_RIGHT',3);

// Arrow type for constrain type
DEFINE('ARROWT_SOLID',0);
DEFINE('ARROWT_OPEN',1);

// Arrow size for constrain lines
DEFINE('ARROW_S1',0);
DEFINE('ARROW_S2',1);
DEFINE('ARROW_S3',2);
DEFINE('ARROW_S4',3);
DEFINE('ARROW_S5',4);

// Activity types for use with utility method CreateSimple()
DEFINE('ACTYPE_NORMAL',0);
DEFINE('ACTYPE_GROUP',1);
DEFINE('ACTYPE_MILESTONE',2);


//===================================================
// CLASS GanttGraph
// Description: Main class to handle gantt graphs
//===================================================
class GanttGraph extends Graph {
    var $scale;                                                 // Public accessible
    var $iObj=array();                          // Gantt objects
    var $iLabelHMarginFactor=0.2;       // 10% margin on each side of the labels
    var $iLabelVMarginFactor=0.4;       // 40% margin on top and bottom of label
    var $iLayout=GANTT_FROMTOP; // Could also be GANTT_EVEN
    var $iSimpleFont = FF_FONT1,$iSimpleFontSize=11;
    var $iSimpleStyle=GANTT_RDIAG,$iSimpleColor='yellow',$iSimpleBkgColor='red';
    var $iSimpleProgressBkgColor='gray',$iSimpleProgressColor='darkgreen';
    var $iSimpleProgressStyle=GANTT_SOLID;
//---------------
// CONSTRUCTOR  
    // Create a new gantt graph
    function GanttGraph($aWidth=0,$aHeight=0,$aCachedName="",$aTimeOut=0,$aInline=true) {
        Graph::Graph($aWidth,$aHeight,$aCachedName,$aTimeOut,$aInline);         
        $this->scale = new GanttScale($this->img);
        if( $aWidth > 0 )
                $this->img->SetMargin($aWidth/17,$aWidth/17,$aHeight/7,$aHeight/10);
                
        $this->scale->ShowHeaders(GANTT_HWEEK|GANTT_HDAY);
        $this->SetBox();
    }
        
//---------------
// PUBLIC METHODS

    // 

    function SetSimpleFont($aFont,$aSize) {
        $this->iSimpleFont = $aFont;
        $this->iSimpleFontSize = $aSize;
    }

    function SetSimpleStyle($aBand,$aColor,$aBkgColor) {
        $this->iSimpleStyle = $aBand;
        $this->iSimpleColor = $aColor;
        $this->iSimpleBkgColor = $aSimpleBkgColor;
    }

// A utility function to help create the Gantt charts
    function CreateSimple($data,$constrains=array(),$progress=array()) {
        
    for( $i=0; $i < count($data); ++$i) {
        switch( $data[$i][1] ) {
            case ACTYPE_GROUP:
                // Create a slightly smaller height bar since the
                // "wings" at the end will make it look taller
                $a = new GanttBar($data[$i][0],$data[$i][2],$data[$i][3],$data[$i][4],'',8);
                $a->title->SetFont($this->iSimpleFont,FS_BOLD,$this->iSimpleFontSize);          
                $a->rightMark->Show();
                $a->rightMark->SetType(MARK_RIGHTTRIANGLE);
                $a->rightMark->SetWidth(8);
                $a->rightMark->SetColor('black');
                $a->rightMark->SetFillColor('black');
            
                $a->leftMark->Show();
                $a->leftMark->SetType(MARK_LEFTTRIANGLE);
                $a->leftMark->SetWidth(8);
                $a->leftMark->SetColor('black');
                $a->leftMark->SetFillColor('black');
            
                $a->SetPattern(BAND_SOLID,'black');
                $csimpos = 6;
                break;
                
            case ACTYPE_NORMAL:
                $a = new GanttBar($data[$i][0],$data[$i][2],$data[$i][3],$data[$i][4],'',10);
                $a->title->SetFont($this->iSimpleFont,FS_NORMAL,$this->iSimpleFontSize);
                $a->SetPattern($this->iSimpleStyle,$this->iSimpleColor);
                $a->SetFillColor($this->iSimpleBkgColor);
                // Check if this activity should have a constrain line
                $n = count($constrains);
                for( $j=0; $j < $n; ++$j ) {
                    if( $constrains[$j][0]==$data[$i][0] ) {
                        $a->SetConstrain($constrains[$j][1],$constrains[$j][2],'black',ARROW_S2,ARROWT_SOLID);    
                        break;
                    }
                }

                // Check if this activity have a progress bar
                $n = count($progress);
                for( $j=0; $j < $n; ++$j ) {
                    if( $progress[$j][0]==$data[$i][0] ) {
                        $a->progress->Set($progress[$j][1]);
                        $a->progress->SetPattern($this->iSimpleProgressStyle,
                                                 $this->iSimpleProgressColor);
                        $a->progress->SetFillColor($this->iSimpleProgressBkgColor);
                        //$a->progress->SetPattern($progress[$j][2],$progress[$j][3]);
                        break;
                    }
                }
                $csimpos = 6;
                break;

            case ACTYPE_MILESTONE:
                $a = new MileStone($data[$i][0],$data[$i][2],$data[$i][3]);
                $a->title->SetFont($this->iSimpleFont,FS_NORMAL,$this->iSimpleFontSize);
                $csimpos = 5;
                break;
            default:
                die('Unknown activity type');
            break;
        }

        // Setup caption
        $a->caption->Set($data[$i][$csimpos-1]);

        // Check if this activity should have a CSIM targetĀ ?
        if( !empty($data[$i][$csimpos]) ) {
            $a->SetCSIMTarget($data[$i][$csimpos]);
            $a->SetCSIMAlt($data[$i][$csimpos+1]);
        }
        if( !empty($data[$i][$csimpos+2]) ) {
            $a->title->SetCSIMTarget($data[$i][$csimpos+2]);
            $a->title->SetCSIMAlt($data[$i][$csimpos+3]);
        }

        $this->Add($a);
    }
}

        
    // Set what headers should be shown
    function ShowHeaders($aFlg) {
        $this->scale->ShowHeaders($aFlg);
    }
        
    // Specify the fraction of the font height that should be added 
    // as vertical margin
    function SetLabelVMarginFactor($aVal) {
        $this->iLabelVMarginFactor = $aVal;
    }
        
    // Add a new Gantt object
    function Add($aObject) {
        if( is_array($aObject) ) {
            for($i=0; $i<count($aObject); ++$i)
                $this->iObj[] = $aObject[$i];
        }
        else
            $this->iObj[] = $aObject;
    }

    // Override inherit method from Graph and give a warning message
    function SetScale() {
        JpGraphError::Raise("SetScale() is not meaningfull with Gantt charts.");
        // Empty
    }

    // Specify the date range for Gantt graphs (if this is not set it will be
    // automtically determined from the input data)
    function SetDateRange($aStart,$aEnd) {
        $this->scale->SetRange($aStart,$aEnd);
    }
        
    // Get the maximum width of the titles for the bars 
    function GetMaxLabelWidth() {
        $m=0;
        if( $this->iObj != null ) {
            $m = $this->iObj[0]->title->GetWidth($this->img);
            for($i=1; $i<count($this->iObj); ++$i) {
                if( $this->iObj[$i]->title->HasTabs() ) {
                    list($tot,$w) = $this->iObj[$i]->title->GetWidth($this->img,true);
                    $m=max($m,$tot);
                }
                else 
                    $m=max($m,$this->iObj[$i]->title->GetWidth($this->img));
            }
        }
        return $m;
    }
        
    // Get the maximum height of the titles for the bars
    function GetMaxLabelHeight() {
        $m=0;
        if( $this->iObj != null ) {
            $m = $this->iObj[0]->title->GetHeight($this->img);
            for($i=1; $i<count($this->iObj); ++$i) {
                $m=max($m,$this->iObj[$i]->title->GetHeight($this->img));
            }
        }
        return $m;
    }

    function GetMaxBarAbsHeight() {
        $m=0;
        if( $this->iObj != null ) {
            $m = $this->iObj[0]->GetAbsHeight($this->img);
            for($i=1; $i<count($this->iObj); ++$i) {
                $m=max($m,$this->iObj[$i]->GetAbsHeight($this->img));
            }
        }
        return $m;              
    }
        
    // Get the maximum used line number (vertical position) for bars
    function GetBarMaxLineNumber() {
        $m=0;
        if( $this->iObj != null ) {
            $m = $this->iObj[0]->GetLineNbr();
            for($i=1; $i<count($this->iObj); ++$i) {
                $m=max($m,$this->iObj[$i]->GetLineNbr());
            }
        }
        return $m;
    }
        
    // Get the minumum and maximum used dates for all bars
    function GetBarMinMax() {
        $max=$this->scale->NormalizeDate($this->iObj[0]->GetMaxDate());
        $min=$this->scale->NormalizeDate($this->iObj[0]->GetMinDate());
        for($i=1; $i<count($this->iObj); ++$i) {
            $max=Max($max,$this->scale->NormalizeDate($this->iObj[$i]->GetMaxDate()));
            $min=Min($min,$this->scale->NormalizeDate($this->iObj[$i]->GetMinDate()));
        }
        $minDate = date("Y-m-d",$min);
        $min = strtotime($minDate);
        $maxDate = date("Y-m-d",$max);
        $max = strtotime($maxDate);     
        return array($min,$max);
    }

    // Stroke the gantt chart
    function Stroke($aStrokeFileName="") {      

        // If the filename is the predefined value = '_csim_special_'
        // we assume that the call to stroke only needs to do enough
        // to correctly generate the CSIM maps.
        // We use this variable to skip things we don't strictly need
        // to do to generate the image map to improve performance
        // a best we can. Therefor you will see a lot of tests !$_csim in the
        // code below.
        $_csim = ($aStrokeFileName===_CSIM_SPECIALFILE);

        // Should we autoscale dates?
        if( !$this->scale->IsRangeSet() ) {
            list($min,$max) = $this->GetBarMinMax();
            $this->scale->SetRange($min,$max);
        }

        $this->scale->AdjustStartEndDay();
                        
        if( $this->img->img == null ) {
            // The predefined left, right, top, bottom margins.
            // Note that the top margin might incease depending on
            // the title.
            $lm=30;$rm=30;$tm=20;$bm=30;                        
            if( BRAND_TIMING ) $bm += 10;
                        
            // First find out the height                        
            $n=$this->GetBarMaxLineNumber()+1;
            $m=max($this->GetMaxLabelHeight(),$this->GetMaxBarAbsHeight());
            $height=$n*((1+$this->iLabelVMarginFactor)*$m);                     
                        
            // Add the height of the scale titles                       
            $h=$this->scale->GetHeaderHeight();
            $height += $h;

            // Calculate the top margin needed for title and subtitle
            if( $this->title->t != "" ) {
                $tm += $this->title->GetFontHeight($this->img);
            }
            if( $this->subtitle->t != "" ) {
                $tm += $this->subtitle->GetFontHeight($this->img);
            }

            // ...and then take the bottom and top plot margins into account
            $height += $tm + $bm + $this->scale->iTopPlotMargin + $this->scale->iBottomPlotMargin;
                        
            // Now find the minimum width for the chart required
            $fw=$this->scale->day->GetFontWidth($this->img)+4; // Add 2pixel margin on each side
            $nd=$this->scale->GetNumberOfDays();

            // If we display week we must make sure that 7*$fw is enough
            // to fit up to 10 characters of the week font (if the week is enabled)
            if( $this->scale->IsDisplayWeek() ) {
                // Depending on what format the suer has choose we need different amount
                // of space
                $fsw = strlen($this->scale->week->iLabelFormStr);
                if( $this->scale->week->iStyle==WEEKSTYLE_FIRSTDAY2WNBR ) {
                    $fsw += 8;
                }
                elseif( $this->scale->week->iStyle==WEEKSTYLE_FIRSTDAYWNBR ) {
                    $fsw += 7;
                }
                else {
                    $fsw += 4;
                }
                    
                $ww = $fsw*$this->scale->week->GetFontWidth($this->img);
                if( 7*$fw < $ww ) {
                    $fw = ceil($ww/7);
                }
            }

            if( !$this->scale->IsDisplayDay() && 
                !( ($this->scale->week->iStyle==WEEKSTYLE_FIRSTDAYWNBR || 
                    $this->scale->week->iStyle==WEEKSTYLE_FIRSTDAY2WNBR) && $this->scale->IsDisplayWeek() ) ) {
                // If we don't display the individual days we can shrink the
                // scale a little bit. This is a little bit pragmatic at the 
                // moment and should be re-written to take into account
                // a) What scales exactly are shown and 
                // b) what format do they use so we know how wide we need to
                // make each scale text space at minimum.
                $fw /= 2;
                if( !$this->scale->IsDisplayWeek() ) {
                    $fw /= 1.8;
                }
            }

            // Has the user specified a width or do we need to
            // determine it?
            if( $this->img->width <= 0 ) {
                // Now determine the width for the activity titles column
                // This is complicated by the fact that the titles may have
                // tabs. In that case we also need to calculate the individual
                // tab positions based on the width of the individual columns
                
                $titlewidth = $this->GetMaxLabelWidth();
                
                // Now get the total width taking 
                // titlewidth, left and rigt margin, dayfont size 
                // into account
                $width = $titlewidth + $nd*$fw + $lm+$rm;
            }
            else
                $width = $this->img->width;
                                                
            $this->img->CreateImgCanvas($width,$height);                        
            $this->img->SetMargin($lm,$rm,$tm,$bm);
        }
                
        // Should we start from the top or just spread the bars out even over the
        // available height
        $this->scale->SetVertLayout($this->iLayout);                    
        if( $this->iLayout == GANTT_FROMTOP ) {
            $maxheight=max($this->GetMaxLabelHeight(),$this->GetMaxBarAbsHeight());
            $this->scale->SetVertSpacing($maxheight*(1+$this->iLabelVMarginFactor));
        }
        // If it hasn't been set find out the maximum line number
        if( $this->scale->iVertLines == -1 ) 
            $this->scale->iVertLines = $this->GetBarMaxLineNumber()+1;  
                
        $maxwidth=max($this->GetMaxLabelWidth(),$this->scale->tableTitle->GetWidth($this->img));
        $this->scale->SetLabelWidth($maxwidth*(1+$this->iLabelHMarginFactor));
        if( !$_csim ) 
            $this->StrokePlotArea();

        $this->scale->Stroke();

        if( !$_csim ) 
            $this->StrokePlotBox();
        
        $n = count($this->iObj);
        for($i=0; $i < $n; ++$i) {
            $this->iObj[$i]->SetLabelLeftMargin(round($maxwidth*$this->iLabelHMarginFactor/2));
            $this->iObj[$i]->Stroke($this->img,$this->scale);
        }

        if( !$_csim ) {
            $this->StrokeConstrains();
            $this->StrokeTitles();
            $this->footer->Stroke($this->img);

            // If the filename is given as the special "__handle"
            // then the image handler is returned and the image is NOT
            // streamed back
            if( $aStrokeFileName == _IMG_HANDLER ) {
                return $this->img->img;
            }
            else {
                // Finally stream the generated picture                                 
                $this->cache->PutAndStream($this->img,$this->cache_name,$this->inline,
                                           $aStrokeFileName);           
            }
        }
    }

    function StrokeConstrains() {
        $n = count($this->iObj);

        // Stroke all constrains
        for($i=0; $i < $n; ++$i) {
            $vpos = $this->iObj[$i]->iConstrainRow;
            if( $vpos >= 0 ) {
                $c1 = $this->iObj[$i]->iConstrainPos;

                // Find out which object is on the target row
                $targetobj = -1;
                for( $j=0; $j < $n && $targetobj == -1; ++$j ) {
                    if( $this->iObj[$j]->iVPos == $vpos ) {
                        $targetobj = $j;
                    }
                }
                if( $targetobj == -1 ) {
                    JpGraphError::Raise('You have specifed a constrain from row='.
                                        $this->iObj[$i]->iVPos.
                                        ' to row='.$vpos.' which does not have any activity.');
                    exit();
                }
                $c2 = $this->iObj[$targetobj]->iConstrainPos;
                if( count($c1) == 4 && count($c2 ) == 4) {
                    switch( $this->iObj[$i]->iConstrainType ) {
                        case CONSTRAIN_ENDSTART:
                            if( $c1[1] < $c2[1] ) {
                                $link = new GanttLink($c1[2],$c1[3],$c2[0],$c2[1]);
                            }
                            else {
                                $link = new GanttLink($c1[2],$c1[1],$c2[0],$c2[3]);
                            }
                            $link->SetPath(3);
                            break;
                        case CONSTRAIN_STARTEND:
                            if( $c1[1] < $c2[1] ) {
                                $link = new GanttLink($c1[0],$c1[3],$c2[2],$c2[1]);
                            }
                            else {
                                $link = new GanttLink($c1[0],$c1[1],$c2[2],$c2[3]);
                            }
                            $link->SetPath(0);
                            break;
                        case CONSTRAIN_ENDEND:
                            if( $c1[1] < $c2[1] ) {
                                $link = new GanttLink($c1[2],$c1[3],$c2[2],$c2[1]);
                            }
                            else {
                                $link = new GanttLink($c1[2],$c1[1],$c2[2],$c2[3]);
                            }
                            $link->SetPath(1);
                            break;
                        case CONSTRAIN_STARTSTART:
                            if( $c1[1] < $c2[1] ) {
                                $link = new GanttLink($c1[0],$c1[3],$c2[0],$c2[1]);
                            }
                            else {
                                $link = new GanttLink($c1[0],$c1[1],$c2[0],$c2[3]);
                            }
                            $link->SetPath(3);
                            break;
                        default:
                            JpGraphError::Raise('Unknown constrain type specified from row='.
                                $this->iObj[$i]->iVPos.
                                ' to row='.$vpos);
                            break;
                    }
                    $link->SetColor($this->iObj[$i]->iConstrainColor);
                    $link->SetArrow($this->iObj[$i]->iConstrainArrowSize,
                                    $this->iObj[$i]->iConstrainArrowType);
                    $link->Stroke($this->img);
                }
            }
        }

    }

    function GetCSIMAreas() {
        if( !$this->iHasStroked )
            $this->Stroke(_CSIM_SPECIALFILE);
        $csim='';
        $n = count($this->iObj);
        for( $i=$n-1; $i >= 0; --$i ) 
            $csim .= $this->iObj[$i]->GetCSIMArea();
        return $csim;
    }
}

//===================================================
// CLASS TextProperty
// Description: Holds properties for a text
//===================================================
class TextProperty {
    var $iFFamily=FF_FONT1,$iFStyle=FS_NORMAL,$iFSize=10;
    var $iColor="black";
    var $iShow=true;
    var $iText="";
    var $iHAlign="left",$iVAlign="bottom";
    var $csimtarget='',$csimalt='';
        
//---------------
// CONSTRUCTOR  
    function TextProperty($aTxt="") {
        $this->iText = $aTxt;
    }           
        
//---------------
// PUBLIC METHODS       
    function Set($aTxt) {
        $this->iText = $aTxt;
    }

    function SetCSIMTarget($aTarget,$aAltText='') {
        $this->csimtarget=$aTarget;
        $this->csimalt=$aAltText;
    }
    
    function SetCSIMAlt($aAlt) {
        $this->csimalt=$aAlt;
    }

    // Set text color
    function SetColor($aColor) {
        $this->iColor = $aColor;
    }
        
    function HasTabs() {
        return substr_count($this->iText,"\t") > 0;
    }
        
    // Get number of tabs in string
    function GetNbrTabs() {
        substr_count($this->iText,"\t");
    }
        
    // Set alignment
    function Align($aHAlign,$aVAlign="bottom") {
        $this->iHAlign=$aHAlign;
        $this->iVAlign=$aVAlign;
    }
        
    // Specify font
    function SetFont($aFFamily,$aFStyle=FS_NORMAL,$aFSize=10) {
        $this->iFFamily = $aFFamily;
        $this->iFStyle   = $aFStyle;
        $this->iFSize    = $aFSize;
    }
        
    // Get width of text. If text contains several columns separated by
    // tabs then return both the total width as well as an array with a 
    // width for each column.
    function GetWidth($aImg,$aUseTabs=false,$aTabExtraMargin=1.1) {
        if( strlen($this->iText)== 0 ) return;
        $tmp = split("\t",$this->iText);
        $aImg->SetFont($this->iFFamily,$this->iFStyle,$this->iFSize);
        if( count($tmp) <= 1 || !$aUseTabs ) {
            return $aImg->GetTextWidth($this->iText);
        }
        else {
            $tot=0;
            for($i=0; $i<count($tmp); ++$i) {
                $res[$i] = $aImg->GetTextWidth($tmp[$i]);
                $tot += $res[$i]*$aTabExtraMargin;
            }
            return array($tot,$res);
        }
    }
        
    // Get total height of text
    function GetHeight($aImg) {
        $aImg->SetFont($this->iFFamily,$this->iFStyle,$this->iFSize);
        return $aImg->GetFontHeight();
    }
        
    // Unhide/hide the text     
    function Show($aShow) {
        $this->iShow=$aShow;
    }
        
    // Stroke text at (x,y) coordinates. If the text contains tabs then the
    // x parameter should be an array of positions to be used for each successive
    // tab mark. If no array is supplied then the tabs will be ignored.
    function Stroke($aImg,$aX,$aY) {
        if( $this->iShow ) {
            $aImg->SetColor($this->iColor);
            $aImg->SetFont($this->iFFamily,$this->iFStyle,$this->iFSize);
            $aImg->SetTextAlign($this->iHAlign,$this->iVAlign);                 
            if( $this->GetNbrTabs() <= 1 || !is_array($aX) ) {
                                // Get rid of any "\t" characters and stroke string
                $aImg->StrokeText($aX,$aY,str_replace("\t"," ",$this->iText));
            }
            else {
                $tmp = split("\t",$this->iText);
                $n = min(count($tmp),count($aX));
                for($i=0; $i<$n; ++$i) {
                    $aImg->StrokeText($aX[$i],$aY,$tmp[$i]);
                }       
            }
        }
    }
}

//===================================================
// CLASS HeaderProperty
// Description: Data encapsulating class to hold property 
// for each type of the scale headers
//===================================================
class HeaderProperty {
    var $iTitleVertMargin=3,$iFFamily=FF_FONT0,$iFStyle=FS_NORMAL,$iFSize=8;
    var $iFrameColor="black",$iFrameWeight=1;
    var $iShowLabels=true,$iShowGrid=true;
    var $iBackgroundColor="white";
    var $iWeekendBackgroundColor="lightgray",$iSundayTextColor="red"; // these are only used with day scale
    var $iTextColor="black";
    var $iLabelFormStr="%d";
    var $grid,$iStyle=0;

//---------------
// CONSTRUCTOR  
    function HeaderProperty() {
        $this->grid = new LineProperty();
    }

//---------------
// PUBLIC METHODS               
    function Show($aShow) {
        $this->iShowLabels = $aShow;
    }
        
    function SetFont($aFFamily,$aFStyle=FS_NORMAL,$aFSize=10) {
        $this->iFFamily = $aFFamily;
        $this->iFStyle   = $aFStyle;
        $this->iFSize    = $aFSize;
    }

    function SetFontColor($aColor) {
        $this->iTextColor = $aColor;
    }
        
    function GetFontHeight($aImg) {
        $aImg->SetFont($this->iFFamily,$this->iFStyle,$this->iFSize);
        return $aImg->GetFontHeight();
    }

    function GetFontWidth($aImg) {
        $aImg->SetFont($this->iFFamily,$this->iFStyle,$this->iFSize);
        return $aImg->GetFontWidth();
    }
        
    function SetStyle($aStyle) {
        $this->iStyle = $aStyle;
    }
        
    function SetBackgroundColor($aColor) {
        $this->iBackgroundColor=$aColor;
    }

    function SetFrameWeight($aWeight) {
        $this->iFrameWeight=$aWeight;
    }

    function SetFrameColor($aColor) {
        $this->iFrameColor=$aColor;
    }
        
    // Only used by day scale
    function SetWeekendColor($aColor) {
        $this->iWeekendBackgroundColor=$aColor;
    }
        
    // Only used by day scale
    function SetSundayFontColor($aColor) {
        $this->iSundayTextColor=$aColor;
    }
        
    function SetTitleVertMargin($aMargin) {
        $this->iTitleVertMargin=$aMargin;
    }
        
    function SetLabelFormatString($aStr) {
        $this->iLabelFormStr=$aStr;
    }
}

//===================================================
// CLASS GanttScale
// Description: Responsible for calculating and showing
// the scale in a gantt chart. This includes providing methods for
// converting dates to position in the chart as well as stroking the
// date headers (days, week, etc).
//===================================================
class GanttScale {
    var $day,$week,$month,$year;
    var $divider,$dividerh,$tableTitle;
    var $iStartDate=-1,$iEndDate=-1;
    // Number of gantt bar position (n.b not necessariliy the same as the number of bars)
    // we could have on bar in position 1, and one bar in position 5 then there are two
    // bars but the number of bar positions is 5
    var $iVertLines=-1; 
    // The width of the labels (defaults to the widest of all labels)
    var $iLabelWidth;   
    // Out image to stroke the scale to
    var $iImg;  
    var $iTableHeaderBackgroundColor="white",$iTableHeaderFrameColor="black";
    var $iTableHeaderFrameWeight=1;
    var $iAvailableHeight=-1,$iVertSpacing=-1,$iVertHeaderSize=-1;
    var $iDateLocale;
    var $iVertLayout=GANTT_EVEN;
    var $iTopPlotMargin=10,$iBottomPlotMargin=15;
    var $iUsePlotWeekendBackground=true;
    var $iWeekStart = 1;        // Default to have weekends start on Monday
        
//---------------
// CONSTRUCTOR  
    function GanttScale(&$aImg) {
        $this->iImg = &$aImg;           
        $this->iDateLocale = new DateLocale();
        $this->day = new HeaderProperty();
        $this->day->grid->SetColor("gray");

        $this->week = new HeaderProperty();
        $this->week->SetLabelFormatString("w%d");
        $this->week->SetFont(FF_FONT1);

        $this->month = new HeaderProperty();
        $this->month->SetFont(FF_FONT1,FS_BOLD);

        $this->year = new HeaderProperty();
        $this->year->SetFont(FF_FONT1,FS_BOLD);         
                
        $this->divider=new LineProperty();
        $this->dividerh=new LineProperty();             
        $this->tableTitle=new TextProperty();
    }
        
//---------------
// PUBLIC METHODS       
    // Specify what headers should be visible
    function ShowHeaders($aFlg) {
        $this->day->Show($aFlg & GANTT_HDAY);
        $this->week->Show($aFlg & GANTT_HWEEK);
        $this->month->Show($aFlg & GANTT_HMONTH);
        $this->year->Show($aFlg & GANTT_HYEAR);

        // Make some default settings of gridlines whihc makes sense
        if( $aFlg & GANTT_HWEEK ) {
            $this->month->grid->Show(false);
            $this->year->grid->Show(false);
        }
    }
        
    // Should the weekend background stretch all the way down in the plotarea
    function UseWeekendBackground($aShow) {
        $this->iUsePlotWeekendBackground = $aShow;
    }
        
    // Have a range been specified?
    function IsRangeSet() {
        return $this->iStartDate!=-1 && $this->iEndDate!=-1;
    }
        
    // Should the layout be from top or even?
    function SetVertLayout($aLayout) {
        $this->iVertLayout = $aLayout;
    }
        
    // Which locale should be used?
    function SetDateLocale($aLocale) {
        $this->iDateLocale->Set($aLocale);
    }
        
    // Number of days we are showing
    function GetNumberOfDays() {
        return round(($this->iEndDate-$this->iStartDate)/SECPERDAY)+1;
    }
        
    // The width of the actual plot area
    function GetPlotWidth() {
        $img=$this->iImg;
        return $img->width - $img->left_margin - $img->right_margin;
    }

    // Specify the width of the titles(labels) for the activities
    // (This is by default set to the minimum width enought for the
    // widest title)
    function SetLabelWidth($aLabelWidth) {
        $this->iLabelWidth=$aLabelWidth;
        }

        // Which day should the week start?
        // 0==Sun, 1==Monday, 2==Tuesday etc
        function SetWeekStart($aStartDay) {
                $this->iWeekStart = $aStartDay % 7;
                
                //Recalculate the startday since this will change the week start
                $this->SetRange($this->iStartDate,$this->iEndDate);
        }
        
    // Do we show day scale?
    function IsDisplayDay() {
        return $this->day->iShowLabels;
    }
        
    // Do we show week scale?
    function IsDisplayWeek() {
        return $this->week->iShowLabels;
    }
        
    // Do we show month scale?
    function IsDisplayMonth() {
        return $this->month->iShowLabels;
    }
        
    // Do we show year scale?
    function IsDisplayYear() {
        return $this->year->iShowLabels;
    }

    // Specify spacing (in percent of bar height) between activity bars
    function SetVertSpacing($aSpacing) {
        $this->iVertSpacing = $aSpacing;
    }

    // Specify scale min and max date either as timestamp or as date strings
    // Always round to the nearest week boundary
    function SetRange($aMin,$aMax) {
        $this->iStartDate = $this->NormalizeDate($aMin);
        $this->iEndDate = $this->NormalizeDate($aMax);  
        }


        // Adjust the start and end date so they fit to beginning/ending
        // of the week taking the specified week start day into account.
        function AdjustStartEndDay() {
                // Get day in week for start and ending date (Sun==0)
                $ds=strftime("%w",$this->iStartDate);
                $de=strftime("%w",$this->iEndDate);     
        
                // We want to start on iWeekStart day. But first we subtract a week
                // if the startdate is "behind" the day the week start at. 
                // This way we ensure that the given start date is always included 
                // in the range. If we don't do this the nearest correct weekday in the week 
                // to start at might be later than the start date.
                if( $ds < $this->iWeekStart )
                        $d = strtotime('-7 day',$this->iStartDate);
                else
                        $d = $this->iStartDate;
                $adjdate = strtotime(($this->iWeekStart-$ds).' day',$d /*$this->iStartDate*/ );
                $this->iStartDate = $adjdate;
        
                // We want to end on the last day of the week
                $preferredEndDay = ($this->iWeekStart+6)%7;
                if( $preferredEndDay != $de ) { 
                        // Solve equivalence eq:    $de + x ~ $preferredDay (mod 7)
                        $adj = (7+($preferredEndDay - $de)) % 7;
                        $adjdate = strtotime("+$adj day",$this->iEndDate);
                        $this->iEndDate = $adjdate;     
                }       
        }

    // Specify background for the table title area (upper left corner of the table)     
    function SetTableTitleBackground($aColor) {
        $this->iTableHeaderBackgroundColor = $aColor;
    }

///////////////////////////////////////
// PRIVATE Methods
        
    // Determine the height of all the scale headers combined
    function GetHeaderHeight() {
        $img=$this->iImg;
        $height=1;
        if( $this->day->iShowLabels ) {
            $height += $this->day->GetFontHeight($img);
            $height += $this->day->iTitleVertMargin;
        }
        if( $this->week->iShowLabels ) {
            $height += $this->week->GetFontHeight($img);
            $height += $this->week->iTitleVertMargin;
        }
        if( $this->month->iShowLabels ) {
            $height += $this->month->GetFontHeight($img);
            $height += $this->month->iTitleVertMargin;
        }
        if( $this->year->iShowLabels ) {
            $height += $this->year->GetFontHeight($img);
            $height += $this->year->iTitleVertMargin;
        }
        return $height;
    }
        
    // Get width (in pisels) for a single day
    function GetDayWidth() {
        return ($this->GetPlotWidth()-$this->iLabelWidth+1)/$this->GetNumberOfDays();   
    }

    // Nuber of days in a year
    function GetNumDaysInYear($aYear) {
        if( $this->IsLeap($aYear) )
            return 366;
        else
            return 365;
    }
        
    // Get week number 
    function GetWeekNbr($aDate) {
        // We can't use the internal strftime() since it gets the weeknumber
        // wrong since it doesn't follow ISO.
        // Even worse is that this works differently if we are on a Windows
        // or UNIX box (it even differs between UNIX boxes how strftime()
        // is natively implemented)
        //
        // Credit to Nicolas Hoizey <nhoizey@phpheaven.net> for this elegant
        // version of Week Nbr calculation. 
                
        $day = $this->NormalizeDate($aDate);
                
        /*-------------------------------------------------------------------------
          According to ISO-8601 :
          "Week 01 of a year is per definition the first week that has the Thursday in this year,
          which is equivalent to the week that contains the fourth day of January.
          In other words, the first week of a new year is the week that has the majority of its
          days in the new year."
                  
          Be carefull, with PHP, -3 % 7 = -3, instead of 4 !!!
                  
          day of year             = date("z", $day) + 1
          offset to thursday      = 3 - (date("w", $day) + 6) % 7
          first thursday of year  = 1 + (11 - date("w", mktime(0, 0, 0, 1, 1, date("Y", $day)))) % 7
          week number             = (thursday's day of year - first thursday's day of year) / 7 + 1
          ---------------------------------------------------------------------------*/
                 
        $thursday = $day + 60 * 60 * 24 * (3 - (date("w", $day) + 6) % 7);              // take week's thursday
        $week = 1 + (date("z", $thursday) - (11 - date("w", mktime(0, 0, 0, 1, 1, date("Y", $thursday)))) % 7) / 7;
                  
        return $week;
    }
        
    // Is year a leap year?
    function IsLeap($aYear) {
        // Is the year a leap year?
        //$year = 0+date("Y",$aDate);
        if( $aYear % 4 == 0)
            if( !($aYear % 100 == 0) || ($aYear % 400 == 0) )
                return true;
        return false;
    }

    // Get current year
    function GetYear($aDate) {
        return 0+Date("Y",$aDate);
    }
        
    // Return number of days in a year
    function GetNumDaysInMonth($aMonth,$aYear) {
        $days=array(31,28,31,30,31,30,31,31,30,31,30,31);
        $daysl=array(31,29,31,30,31,30,31,31,30,31,30,31);
        if( $this->IsLeap($aYear))
            return $daysl[$aMonth];
        else
            return $days[$aMonth];
    }
        
    // Get day in month
    function GetMonthDayNbr($aDate) {
        return 0+strftime("%d",$aDate);
    }

    // Get day in year
    function GetYearDayNbr($aDate) {
        return 0+strftime("%j",$aDate);
    }
        
    // Get month number
    function GetMonthNbr($aDate) {
        return 0+strftime("%m",$aDate);
    }
        
    // Translate a date to screen coordinates   (horizontal scale)
    function TranslateDate($aDate) {
        $aDate = $this->NormalizeDate($aDate);
        $img=$this->iImg;               
        return ($aDate-$this->iStartDate)/SECPERDAY*$this->GetDayWidth()+$img->left_margin+$this->iLabelWidth;;
    }

    // Get screen coordinatesz for the vertical position for a bar              
    function TranslateVertPos($aPos) {
        $img=$this->iImg;
        $ph=$this->iAvailableHeight;
        if( $aPos > $this->iVertLines ) 
            JpGraphError::Raise("Illegal vertical position $aPos");
        if( $this->iVertLayout == GANTT_EVEN ) {
            // Position the top bar at 1 vert spacing from the scale
            return round($img->top_margin + $this->iVertHeaderSize +  ($aPos+1)*$this->iVertSpacing);
        }
        else {
            // position the top bar at 1/2 a vert spacing from the scale
            return round($img->top_margin + $this->iVertHeaderSize  + $this->iTopPlotMargin + ($aPos+1)*$this->iVertSpacing);           
        }
    }
        
    // What is the vertical spacing?
    function GetVertSpacing() {
        return $this->iVertSpacing;
    }
                                        
    // Convert a date to timestamp
    function NormalizeDate($aDate) {
        if( is_string($aDate) )
            return strtotime($aDate);
        elseif( is_int($aDate) || is_float($aDate) )
            return $aDate;
        else
            JpGraphError::Raise("Unknown date format in GanttScale ($aDate).");
    }

    // Stroke the day scale (including gridlines)                       
    function StrokeDays($aYCoord) {
        $wdays=$this->iDateLocale->GetDayAbb(); 
        $img=$this->iImg;       
        $daywidth=$this->GetDayWidth();
        $xt=$img->left_margin+$this->iLabelWidth;
        $yt=$aYCoord+$img->top_margin;          
        if( $this->day->iShowLabels ) {
            $img->SetFont($this->day->iFFamily,$this->day->iFStyle,$this->day->iFSize);
            $xb=$img->width-$img->right_margin;
            $yb=$yt + $img->GetFontHeight() + $this->day->iTitleVertMargin + $this->day->iFrameWeight;
            $img->SetColor($this->day->iBackgroundColor);
            $img->FilledRectangle($xt,$yt,$xb,$yb);

            $img->SetColor($this->day->grid->iColor);
            $x = $xt;   
            $img->SetTextAlign("center");
            $day = $this->iWeekStart;
            //echo "n=".$this->GetNumberOfDays()."<p>";
            for($i=0; $i<$this->GetNumberOfDays(); ++$i, $x+=$daywidth, $day += 1,$day %= 7) {
                if( $day==6 || $day==0 ) {
                    $img->PushColor($this->day->iWeekendBackgroundColor);
                    if( $this->iUsePlotWeekendBackground )
                        $img->FilledRectangle($x,$yt+$this->day->iFrameWeight,$x+$daywidth,$img->height-$img->bottom_margin);                                           
                    else
                        $img->FilledRectangle($x,$yt+$this->day->iFrameWeight,$x+$daywidth,$yb-$this->day->iFrameWeight);
                    $img->PopColor();
                }
                if( $day==0 ) 
                    $img->PushColor($this->day->iSundayTextColor);
                else
                    $img->PushColor($this->day->iTextColor);
                $img->StrokeText(round($x+$daywidth/2+1),
                                 round($yb-$this->day->iTitleVertMargin),
                                 $wdays[$day]);
                $img->PopColor();                                               
                $img->Line($x,$yt,$x,$yb);
                $this->day->grid->Stroke($img,$x,$yb,$x,$img->height-$img->bottom_margin);
            }                   
            $img->SetColor($this->day->iFrameColor);
            $img->SetLineWeight($this->day->iFrameWeight);
            $img->Rectangle($xt,$yt,$xb,$yb);
            return $yb - $img->top_margin;
        }
        return $aYCoord;
    }
        
    // Stroke week header and grid
    function StrokeWeeks($aYCoord) {
        $wdays=$this->iDateLocale->GetDayAbb(); 
        $img=$this->iImg;       
        $weekwidth=$this->GetDayWidth()*7;
        $xt=$img->left_margin+$this->iLabelWidth;
        $yt=$aYCoord+$img->top_margin;          
        $img->SetFont($this->week->iFFamily,$this->week->iFStyle,$this->week->iFSize);
        $xb=$img->width-$img->right_margin;
        $yb=$yt + $img->GetFontHeight() + $this->week->iTitleVertMargin + $this->week->iFrameWeight;
                
        $week = $this->iStartDate;
        $weeknbr=$this->GetWeekNbr($week);
        if( $this->week->iShowLabels ) {
            $img->SetColor($this->week->iBackgroundColor);
            $img->FilledRectangle($xt,$yt,$xb,$yb);
            $img->SetColor($this->week->grid->iColor);
            $x = $xt;
            if( $this->week->iStyle==WEEKSTYLE_WNBR ) {
                $img->SetTextAlign("center");
                $txtOffset = $weekwidth/2+1;
            }
            elseif( $this->week->iStyle==WEEKSTYLE_FIRSTDAY  || 
                    $this->week->iStyle==WEEKSTYLE_FIRSTDAY2 ||
                    $this->week->iStyle==WEEKSTYLE_FIRSTDAYWNBR ||
                    $this->week->iStyle==WEEKSTYLE_FIRSTDAY2WNBR ) {
                $img->SetTextAlign("left");
                $txtOffset = 3;
            }
            else
                JpGraphError::Raise("Unknown formatting style for week.");
                                
            for($i=0; $i<$this->GetNumberOfDays()/7; ++$i, $x+=$weekwidth) {
                $img->PushColor($this->week->iTextColor);
                                
                if( $this->week->iStyle==WEEKSTYLE_WNBR )
                    $txt = sprintf($this->week->iLabelFormStr,$weeknbr);
                elseif( $this->week->iStyle==WEEKSTYLE_FIRSTDAY || 
                        $this->week->iStyle==WEEKSTYLE_FIRSTDAYWNBR ) 
                    $txt = date("j/n",$week);
                elseif( $this->week->iStyle==WEEKSTYLE_FIRSTDAY2 || 
                        $this->week->iStyle==WEEKSTYLE_FIRSTDAY2WNBR ) {
                    $monthnbr = date("n",$week)-1;
                    $shortmonth = $this->iDateLocale->GetShortMonthName($monthnbr);
                    $txt = Date("j",$week)." ".$shortmonth;
                }

                if( $this->week->iStyle==WEEKSTYLE_FIRSTDAYWNBR ||
                    $this->week->iStyle==WEEKSTYLE_FIRSTDAY2WNBR ) {
                    $w = sprintf($this->week->iLabelFormStr,$weeknbr);
                    $txt .= ' '.$w;
                }
                                
                $img->StrokeText(round($x+$txtOffset),
                                 round($yb-$this->week->iTitleVertMargin),$txt);
                                
                $week = strtotime('+7 day',$week); 
                $weeknbr = $this->GetWeekNbr($week);
                $img->PopColor();                                               
                $img->Line($x,$yt,$x,$yb);
                $this->week->grid->Stroke($img,$x,$yb,$x,$img->height-$img->bottom_margin);
            }                   
            $img->SetColor($this->week->iFrameColor);
            $img->SetLineWeight($this->week->iFrameWeight);
            $img->Rectangle($xt,$yt,$xb,$yb);
            return $yb-$img->top_margin;
        }
        return $aYCoord;
    }   
        
    // Format the mont scale header string
    function GetMonthLabel($aMonthNbr,$year) {
        $sn = $this->iDateLocale->GetShortMonthName($aMonthNbr);
        $ln = $this->iDateLocale->GetLongMonthName($aMonthNbr);
        switch($this->month->iStyle) {
            case MONTHSTYLE_SHORTNAME:
                $m=$sn;
            break;
            case MONTHSTYLE_LONGNAME:
                $m=$ln;
            break;
            case MONTHSTYLE_SHORTNAMEYEAR2:
                $m=$sn." '".substr("".$year,2);
            break;
            case MONTHSTYLE_SHORTNAMEYEAR4:
                $m=$sn." ".$year;
            break;
            case MONTHSTYLE_LONGNAMEYEAR2:
                $m=$ln." '".substr("".$year,2);
            break;
            case MONTHSTYLE_LONGNAMEYEAR4:
                $m=$ln." ".$year;
            break;
        }
        return $m;
    }
        
    // Stroke month scale and gridlines
    function StrokeMonths($aYCoord) {
        if( $this->month->iShowLabels ) {
            $monthnbr = $this->GetMonthNbr($this->iStartDate)-1; 
            $img=$this->iImg;   
                
            $xt=$img->left_margin+$this->iLabelWidth;
            $yt=$aYCoord+$img->top_margin;              
            $img->SetFont($this->month->iFFamily,$this->month->iFStyle,$this->month->iFSize);
            $xb=$img->width-$img->right_margin;
            $yb=$yt + $img->GetFontHeight() + $this->month->iTitleVertMargin + $this->month->iFrameWeight;
                        
            $img->SetColor($this->month->iBackgroundColor);
            $img->FilledRectangle($xt,$yt,$xb,$yb);

            $img->SetLineWeight($this->month->grid->iWeight);
            $img->SetColor($this->month->iTextColor);
            $year = 0+strftime("%Y",$this->iStartDate);
            $img->SetTextAlign("center");
            if( $this->GetMonthNbr($this->iStartDate) == $this->GetMonthNbr($this->iEndDate)  
                && $this->GetYear($this->iStartDate)==$this->GetYear($this->iEndDate) ) {
                $monthwidth=$this->GetDayWidth()*($this->GetMonthDayNbr($this->iEndDate) - $this->GetMonthDayNbr($this->iStartDate) + 1);
            } 
            else {
                $monthwidth=$this->GetDayWidth()*($this->GetNumDaysInMonth($monthnbr,$year)-$this->GetMonthDayNbr($this->iStartDate)+1);
            }
            // Is it enough space to stroke the first month?
            $monthName = $this->GetMonthLabel($monthnbr,$year);
            if( $monthwidth >= 1.2*$img->GetTextWidth($monthName) ) {
                $img->SetColor($this->month->iTextColor);                               
                $img->StrokeText(round($xt+$monthwidth/2+1),
                                 round($yb-$this->month->iTitleVertMargin),
                                 $monthName);
            }
            $x = $xt + $monthwidth;
            while( $x < $xb ) {
                $img->SetColor($this->month->grid->iColor);                             
                $img->Line($x,$yt,$x,$yb);
                $this->month->grid->Stroke($img,$x,$yb,$x,$img->height-$img->bottom_margin);
                $monthnbr++;
                if( $monthnbr==12 ) {
                    $monthnbr=0;
                    $year++;
                }
                $monthName = $this->GetMonthLabel($monthnbr,$year);
                $monthwidth=$this->GetDayWidth()*$this->GetNumDaysInMonth($monthnbr,$year);                             
                if( $x + $monthwidth < $xb )
                    $w = $monthwidth;
                else
                    $w = $xb-$x;
                if( $w >= 1.2*$img->GetTextWidth($monthName) ) {
                    $img->SetColor($this->month->iTextColor);                           
                    $img->StrokeText(round($x+$w/2+1),
                                     round($yb-$this->month->iTitleVertMargin),$monthName);
                }
                $x += $monthwidth;
            }   
            $img->SetColor($this->month->iFrameColor);
            $img->SetLineWeight($this->month->iFrameWeight);
            $img->Rectangle($xt,$yt,$xb,$yb);                   
            return $yb-$img->top_margin;
        }
        return $aYCoord;
    }

    // Stroke year scale and gridlines
    function StrokeYears($aYCoord) {
        if( $this->year->iShowLabels ) {
            $year = $this->GetYear($this->iStartDate); 
            $img=$this->iImg;   
                
            $xt=$img->left_margin+$this->iLabelWidth;
            $yt=$aYCoord+$img->top_margin;              
            $img->SetFont($this->year->iFFamily,$this->year->iFStyle,$this->year->iFSize);
            $xb=$img->width-$img->right_margin;
            $yb=$yt + $img->GetFontHeight() + $this->year->iTitleVertMargin + $this->year->iFrameWeight;
                        
            $img->SetColor($this->year->iBackgroundColor);
            $img->FilledRectangle($xt,$yt,$xb,$yb);
            $img->SetLineWeight($this->year->grid->iWeight);
            $img->SetTextAlign("center");
            if( $year == $this->GetYear($this->iEndDate) )
                $yearwidth=$this->GetDayWidth()*($this->GetYearDayNbr($this->iEndDate)-$this->GetYearDayNbr($this->iStartDate)+1);
            else
                $yearwidth=$this->GetDayWidth()*($this->GetNumDaysInYear($year)-$this->GetYearDayNbr($this->iStartDate)+1);
                        
            // The space for a year must be at least 20% bigger than the actual text 
            // so we allow 10% margin on each side
            if( $yearwidth >= 1.20*$img->GetTextWidth("".$year) ) {
                $img->SetColor($this->year->iTextColor);                                
                $img->StrokeText(round($xt+$yearwidth/2+1),
                                 round($yb-$this->year->iTitleVertMargin),
                                 $year);
            }
            $x = $xt + $yearwidth;
            while( $x < $xb ) {
                $img->SetColor($this->year->grid->iColor);                              
                $img->Line($x,$yt,$x,$yb);
                $this->year->grid->Stroke($img,$x,$yb,$x,$img->height-$img->bottom_margin);
                $year += 1;
                $yearwidth=$this->GetDayWidth()*$this->GetNumDaysInYear($year);                         
                if( $x + $yearwidth < $xb )
                    $w = $yearwidth;
                else
                    $w = $xb-$x;
                if( $w >= 1.2*$img->GetTextWidth("".$year) ) {
                    $img->SetColor($this->year->iTextColor);
                    $img->StrokeText(round($x+$w/2+1),
                                     round($yb-$this->year->iTitleVertMargin),
                                     $year);
                }
                $x += $yearwidth;
            }
            $img->SetColor($this->year->iFrameColor);
            $img->SetLineWeight($this->year->iFrameWeight);
            $img->Rectangle($xt,$yt,$xb,$yb);                   
            return $yb-$img->top_margin;
        }
        return $aYCoord;
    }
        
    // Stroke table title (upper left corner)
    function StrokeTableHeaders($aYBottom) {
        $img=$this->iImg;
        $xt=$img->left_margin;
        $yt=$img->top_margin;
        $xb=$xt+$this->iLabelWidth;
        $yb=$aYBottom+$img->top_margin;
                
        $img->SetColor($this->iTableHeaderBackgroundColor);
        $img->FilledRectangle($xt,$yt,$xb,$yb);
        $this->tableTitle->Align("center","center");
        $this->tableTitle->Stroke($img,$xt+($xb-$xt)/2+1,$yt+($yb-$yt)/2);              
        $img->SetColor($this->iTableHeaderFrameColor);
        $img->SetLineWeight($this->iTableHeaderFrameWeight);
        $img->Rectangle($xt,$yt,$xb,$yb);
                
        // Draw the vertical dividing line
        $this->divider->Stroke($img,$xb,$yt,$xb,$img->height-$img->bottom_margin);
                
        // Draw the horizontal dividing line            
        $this->dividerh->Stroke($img,$xt,$yb,$img->width-$img->right_margin,$yb);               
    }

    // Main entry point to stroke scale
    function Stroke() {
        if( !$this->IsRangeSet() )
            JpGraphError::Raise("Gantt scale has not been specified.");
        $img=$this->iImg;
                
        // Stroke all headers. Aa argument we supply the offset from the
        // top which depends on any previous headers
        $offy=$this->StrokeYears(0);
        $offm=$this->StrokeMonths($offy);
        $offw=$this->StrokeWeeks($offm);
        $offd=$this->StrokeDays($offw);

        // We stroke again in case days also have gridlines that may have
        // overwritten the weeks gridline (or month/year). It may seem that we should have logic
        // in the days routine instead but this is much easier and wont make to much
        // of an performance impact.
        $this->StrokeWeeks($offm);              
        $this->StrokeMonths($offy);             
        $this->StrokeYears(0);
        $this->StrokeTableHeaders($offd);
                
        // Now we can calculate the correct scaling factor for each vertical position
        $this->iAvailableHeight = $img->height - $img->top_margin - $img->bottom_margin - $offd;                
        $this->iVertHeaderSize = $offd;
        if( $this->iVertSpacing == -1 )
            $this->iVertSpacing = $this->iAvailableHeight / $this->iVertLines;
    }   
}

//===================================================
// CLASS GanttPlotObject
// The common signature for a Gantt object
//===================================================
class GanttPlotObject {
    var $iVPos=0;                                       // Vertical position
    var $iLabelLeftMargin=2;    // Title margin
    var $iStart="";                             // Start date
    var $title,$caption;
    var $iCaptionMargin=5;
    var $csimarea='',$csimtarget='',$csimalt='';
    var $iConstrainType=CONSTRAIN_ENDSTART,$iConstrainRow=-1;
    var $iConstrainColor='black',$iConstrainArrowSize=ARROW_S2,$iConstrainArrowType=ARROWT_SOLID;
    var $iConstrainPos=array();
                
    function GanttPlotObject() {
        $this->title = new TextProperty();
        $this->title->Align("left","center");
        $this->caption = new TextProperty();
    }

    function GetCSIMArea() {
        return $this->csimarea;
    }

    function SetCSIMTarget($aTarget,$aAltText='') {
        $this->csimtarget=$aTarget;
        $this->csimalt=$aAltText;
    }
    
    function SetCSIMAlt($aAlt) {
        $this->csimalt=$aAlt;
    }

    function SetConstrain($aRow,$aType,$aColor='black',$aArrowSize=ARROW_S2,$aArrowType=ARROWT_SOLID) {
        $this->iConstrainRow = $aRow;
        $this->iConstrainType = $aType;
        $this->iConstrainColor = $aColor;
        $this->iConstrainArrowSize = $aArrowSize;
        $this->iConstrainArrowType = $aArrowType;
    }

    function SetConstrainPos($xt,$yt,$xb,$yb) {
        $this->iConstrainPos = array($xt,$yt,$xb,$yb);
    }

    function GetConstrain() {
        return array($this->iConstrainRow,$this->iConstrainType);
    }
        
    function GetMinDate() {
        return $this->iStart;
    }

    function GetMaxDate() {
        return $this->iStart;
    }
        
    function SetCaptionMargin($aMarg) {
        $this->iCaptionMargin=$aMarg;
    }

    function GetAbsHeight($aImg) {
        return 0; 
    }
        
    function GetLineNbr() {
        return $this->iVPos;
    }

    function SetLabelLeftMargin($aOff) {
        $this->iLabelLeftMargin=$aOff;
    }           
}

//===================================================
// CLASS Progress
// Holds parameters for the progress indicator 
// displyed within a bar
//===================================================
class Progress {
    var $iProgress=-1, $iColor="black", $iFillColor='black';
    var $iPattern=GANTT_SOLID;
    var $iDensity=98, $iHeight=0.65; 
        
    function Set($aProg) {
        if( $aProg < 0.0 || $aProg > 1.0 )
            JpGraphError::Raise("Progress value must in range [0, 1]");
        $this->iProgress = $aProg;
    }

    function SetPattern($aPattern,$aColor="blue",$aDensity=98) {                
        $this->iPattern = $aPattern;
        $this->iColor = $aColor;
        $this->iDensity = $aDensity;
    }

    function SetFillColor($aColor) {
        $this->iFillColor = $aColor;
    }
        
    function SetHeight($aHeight) {
        $this->iHeight = $aHeight;
    }
}

//===================================================
// CLASS GanttBar
// Responsible for formatting individual gantt bars
//===================================================
class GanttBar extends GanttPlotObject {
    var $iEnd;
    var $iHeightFactor=0.5;
    var $iFillColor="white",$iFrameColor="black";
    var $iShadow=false,$iShadowColor="darkgray",$iShadowWidth=1,$iShadowFrame="black";
    var $iPattern=GANTT_RDIAG,$iPatternColor="blue",$iPatternDensity=95;
    var $leftMark,$rightMark;
    var $progress;
//---------------
// CONSTRUCTOR  
    function GanttBar($aPos,$aLabel,$aStart,$aEnd,$aCaption="",$aHeightFactor=0.6) {
        parent::GanttPlotObject();      
        $this->iStart = $aStart;        
        // Is the end date given as a date or as number of days added to start date?
        if( is_string($aEnd) )
            $this->iEnd = strtotime($aEnd)+SECPERDAY;
        elseif(is_int($aEnd) || is_float($aEnd) ) 
            $this->iEnd = strtotime($aStart)+round($aEnd*SECPERDAY);
        $this->iVPos = $aPos;
        $this->iHeightFactor = $aHeightFactor;
        $this->title->Set($aLabel);
        $this->caption = new TextProperty($aCaption);
        $this->caption->Align("left","center");
        $this->leftMark =new PlotMark();
        $this->leftMark->Hide();
        $this->rightMark=new PlotMark();
        $this->rightMark->Hide();
        $this->progress = new Progress();
    }
        
//---------------
// PUBLIC METHODS       
    function SetShadow($aShadow=true,$aColor="gray") {
        $this->iShadow=$aShadow;
        $this->iShadowColor=$aColor;
    }
    
    function GetMaxDate() {
        return $this->iEnd;
    }
        
    function SetHeight($aHeight) {
        $this->iHeightFactor = $aHeight;
    }

    function SetColor($aColor) {
        $this->iFrameColor = $aColor;
    }

    function SetFillColor($aColor) {
        $this->iFillColor = $aColor;
    }

    function GetAbsHeight($aImg) {
        if( is_int($this->iHeightFactor) || $this->leftMark->show || $this->rightMark->show ) {
            $m=-1;
            if( is_int($this->iHeightFactor) )
                $m = $this->iHeightFactor;
            if( $this->leftMark->show ) 
                $m = max($m,$this->leftMark->width*2);
            if( $this->rightMark->show ) 
                $m = max($m,$this->rightMark->width*2);
            return $m;
        }
        else
            return -1;
    }
        
    function SetPattern($aPattern,$aColor="blue",$aDensity=95) {                
        $this->iPattern = $aPattern;
        $this->iPatternColor = $aColor;
        $this->iPatternDensity = $aDensity;
    }

    function Stroke($aImg,$aScale) {
        $factory = new RectPatternFactory();
        $prect = $factory->Create($this->iPattern,$this->iPatternColor);
        $prect->SetDensity($this->iPatternDensity);

        // If height factor is specified as a float between 0,1 then we take it as meaning
        // percetage of the scale width between horizontal line.
        // If it is an integer > 1 we take it to mean the absolute height in pixels
        if( $this->iHeightFactor > -0.0 && $this->iHeightFactor <= 1.1)
            $vs = $aScale->GetVertSpacing()*$this->iHeightFactor;
        elseif(is_int($this->iHeightFactor) && $this->iHeightFactor>2 && $this->iHeightFactor<200)
            $vs = $this->iHeightFactor;
        else
            JpGraphError::Raise("Specified height (".$this->iHeightFactor.") for gantt bar is out of range.");
        
        // Clip date to min max dates to show
        $st = $aScale->NormalizeDate($this->iStart);
        $en = $aScale->NormalizeDate($this->iEnd);
        

        $limst = max($st,$aScale->iStartDate);
        $limen = min($en,$aScale->iEndDate+SECPERDAY);
                        
        $xt = round($aScale->TranslateDate($limst));
        $xb = round($aScale->TranslateDate($limen)-1); 
        $yt = round($aScale->TranslateVertPos($this->iVPos)-$vs-($aScale->GetVertSpacing()/2-$vs/2));
        $yb = round($aScale->TranslateVertPos($this->iVPos)-($aScale->GetVertSpacing()/2-$vs/2));
        $middle = round($yt+($yb-$yt)/2);
        $this->title->Stroke($aImg,$aImg->left_margin+$this->iLabelLeftMargin,$middle);         

        // CSIM for title
        if( $this->title->csimtarget != '' ) {
            $title_xt = $aImg->left_margin+$this->iLabelLeftMargin;
            $title_xb = $title_xt + $this->title->GetWidth($aImg);

            $coords = "$title_xt,$yt,$title_xb,$yt,$title_xb,$yb,$title_xt,$yb";
            $this->csimarea .= "<area shape=\"poly\" coords=\"$coords\" href=\"".$this->title->csimtarget."\"";
            if( $this->title->csimalt != '' ) {
                $tmp = $this->title->csimalt;
                $this->csimarea .= " alt=\"$tmp\" title=\"$tmp\"";
            }
            $this->csimarea .= ">\n";
        }

        // Check if the bar is totally outside the current scale range
        if( $en <  $aScale->iStartDate+SECPERDAY || $st > $aScale->iEndDate )
                return;
                        

        // Remember the positions for the bar
        $this->SetConstrainPos($xt,$yt,$xb,$yb);
                
        $prect->ShowFrame(false);
        $prect->SetBackground($this->iFillColor);
        if( $this->iShadow ) {
            $aImg->SetColor($this->iFrameColor);
            $aImg->ShadowRectangle($xt,$yt,$xb,$yb,$this->iFillColor,$this->iShadowWidth,$this->iShadowColor);                          
            $prect->SetPos(new Rectangle($xt+1,$yt+1,$xb-$xt-$this->iShadowWidth-2,$yb-$yt-$this->iShadowWidth-2));                             
            $prect->Stroke($aImg);
        }
        else {  
            $prect->SetPos(new Rectangle($xt,$yt,$xb-$xt+1,$yb-$yt+1));                         
            $prect->Stroke($aImg);
            $aImg->SetColor($this->iFrameColor);
            $aImg->Rectangle($xt,$yt,$xb,$yb);
        }

        // CSIM for bar
        if( $this->csimtarget != '' ) {

            $coords = "$xt,$yt,$xb,$yt,$xb,$yb,$xt,$yb";
            $this->csimarea .= "<area shape=\"poly\" coords=\"$coords\" href=\"".
                              $this->csimtarget."\"";
            if( $this->csimalt != '' ) {
                $tmp = $this->csimalt;
                $this->csimarea .= " alt=\"$tmp\" title=\"$tmp\"";
            }
            $this->csimarea .= ">\n";
        }

        // Draw progress bar inside activity bar
        if( $this->progress->iProgress > 0 ) {
                
            $xtp = $aScale->TranslateDate($st);
            $xbp = $aScale->TranslateDate($en);
            $len = ($xbp-$xtp)*$this->progress->iProgress;

            $endpos = $xtp+$len;
            if( $endpos > $xt ) {
                $len -= ($xt-$xtp); 

                // Make sure that the progess bar doesn't extend over the end date
                if( $xtp+$len-1 > $xb )
                    $len = $xb - $xtp + 1;
                
                if( $xtp < $xt ) 
                    $xtp = $xt;
                
                $prog = $factory->Create($this->progress->iPattern,$this->progress->iColor);
                $prog->SetDensity($this->progress->iDensity);
                $prog->SetBackground($this->progress->iFillColor);
                $barheight = ($yb-$yt+1);
                if( $this->iShadow ) 
                    $barheight -= $this->iShadowWidth;
                $progressheight = floor($barheight*$this->progress->iHeight);
                $marg = ceil(($barheight-$progressheight)/2);
                $pos = new Rectangle($xtp,$yt + $marg, $len,$barheight-2*$marg);
                $prog->SetPos($pos);
                $prog->Stroke($aImg);
            }
        }
        
        // We don't plot the end mark if the bar has been capped
        if( $limst == $st ) {
            $y = $middle;
            // We treat the RIGHT and LEFT triangle mark a little bi
            // special so that these marks are placed right under the
            // bar.
            if( $this->leftMark->GetType() == MARK_LEFTTRIANGLE ) {
                $y = $yb ; 
            }
            $this->leftMark->Stroke($aImg,$xt,$y);
        }
        if( $limen == $en ) {
            $y = $middle;
            // We treat the RIGHT and LEFT triangle mark a little bi
            // special so that these marks are placed right under the
            // bar.
            if( $this->rightMark->GetType() == MARK_RIGHTTRIANGLE ) {
                $y = $yb ; 
            }
            $this->rightMark->Stroke($aImg,$xb,$y);
            
            $margin = $this->iCaptionMargin;
            if( $this->rightMark->show ) 
                $margin += $this->rightMark->GetWidth();
            $this->caption->Stroke($aImg,$xb+$margin,$middle);          
        }
    }
}

//===================================================
// CLASS MileStone
// Responsible for formatting individual milestones
//===================================================
class MileStone extends GanttPlotObject {
    var $mark;
        
//---------------
// CONSTRUCTOR  
    function MileStone($aVPos,$aLabel,$aDate,$aCaption="") {
        GanttPlotObject::GanttPlotObject();
        $this->caption->Set($aCaption);
        $this->caption->Align("left","center");
        $this->caption->SetFont(FF_FONT1,FS_BOLD);
        $this->title->Set($aLabel);
        $this->title->SetColor("darkred");
        $this->mark = new PlotMark();
        $this->mark->SetWidth(10);
        $this->mark->SetType(MARK_DIAMOND);
        $this->mark->SetColor("darkred");
        $this->mark->SetFillColor("darkred");
        $this->iVPos = $aVPos;
        $this->iStart = $aDate;
    }
        
//---------------
// PUBLIC METHODS       
        
    function GetAbsHeight($aImg) {
        return max($this->title->GetHeight($aImg),$this->mark->GetWidth());
    }
                
    function Stroke($aImg,$aScale) {
        // Put the mark in the middle at the middle of the day
        $d = $aScale->NormalizeDate($this->iStart)+SECPERDAY/2;
        $x = $aScale->TranslateDate($d);
        $y = $aScale->TranslateVertPos($this->iVPos)-($aScale->GetVertSpacing()/2);
        $this->title->Stroke($aImg,$aImg->left_margin+$this->iLabelLeftMargin,$y);

        // CSIM for title
        if( $this->title->csimtarget != '' ) {
            $title_xt = $aImg->left_margin+$this->iLabelLeftMargin;
            $title_xb = $title_xt + $this->title->GetWidth($aImg);
            $yt = round($y - $this->title->GetHeight($aImg)/2);
            $yb = round($y + $this->title->GetHeight($aImg)/2);
            $coords = "$title_xt,$yt,$title_xb,$yt,$title_xb,$yb,$title_xt,$yb";
            $this->csimarea .= "<area shape=\"poly\" coords=\"$coords\" href=\"".$this->title->csimtarget."\"";
            if( $this->title->csimalt != '' ) {
                $tmp = $this->title->csimalt;
                $this->csimarea .= " alt=\"$tmp\" title=\"$tmp\"";
            }
            $this->csimarea .= ">\n";
        }


        if( $d <  $aScale->iStartDate || $d > $aScale->iEndDate )
                return;

        // Remember the coordinates for any constrains linking to
        // this milestone
        $w = $this->mark->GetWidth()/2;
        $this->SetConstrainPos($x,round($y-$w),$x,round($y+$w));
        
        // Setup CSIM
        if( $this->csimtarget != '' ) {
            $this->mark->SetCSIMTarget( $this->csimtarget );
            $this->mark->SetCSIMAlt( $this->csimalt );
        }
                
        $this->mark->Stroke($aImg,$x,$y);               
        $this->caption->Stroke($aImg,$x+$this->mark->width/2+$this->iCaptionMargin,$y);

        $this->csimarea .= $this->mark->GetCSIMAreas();
    }
}


//===================================================
// CLASS GanttVLine
// Responsible for formatting individual milestones
//===================================================

class GanttVLine extends GanttPlotObject {

    var $iLine,$title_margin=3;
    var $iDayOffset=1;  // Defult to right edge of day
        
//---------------
// CONSTRUCTOR  
    function GanttVLine($aDate,$aTitle="",$aColor="black",$aWeight=3,$aStyle="dashed") {
        GanttPlotObject::GanttPlotObject();
        $this->iLine = new LineProperty();
        $this->iLine->SetColor($aColor);
        $this->iLine->SetWeight($aWeight);
        $this->iLine->SetStyle($aStyle);
        $this->iStart = $aDate;
        $this->title->Set($aTitle);
    }

//---------------
// PUBLIC METHODS       

    function SetDayOffset($aOff=0.5) {
        if( $aOff < 0.0 || $aOff > 1.0 )
            JpGraphError::Raise("Offset for vertical line must be in range [0,1]");
        $this->iDayOffset = $aOff;
    }
        
    function SetTitleMargin($aMarg) {
        $this->title_margin = $aMarg;
    }
        
    function Stroke($aImg,$aScale) {
    $d = $aScale->NormalizeDate($this->iStart)+$this->iDayOffset*SECPERDAY;

        if( $d <  $aScale->iStartDate || $d > $aScale->iEndDate )
                return; 

        $x = $aScale->TranslateDate($d);        
        $y1 = $aScale->iVertHeaderSize+$aImg->top_margin;
        $y2 = $aImg->height - $aImg->bottom_margin;     
        $this->iLine->Stroke($aImg,$x,$y1,$x,$y2);
        $this->title->Align("center","top");
        $this->title->Stroke($aImg,$x,$y2+$this->title_margin);
    }   
}

//===================================================
// CLASS LinkArrow
// Handles the drawing of a an arrow 
//===================================================
class LinkArrow {
    var $ix,$iy;
    var $isizespec = array(
        array(2,3),array(3,5),array(3,8),array(6,15),array(8,22));
    var $iDirection=ARROW_DOWN,$iType=ARROWT_SOLID,$iSize=ARROW_S2;
    var $iColor='black';

    function LinkArrow($x,$y,$aDirection,$aType=ARROWT_SOLID,$aSize=ARROW_S2) {
        $this->iDirection = $aDirection;
        $this->iType = $aType;
        $this->iSize = $aSize;
        $this->ix = $x;
        $this->iy = $y;
    }
    
    function SetColor($aColor) {
        $this->iColor = $aColor;
    }

    function SetSize($aSize) {
        $this->iSize = $aSize;
    }

    function SetType($aType) {
        $this->iType = $aType;
    }

    function Stroke($aImg) {
        list($dx,$dy) = $this->isizespec[$this->iSize];
        $x = $this->ix;
        $y = $this->iy;
        switch ( $this->iDirection ) {
            case ARROW_DOWN:
                $c = array($x,$y,$x-$dx,$y-$dy,$x+$dx,$y-$dy,$x,$y);
                break;
            case ARROW_UP:
                $c = array($x,$y,$x-$dx,$y+$dy,$x+$dx,$y+$dy,$x,$y);
                break;
            case ARROW_LEFT:
                $c = array($x,$y,$x+$dy,$y-$dx,$x+$dy,$y+$dx,$x,$y);
                break;
            case ARROW_RIGHT:
                $c = array($x,$y,$x-$dy,$y-$dx,$x-$dy,$y+$dx,$x,$y);
                break;
            default:
                JpGraphError::Raise('Unknown arrow direction for link.');
                die();
                break;
        }
        $aImg->SetColor($this->iColor);
        switch( $this->iType ) {
            case ARROWT_SOLID:
                $aImg->FilledPolygon($c);
                break;
            case ARROWT_OPEN:
                $aImg->Polygon($c);
                break;
            default:
                JpGraphError::Raise('Unknown arrow type for link.');
                die();
                break;          
        }
    }
}

//===================================================
// CLASS GanttLink
// Handles the drawing of a link line between 2 points
//===================================================

class GanttLink {
    var $iArrowType='';
    var $ix1,$ix2,$iy1,$iy2;
    var $iPathType=2,$iPathExtend=15;
    var $iColor='black',$iWeight=1;
    var $iArrowSize=ARROW_S2,$iArrowType=ARROWT_SOLID;

    function GanttLink($x1=0,$y1=0,$x2=0,$y2=0) {
        $this->ix1 = $x1;
        $this->ix2 = $x2;
        $this->iy1 = $y1;
        $this->iy2 = $y2;
    }

    function SetPos($x1,$y1,$x2,$y2) {
        $this->ix1 = $x1;
        $this->ix2 = $x2;
        $this->iy1 = $y1;
        $this->iy2 = $y2;
    }

    function SetPath($aPath) {
        $this->iPathType = $aPath;
    }

    function SetColor($aColor) {
        $this->iColor = $aColor;
    }

    function SetArrow($aSize,$aType=ARROWT_SOLID) {
        $this->iArrowSize = $aSize;
        $this->iArrowType = $aType;
    }
    
    function SetWeight($aWeight) {
        $this->iWeight = $aWeight;
    }

    function Stroke($aImg) {
        // The way the path for the arrow is constructed is partly based
        // on some heuristics. This is not an exact science but draws the
        // path in a way that, for me, makes esthetic sence. For example
        // if the start and end activities are very close we make a small
        // detour to endter the target horixontally. If there are more
        // space between axctivities then no suh detour is made and the 
        // target is "hit" directly vertical. I have tried to keep this
        // simple. no doubt this could become almost infinitive complex
        // and have some real AI. Feel free to modify this.
        // This will no-doubt be tweaked as times go by. One design aim
        // is to avoid having the user choose what types of arrow
        // he wants.

        // The arrow is drawn between (x1,y1) to (x2,y2)
        $x1 = $this->ix1 ;
        $x2 = $this->ix2 ;
        $y1 = $this->iy1 ;
        $y2 = $this->iy2 ;

        // Depending on if the target is below or above we have to
        // handle thi different.
        if( $y2 > $y1 ) {
            $arrowtype = ARROW_DOWN;
            $midy = round(($y2-$y1)/2+$y1);
            if( $x2 > $x1 ) {
                switch ( $this->iPathType  ) {
                    case 0:
                        $c = array($x1,$y1,$x1,$midy,$x2,$midy,$x2,$y2);
                        break;
                    case 1:
                    case 2:
                    case 3:
                        $c = array($x1,$y1,$x2,$y1,$x2,$y2);
                        break;
                    default:
                        JpGraphError::Raise('Internal error: Unknown path type (='.$this->iPathType .') specified for link.');
                        exit(1);
                        break;
                }
            }
            else {
                switch ( $this->iPathType  ) {
                    case 0:
                    case 1:
                        $c = array($x1,$y1,$x1,$midy,$x2,$midy,$x2,$y2);
                        break;
                    case 2:
                        // Always extend out horizontally a bit from the first point
                        // If we draw a link back in time (end to start) and the bars 
                        // are very close we also change the path so it comes in from 
                        // the left on the activity
                        $c = array($x1,$y1,$x1+$this->iPathExtend,$y1,
                                   $x1+$this->iPathExtend,$midy,
                                   $x2,$midy,$x2,$y2);
                        break;
                    case 3:
                        if( $y2-$midy < 6 ) {
                            $c = array($x1,$y1,$x1,$midy,
                                       $x2-$this->iPathExtend,$midy,
                                       $x2-$this->iPathExtend,$y2,
                                       $x2,$y2);
                            $arrowtype = ARROW_RIGHT;
                        }
                        else {
                            $c = array($x1,$y1,$x1,$midy,$x2,$midy,$x2,$y2);
                        }
                        break;
                    default:
                        JpGraphError::Raise('Internal error: Unknown path type specified for link.');
                        exit(1);
                        break;
                }
            }
            $arrow = new LinkArrow($x2,$y2,$arrowtype);
        }
        else {
            // Y2 < Y1
            $arrowtype = ARROW_UP;
            $midy = round(($y1-$y2)/2+$y2);
            if( $x2 > $x1 ) {
                switch ( $this->iPathType  ) {
                    case 0:
                    case 1:
                        $c = array($x1,$y1,$x1,$midy,$x2,$midy,$x2,$y2);
                        break;
                    case 3:
                        if( $midy-$y2 < 8 ) {
                            $arrowtype = ARROW_RIGHT;
                            $c = array($x1,$y1,$x1,$y2,$x2,$y2);
                        }
                        else {
                            $c = array($x1,$y1,$x1,$midy,$x2,$midy,$x2,$y2);
                        }
                        break;
                    default:
                        JpGraphError::Raise('Internal error: Unknown path type specified for link.');
                        break;
                }
            }
            else {
                switch ( $this->iPathType  ) {
                    case 0:
                    case 1:
                        $c = array($x1,$y1,$x1,$midy,$x2,$midy,$x2,$y2);
                        break;
                    case 2:
                        // Always extend out horizontally a bit from the first point
                        $c = array($x1,$y1,$x1+$this->iPathExtend,$y1,
                                   $x1+$this->iPathExtend,$midy,
                                   $x2,$midy,$x2,$y2);
                        break;
                    case 3:
                        if( $midy-$y2 < 16 ) {
                            $arrowtype = ARROW_RIGHT;
                            $c = array($x1,$y1,$x1,$midy,$x2-$this->iPathExtend,$midy,
                                       $x2-$this->iPathExtend,$y2,
                                       $x2,$y2);
                        }
                        else {
                            $c = array($x1,$y1,$x1,$midy,$x2,$midy,$x2,$y2);
                        }
                        break;
                    default:
                        JpGraphError::Raise('Internal error: Unknown path type specified for link.');
                        exit(1);
                        break;
                }
            }
            $arrow = new LinkArrow($x2,$y2,$arrowtype);
        }
        $aImg->SetColor($this->iColor);
        $aImg->SetLineWeight($this->iWeight);
        $aImg->Polygon($c);
        $aImg->SetLineWeight(1);
        $arrow->SetColor($this->iColor);
        $arrow->SetSize($this->iArrowSize);
        $arrow->SetType($this->iArrowType);
        $arrow->Stroke($aImg);
    }
}

// <EOF>
?>