<?php
/**
 * File containing the ezcGraphGdDriver class
 *
 * @package Graph
 * @version 1.5
 * @copyright Copyright (C) 2005-2009 eZ Systems AS. All rights reserved.
 * @license http://ez.no/licenses/new_bsd New BSD License
 */
/**
 * Driver using PHPs ext/gd to draw images. The GD extension is available on 
 * nearly all PHP installations, but slow and produces slightly incorrect 
 * results.
 *
 * The driver can make use of the different font extensions available with 
 * ext/gd. It is possible to use Free Type 2, native TTF and PostScript Type 1 
 * fonts.
 *
 * The options of this driver are configured in {@link ezcGraphGdDriverOptions}
 * extending the basic driver options class {@link ezcGraphDriverOptions}.
 *
 * <code>
 *   $graph = new ezcGraphPieChart();
 *   $graph->palette = new ezcGraphPaletteEzGreen();
 *   $graph->title = 'Access statistics';
 *   $graph->legend = false;
 *   
 *   $graph->driver = new ezcGraphGdDriver();
 *   $graph->options->font = 'tutorial_font.ttf';
 *
 *   // Generate a Jpeg with lower quality. The default settings result in a image
 *   // with better quality.
 *   // 
 *   // The reduction of the supersampling to 1 will result in no anti aliasing of
 *   // the image. JPEG is not the optimal format for grapics, PNG is far better for
 *   // this kind of images.
 *   $graph->driver->options->supersampling = 1;
 *   $graph->driver->options->jpegQuality = 100;
 *   $graph->driver->options->imageFormat = IMG_JPEG;
 *   
 *   $graph->data['Access statistics'] = new ezcGraphArrayDataSet( array(
 *       'Mozilla' => 19113,
 *       'Explorer' => 10917,
 *       'Opera' => 1464,
 *       'Safari' => 652,
 *       'Konqueror' => 474,
 *   ) );
 *   
 *   $graph->render( 400, 200, 'tutorial_dirver_gd.jpg' );
 * </code>
 *
 * @version 1.5
 * @package Graph
 * @mainclass
 */
class ezcGraphGdDriver extends ezcGraphDriver
{

    /**
     * Image resource
     * 
     * @var resource
     */
    protected $image;

    /**
     * Array with image files to draw
     * 
     * @var array
     */
    protected $preProcessImages = array();

    /**
     * List of strings to draw
     * array ( array(
     *          'text' => array( 'strings' ),
     *          'options' => ezcGraphFontOptions,
     *      )
     * 
     * @var array
     */
    protected $strings = array();

    /**
     * Contains resources for already loaded ps fonts.
     *  array(
     *      path => resource
     *  )
     * 
     * @var array
     */
    protected $psFontResources = array();

    /**
     * Constructor
     * 
     * @param array $options Default option array
     * @return void
     * @ignore
     */
    public function __construct( array $options = array() )
    {
        ezcBase::checkDependency( 'Graph', ezcBase::DEP_PHP_EXTENSION, 'gd' );
        $this->options = new ezcGraphGdDriverOptions( $options );
    }

    /**
     * Returns the image resource to draw on.
     *
     * If no resource exists the image will be created. The size of the 
     * returned image depends on the supersampling factor and the size of the
     * chart.
     * 
     * @return resource
     */
    protected function getImage()
    {
        if ( !isset( $this->image ) )
        {
            $this->image = imagecreatetruecolor( 
                $this->supersample( $this->options->width ), 
                $this->supersample( $this->options->height )
            );

            // Default to a transparent white background
            $bgColor = imagecolorallocatealpha( $this->image, 255, 255, 255, 127 );
            imagealphablending( $this->image, true );
            imagesavealpha( $this->image, true );
            imagefill( $this->image, 1, 1, $bgColor );

            imagesetthickness( 
                $this->image, 
                $this->options->supersampling
            );
        }

        return $this->image;
    }

    /**
     * Allocates a color
     *
     * This function tries to allocate the requested color. If the color 
     * already exists in the imaga it will be reused.
     * 
     * @param ezcGraphColor $color 
     * @return int Color index
     */
    protected function allocate( ezcGraphColor $color )
    {
        $image = $this->getImage();

        if ( $color->alpha > 0 )
        {
            $fetched = imagecolorexactalpha( $image, $color->red, $color->green, $color->blue, $color->alpha / 2 );
            if ( $fetched < 0 )
            {
                $fetched = imagecolorallocatealpha( $image, $color->red, $color->green, $color->blue, $color->alpha / 2 );
            }
            return $fetched;
        }
        else
        {
            $fetched = imagecolorexact( $image, $color->red, $color->green, $color->blue );
            if ( $fetched < 0 )
            {
                $fetched = imagecolorallocate( $image, $color->red, $color->green, $color->blue );
            }
            return $fetched;
        }
    }

    /**
     * Creates an image resource from an image file
     *
     * @param string $file Filename
     * @return resource Image
     */
    protected function imageCreateFrom( $file )
    {
        $data = getimagesize( $file );

        switch ( $data[2] )
        {
            case 1:
                return array(
                    'width' => $data[0],
                    'height' => $data[1],
                    'image' => imagecreatefromgif( $file )
                );
            case 2:
                return array(
                    'width' => $data[0],
                    'height' => $data[1],
                    'image' => imagecreatefromjpeg( $file )
                );
            case 3:
                return array(
                    'width' => $data[0],
                    'height' => $data[1],
                    'image' => imagecreatefrompng( $file )
                );
            default:
                throw new ezcGraphGdDriverUnsupportedImageTypeException( $data[2] );
        }
    }

    /**
     * Supersamples a single coordinate value.
     *
     * Applies supersampling to a single coordinate value.
     * 
     * @param float $value Coordinate value
     * @return float Supersampled coordinate value
     */
    protected function supersample( $value )
    {
        $mod = (int) floor( $this->options->supersampling / 2 );
        return $value * $this->options->supersampling - $mod;
    }

    /**
     * Draws a single polygon. 
     * 
     * @param array $points Point array
     * @param ezcGraphColor $color Polygon color
     * @param mixed $filled Filled
     * @param float $thickness Line thickness
     * @return void
     */
    public function drawPolygon( array $points, ezcGraphColor $color, $filled = true, $thickness = 1. )
    {
        $image = $this->getImage();

        $drawColor = $this->allocate( $color );

        // Create point array
        $pointCount = count( $points );
        $pointArray = array();
        for ( $i = 0; $i < $pointCount; ++$i )
        {
            $pointArray[] = $this->supersample( $points[$i]->x );
            $pointArray[] = $this->supersample( $points[$i]->y );
        }

        // Draw polygon
        if ( $filled )
        {
            imagefilledpolygon( $image, $pointArray, $pointCount, $drawColor );
        }
        else
        {
            imagepolygon( $image, $pointArray, $pointCount, $drawColor );
        }

        return $points;
    }
    
    /**
     * Draws a line 
     * 
     * @param ezcGraphCoordinate $start Start point
     * @param ezcGraphCoordinate $end End point
     * @param ezcGraphColor $color Line color
     * @param float $thickness Line thickness
     * @return void
     */
    public function drawLine( ezcGraphCoordinate $start, ezcGraphCoordinate $end, ezcGraphColor $color, $thickness = 1. )
    {
        $image = $this->getImage();

        $drawColor = $this->allocate( $color );

        imagesetthickness( 
            $this->image, 
            $this->options->supersampling * $thickness
        );

        imageline( 
            $image, 
            $this->supersample( $start->x ), 
            $this->supersample( $start->y ), 
            $this->supersample( $end->x ), 
            $this->supersample( $end->y ), 
            $drawColor
        );

        imagesetthickness( 
            $this->image, 
            $this->options->supersampling
        );

        return array();
    }
    
    /**
     * Returns boundings of text depending on the available font extension
     * 
     * @param float $size Textsize
     * @param ezcGraphFontOptions $font Font
     * @param string $text Text
     * @return ezcGraphBoundings Boundings of text
     */
    protected function getTextBoundings( $size, ezcGraphFontOptions $font, $text )
    {
        switch ( $font->type )
        {
            case ezcGraph::PS_FONT:
                if ( !isset( $this->psFontResources[$font->path] ) )
                {
                    $this->psFontResources[$font->path] = imagePsLoadFont( $font->path );
                }

                $boundings = imagePsBBox( $text, $this->psFontResources[$font->path], $size );
                return new ezcGraphBoundings(
                    $boundings[0],
                    $boundings[1],
                    $boundings[2],
                    $boundings[3]
                );
            case ezcGraph::TTF_FONT:
                switch ( true )
                {
                    case ezcBaseFeatures::hasFunction( 'imageftbbox' ) && !$this->options->forceNativeTTF:
                        $boundings = imageFtBBox( $size, 0, $font->path, $text );
                        return new ezcGraphBoundings(
                            $boundings[0],
                            $boundings[1],
                            $boundings[4],
                            $boundings[5]
                        );
                    case ezcBaseFeatures::hasFunction( 'imagettfbbox' ):
                        $boundings = imageTtfBBox( $size, 0, $font->path, $text );
                        return new ezcGraphBoundings(
                            $boundings[0],
                            $boundings[1],
                            $boundings[4],
                            $boundings[5]
                        );
                }
                break;
        }
    }

    /**
     * Render text depending of font type and available font extensions
     * 
     * @param resource $image Image resource
     * @param string $text Text
     * @param int $type Font type
     * @param string $path Font path
     * @param ezcGraphColor $color Font color
     * @param ezcGraphCoordinate $position Position
     * @param float $size Textsize
     * @param ezcGraphRotation $rotation
     *
     * @return void
     */
    protected function renderText( $image, $text, $type, $path, ezcGraphColor $color, ezcGraphCoordinate $position, $size, ezcGraphRotation $rotation = null )
    {
        if ( $rotation !== null )
        {
            // Rotation is relative to top left point of text and not relative
            // to the bounding coordinate system
            $rotation = new ezcGraphRotation(
                $rotation->getRotation(),
                new ezcGraphCoordinate(
                    $rotation->getCenter()->x - $position->x,
                    $rotation->getCenter()->y - $position->y
                )
            );
        }

        switch ( $type )
        {
            case ezcGraph::PS_FONT:
                imagePsText( 
                    $image, 
                    $text, 
                    $this->psFontResources[$path], 
                    $size, 
                    $this->allocate( $color ), 
                    1, 
                    $position->x + 
                        ( $rotation === null ? 0 : $rotation->get( 0, 2 ) ),
                    $position->y + 
                        ( $rotation === null ? 0 : $rotation->get( 1, 2 ) ),
                    0,
                    0,
                    ( $rotation === null ? 0 : -$rotation->getRotation() ),
                    4
                );
                break;
            case ezcGraph::TTF_FONT:
                switch ( true )
                {
                    case ezcBaseFeatures::hasFunction( 'imagefttext' ) && !$this->options->forceNativeTTF:
                        imageFtText(
                            $image, 
                            $size,
                            ( $rotation === null ? 0 : -$rotation->getRotation() ),
                            $position->x + 
                                ( $rotation === null ? 0 : $rotation->get( 0, 2 ) ),
                            $position->y + 
                                ( $rotation === null ? 0 : $rotation->get( 1, 2 ) ),
                            $this->allocate( $color ),
                            $path,
                            $text
                        );
                        break;
                    case ezcBaseFeatures::hasFunction( 'imagettftext' ):
                        imageTtfText(
                            $image, 
                            $size,
                            ( $rotation === null ? 0 : -$rotation->getRotation() ),
                            $position->x + 
                                ( $rotation === null ? 0 : $rotation->get( 0, 2 ) ),
                            $position->y + 
                                ( $rotation === null ? 0 : $rotation->get( 1, 2 ) ),
                            $this->allocate( $color ),
                            $path,
                            $text
                        );
                        break;
                }
                break;
        }
    }

    /**
     * Writes text in a box of desired size
     * 
     * @param string $string Text
     * @param ezcGraphCoordinate $position Top left position
     * @param float $width Width of text box
     * @param float $height Height of text box
     * @param int $align Alignement of text
     * @param ezcGraphRotation $rotation
     * @return void
     */
    public function drawTextBox( $string, ezcGraphCoordinate $position, $width, $height, $align, ezcGraphRotation $rotation = null )
    {
        $padding = $this->options->font->padding + ( $this->options->font->border !== false ? $this->options->font->borderWidth : 0 );

        $width -= $padding * 2;
        $height -= $padding * 2;
        $position->x += $padding;
        $position->y += $padding;

        // Try to get a font size for the text to fit into the box
        $maxSize = min( $height, $this->options->font->maxFontSize );
        $result = false;
        for ( $size = $maxSize; $size >= $this->options->font->minFontSize; --$size )
        {
            $result = $this->testFitStringInTextBox( $string, $position, $width, $height, $size );
            if ( is_array( $result ) )
            {
                break;
            }
            $size = floor( ( $newsize = $size * ( $result ) ) >= $size ? $size - 1 : $newsize );
        }

        if ( !is_array( $result ) )
        {
            if ( ( $height >= $this->options->font->minFontSize ) &&
                 ( $this->options->autoShortenString ) )
            {
                $result = $this->tryFitShortenedString( $string, $position, $width, $height, $size = $this->options->font->minFontSize );
            } 
            else
            {
                throw new ezcGraphFontRenderingException( $string, $this->options->font->minFontSize, $width, $height );
            }
        }

        $this->options->font->minimalUsedFont = $size;

        $this->strings[] = array(
            'text' => $result,
            'position' => $position,
            'width' => $width,
            'height' => $height,
            'align' => $align,
            'font' => $this->options->font,
            'rotation' => $rotation,
        );

        return array(
            clone $position,
            new ezcGraphCoordinate( $position->x + $width, $position->y ),
            new ezcGraphCoordinate( $position->x + $width, $position->y + $height ),
            new ezcGraphCoordinate( $position->x, $position->y + $height ),
        );
    }
    
    /**
     * Draw all collected texts
     *
     * The texts are collected and their maximum possible font size is 
     * calculated. This function finally draws the texts on the image, this
     * delayed drawing has two reasons:
     *
     * 1) This way the text strings are always on top of the image, what 
     *    results in better readable texts
     * 2) The maximum possible font size can be calculated for a set of texts
     *    with the same font configuration. Strings belonging to one chart 
     *    element normally have the same font configuration, so that all texts
     *    belonging to one element will have the same font size.
     * 
     * @access protected
     * @return void
     */
    protected function drawAllTexts()
    {
        $image = $this->getImage();

        foreach ( $this->strings as $text )
        {
            $size = $text['font']->minimalUsedFont;

            $completeHeight = count( $text['text'] ) * $size + ( count( $text['text'] ) - 1 ) * $this->options->lineSpacing;

            // Calculate y offset for vertical alignement
            switch ( true )
            {
                case ( $text['align'] & ezcGraph::BOTTOM ):
                    $yOffset = $text['height'] - $completeHeight;
                    break;
                case ( $text['align'] & ezcGraph::MIDDLE ):
                    $yOffset = ( $text['height'] - $completeHeight ) / 2;
                    break;
                case ( $text['align'] & ezcGraph::TOP ):
                default:
                    $yOffset = 0;
                    break;
            }

            $padding = $text['font']->padding + $text['font']->borderWidth / 2;
            if ( $this->options->font->minimizeBorder === true )
            {
                // Calculate maximum width of text rows
                $width = false;
                foreach ( $text['text'] as $line )
                {
                    $string = implode( ' ', $line );
                    $boundings = $this->getTextBoundings( $size, $text['font'], $string );
                    if ( ( $width === false) || ( $boundings->width > $width ) )
                    {
                        $width = $boundings->width;
                    }
                }

                switch ( true )
                {
                    case ( $text['align'] & ezcGraph::LEFT ):
                        $xOffset = 0;
                        break;
                    case ( $text['align'] & ezcGraph::CENTER ):
                        $xOffset = ( $text['width'] - $width ) / 2;
                        break;
                    case ( $text['align'] & ezcGraph::RIGHT ):
                        $xOffset = $text['width'] - $width;
                        break;
                }

                $borderPolygonArray = array(
                    new ezcGraphCoordinate(
                        $text['position']->x - $padding + $xOffset,
                        $text['position']->y - $padding + $yOffset
                    ),
                    new ezcGraphCoordinate(
                        $text['position']->x + $padding * 2 + $xOffset + $width,
                        $text['position']->y - $padding + $yOffset
                    ),
                    new ezcGraphCoordinate(
                        $text['position']->x + $padding * 2 + $xOffset + $width,
                        $text['position']->y + $padding * 2 + $yOffset + $completeHeight
                    ),
                    new ezcGraphCoordinate(
                        $text['position']->x - $padding + $xOffset,
                        $text['position']->y + $padding * 2 + $yOffset + $completeHeight
                    ),
                );
            }
            else
            {
                $borderPolygonArray = array(
                    new ezcGraphCoordinate(
                        $text['position']->x - $padding,
                        $text['position']->y - $padding
                    ),
                    new ezcGraphCoordinate(
                        $text['position']->x + $padding * 2 + $text['width'],
                        $text['position']->y - $padding
                    ),
                    new ezcGraphCoordinate(
                        $text['position']->x + $padding * 2 + $text['width'],
                        $text['position']->y + $padding * 2 + $text['height']
                    ),
                    new ezcGraphCoordinate(
                        $text['position']->x - $padding,
                        $text['position']->y + $padding * 2 + $text['height']
                    ),
                );
            }

            if ( $text['rotation'] !==  null )
            {
                foreach ( $borderPolygonArray as $nr => $point )
                {
                    $borderPolygonArray[$nr] = $text['rotation']->transformCoordinate( $point );
                }
            }

            if ( $text['font']->background !== false )
            {
                $this->drawPolygon( 
                    $borderPolygonArray, 
                    $text['font']->background,
                    true
                );
            }

            if ( $text['font']->border !== false )
            {
                $this->drawPolygon( 
                    $borderPolygonArray, 
                    $text['font']->border,
                    false,
                    $text['font']->borderWidth
                );
            }

            // Render text with evaluated font size
            foreach ( $text['text'] as $line )
            {
                $string = implode( ' ', $line );
                $boundings = $this->getTextBoundings( $size, $text['font'], $string );
                $text['position']->y += $size;

                switch ( true )
                {
                    case ( $text['align'] & ezcGraph::LEFT ):
                        $position = new ezcGraphCoordinate( 
                            $text['position']->x, 
                            $text['position']->y + $yOffset
                        );
                        break;
                    case ( $text['align'] & ezcGraph::RIGHT ):
                        $position = new ezcGraphCoordinate( 
                            $text['position']->x + ( $text['width'] - $boundings->width ), 
                            $text['position']->y + $yOffset
                        );
                        break;
                    case ( $text['align'] & ezcGraph::CENTER ):
                        $position = new ezcGraphCoordinate( 
                            $text['position']->x + ( ( $text['width'] - $boundings->width ) / 2 ), 
                            $text['position']->y + $yOffset
                        );
                        break;
                }

                // Calculate relative modification of rotation center point
                if ( $text['rotation'] !== null )
                {
                    $rotation = new ezcGraphRotation(
                        $text['rotation']->getRotation(),
                        new ezcGraphCoordinate(
                            $text['rotation']->getCenter()->x + 
                                $position->x - $text['position']->x,
                            $text['rotation']->getCenter()->y + 
                                $position->y - $text['position']->y
                        )
                    );
                    $rotation = $text['rotation'];
                }
                else
                {
                    $rotation = null;
                }

                // Optionally draw text shadow
                if ( $text['font']->textShadow === true )
                {
                    $this->renderText( 
                        $image, 
                        $string,
                        $text['font']->type, 
                        $text['font']->path, 
                        $text['font']->textShadowColor,
                        new ezcGraphCoordinate(
                            $position->x + $text['font']->textShadowOffset,
                            $position->y + $text['font']->textShadowOffset
                        ),
                        $size,
                        $rotation
                    );
                }
                
                // Finally draw text
                $this->renderText( 
                    $image, 
                    $string,
                    $text['font']->type, 
                    $text['font']->path, 
                    $text['font']->color, 
                    $position,
                    $size,
                    $rotation
                );

                $text['position']->y += $size * $this->options->lineSpacing;
            }
        }
    }

    /**
     * Draws a sector of cirlce
     * 
     * @param ezcGraphCoordinate $center Center of circle
     * @param mixed $width Width
     * @param mixed $height Height
     * @param mixed $startAngle Start angle of circle sector
     * @param mixed $endAngle End angle of circle sector
     * @param ezcGraphColor $color Color
     * @param mixed $filled Filled
     * @return void
     */
    public function drawCircleSector( ezcGraphCoordinate $center, $width, $height, $startAngle, $endAngle, ezcGraphColor $color, $filled = true )
    {
        $image = $this->getImage();
        $drawColor = $this->allocate( $color );

        // Normalize angles
        if ( $startAngle > $endAngle )
        {
            $tmp = $startAngle;
            $startAngle = $endAngle;
            $endAngle = $tmp;
        }

        if ( ( $endAngle - $startAngle ) > 359.99999 )
        {
            return $this->drawCircle( $center, $width, $height, $color, $filled );
        }

        // Because of bug #45552 in PHPs ext/GD we check for a minimal distance
        // on the outer border of the circle sector, and skip the drawing if
        // the distance is lower then 1.
        //
        // See also: http://bugs.php.net/45552
        $startPoint = new ezcGraphVector( 
            $center->x + 
                ( ( cos( deg2rad( $startAngle ) ) * $width ) / 2 ),
            $center->y + 
                ( ( sin( deg2rad( $startAngle ) ) * $height ) / 2 )
        );
        if ( $startPoint->sub( new ezcGraphVector( 
                $center->x + 
                    ( ( cos( deg2rad( $endAngle ) ) * $width ) / 2 ),
                $center->y + 
                    ( ( sin( deg2rad( $endAngle ) ) * $height ) / 2 )
             ) )->length() < 1 )
        {
            // Skip this circle sector
            return array();
        }

        if ( $filled )
        {
            imagefilledarc( 
                $image, 
                $this->supersample( $center->x ), 
                $this->supersample( $center->y ), 
                $this->supersample( $width ), 
                $this->supersample( $height ), 
                $startAngle, 
                $endAngle, 
                $drawColor, 
                IMG_ARC_PIE 
            );
        }
        else
        {
            imagefilledarc( 
                $image, 
                $this->supersample( $center->x ), 
                $this->supersample( $center->y ), 
                $this->supersample( $width ), 
                $this->supersample( $height ), 
                $startAngle, 
                $endAngle, 
                $drawColor, 
                IMG_ARC_PIE | IMG_ARC_NOFILL | IMG_ARC_EDGED
            );
        }

        // Create polygon array to return
        $polygonArray = array( $center );
        for ( $angle = $startAngle; $angle < $endAngle; $angle += $this->options->imageMapResolution )
        {
            $polygonArray[] = new ezcGraphCoordinate(
                $center->x + 
                    ( ( cos( deg2rad( $angle ) ) * $width ) / 2 ),
                $center->y + 
                    ( ( sin( deg2rad( $angle ) ) * $height ) / 2 )
            );
        }
        $polygonArray[] = new ezcGraphCoordinate(
            $center->x + 
                ( ( cos( deg2rad( $endAngle ) ) * $width ) / 2 ),
            $center->y + 
                ( ( sin( deg2rad( $endAngle ) ) * $height ) / 2 )
        );

        return $polygonArray;
    }

    /**
     * Draws a single element of a circular arc
     * 
     * ext/gd itself does not support something like circular arcs, so that
     * this functions draws rectangular polygons as a part of circular arcs
     * to interpolate them. This way it is possible to apply a linear gradient
     * to the circular arc, because we draw single steps anyway.
     *
     * @param ezcGraphCoordinate $center Center of ellipse
     * @param integer $width Width of ellipse
     * @param integer $height Height of ellipse
     * @param integer $size Height of border
     * @param float $startAngle Starting angle of circle sector
     * @param float $endAngle Ending angle of circle sector
     * @param ezcGraphColor $color Color of Border
     * @return void
     */
    protected function drawCircularArcStep( ezcGraphCoordinate $center, $width, $height, $size, $startAngle, $endAngle, ezcGraphColor $color )
    {
        $this->drawPolygon(
            array(
                new ezcGraphCoordinate(
                    $center->x + 
                        ( ( cos( deg2rad( $startAngle ) ) * $width ) / 2 ),
                    $center->y + 
                        ( ( sin( deg2rad( $startAngle ) ) * $height ) / 2 )
                ),
                new ezcGraphCoordinate(
                    $center->x + 
                        ( ( cos( deg2rad( $startAngle ) ) * $width ) / 2 ),
                    $center->y + 
                        ( ( sin( deg2rad( $startAngle ) ) * $height ) / 2 ) + $size
                ),
                new ezcGraphCoordinate(
                    $center->x + 
                        ( ( cos( deg2rad( $endAngle ) ) * $width ) / 2 ),
                    $center->y + 
                        ( ( sin( deg2rad( $endAngle ) ) * $height ) / 2 ) + $size
                ),
                new ezcGraphCoordinate(
                    $center->x + 
                        ( ( cos( deg2rad( $endAngle ) ) * $width ) / 2 ),
                    $center->y + 
                        ( ( sin( deg2rad( $endAngle ) ) * $height ) / 2 )
                ),
            ),
            $color->darken( $this->options->shadeCircularArc * ( 1 + cos ( deg2rad( $startAngle ) ) ) / 2 ),
            true
        );
    }
 
    /**
     * Draws a circular arc
     * 
     * @param ezcGraphCoordinate $center Center of ellipse
     * @param integer $width Width of ellipse
     * @param integer $height Height of ellipse
     * @param integer $size Height of border
     * @param float $startAngle Starting angle of circle sector
     * @param float $endAngle Ending angle of circle sector
     * @param ezcGraphColor $color Color of Border
     * @param bool $filled
     * @return void
     */
    public function drawCircularArc( ezcGraphCoordinate $center, $width, $height, $size, $startAngle, $endAngle, ezcGraphColor $color, $filled = true )
    {
        $image = $this->getImage();
        $drawColor = $this->allocate( $color );

        // Normalize angles
        if ( $startAngle > $endAngle )
        {
            $tmp = $startAngle;
            $startAngle = $endAngle;
            $endAngle = $tmp;
        }
 
        if ( $filled === true )
        {
            $startIteration = ceil( $startAngle / $this->options->detail ) * $this->options->detail;
            $endIteration = floor( $endAngle / $this->options->detail ) * $this->options->detail;

            if ( $startAngle < $startIteration )
            {
                // Draw initial step
                $this->drawCircularArcStep( 
                    $center, 
                    $width, 
                    $height, 
                    $size, 
                    $startAngle, 
                    $startIteration, 
                    $color
                );
            }

            // Draw all steps
            for ( ; $startIteration < $endIteration; $startIteration += $this->options->detail )
            {
                $this->drawCircularArcStep( 
                    $center, 
                    $width, 
                    $height, 
                    $size, 
                    $startIteration, 
                    $startIteration + $this->options->detail, 
                    $color 
                );
            }

            if ( $endIteration < $endAngle )
            {
                // Draw closing step
                $this->drawCircularArcStep( 
                    $center, 
                    $width, 
                    $height, 
                    $size, 
                    $endIteration, 
                    $endAngle, 
                    $color 
                );
            }
        }
        else
        {
            imagefilledarc( 
                $image, 
                $this->supersample( $center->x ), 
                $this->supersample( $center->y ), 
                $this->supersample( $width ), 
                $this->supersample( $height ), 
                $startAngle, 
                $endAngle, 
                $drawColor, 
                IMG_ARC_PIE | IMG_ARC_NOFILL
            );
        }

        // Create polygon array to return
        $polygonArray = array();
        for ( $angle = $startAngle; $angle < $endAngle; $angle += $this->options->imageMapResolution )
        {
            $polygonArray[] = new ezcGraphCoordinate(
                $center->x + 
                    ( ( cos( deg2rad( $angle ) ) * $width ) / 2 ),
                $center->y + 
                    ( ( sin( deg2rad( $angle ) ) * $height ) / 2 )
            );
        }
        $polygonArray[] = new ezcGraphCoordinate(
            $center->x + 
                ( ( cos( deg2rad( $endAngle ) ) * $width ) / 2 ),
            $center->y + 
                ( ( sin( deg2rad( $endAngle ) ) * $height ) / 2 )
        );

        for ( $angle = $endAngle; $angle > $startAngle; $angle -= $this->options->imageMapResolution )
        {
            $polygonArray[] = new ezcGraphCoordinate(
                $center->x + 
                    ( ( cos( deg2rad( $angle ) ) * $width ) / 2 ) + $size,
                $center->y + 
                    ( ( sin( deg2rad( $angle ) ) * $height ) / 2 )
            );
        }
        $polygonArray[] = new ezcGraphCoordinate(
            $center->x + 
                ( ( cos( deg2rad( $startAngle ) ) * $width ) / 2 ) + $size,
            $center->y + 
                ( ( sin( deg2rad( $startAngle ) ) * $height ) / 2 )
        );

        return $polygonArray;
    }
    
    /**
     * Draw circle 
     * 
     * @param ezcGraphCoordinate $center Center of ellipse
     * @param mixed $width Width of ellipse
     * @param mixed $height height of ellipse
     * @param ezcGraphColor $color Color
     * @param mixed $filled Filled
     * @return void
     */
    public function drawCircle( ezcGraphCoordinate $center, $width, $height, ezcGraphColor $color, $filled = true )
    {
        $image = $this->getImage();

        $drawColor = $this->allocate( $color );

        if ( $filled )
        {
            imagefilledellipse( 
                $image, 
                $this->supersample( $center->x ), 
                $this->supersample( $center->y ), 
                $this->supersample( $width ), 
                $this->supersample( $height ), 
                $drawColor 
            );
        }
        else
        {
            imageellipse( 
                $image, 
                $this->supersample( $center->x ), 
                $this->supersample( $center->y ), 
                $this->supersample( $width ), 
                $this->supersample( $height ), 
                $drawColor 
            );
        }

        $polygonArray = array();
        for ( $angle = 0; $angle < 360; $angle += $this->options->imageMapResolution )
        {
            $polygonArray[] = new ezcGraphCoordinate(
                $center->x + 
                    ( ( cos( deg2rad( $angle ) ) * $width ) / 2 ),
                $center->y + 
                    ( ( sin( deg2rad( $angle ) ) * $height ) / 2 )
            );
        }

        return $polygonArray;
    }
    
    /**
     * Draw an image
     *
     * The actual drawing of the image is delayed, to not apply supersampling 
     * to the image. The image will normally be resized using the gd function
     * imagecopyresampled, which provides nice antialiased scaling, so that
     * additional supersampling would make the image look blurred. The delayed
     * images will be pre-processed, so that they are draw in the back of 
     * everything else.
     * 
     * @param mixed $file Image file
     * @param ezcGraphCoordinate $position Top left position
     * @param mixed $width Width of image in destination image
     * @param mixed $height Height of image in destination image
     * @return void
     */
    public function drawImage( $file, ezcGraphCoordinate $position, $width, $height )
    {
        $this->preProcessImages[] = array(
            'file' => $file, 
            'position' => clone $position,
            'width' => $width,
            'height' => $height,
        );

        return array(
            $position,
            new ezcGraphCoordinate( $position->x + $width, $position->y ),
            new ezcGraphCoordinate( $position->x + $width, $position->y + $height ),
            new ezcGraphCoordinate( $position->x, $position->y + $height ),
        );
    }

    /**
     * Draw all images to image resource handler
     * 
     * @param resource $image Image to draw on
     * @return resource Updated image resource
     */
    protected function addImages( $image )
    {
        foreach ( $this->preProcessImages as $preImage )
        {
            $preImageData = $this->imageCreateFrom( $preImage['file'] );
            call_user_func_array(
                $this->options->resampleFunction,
                array(
                    $image,
                    $preImageData['image'],
                    $preImage['position']->x, $preImage['position']->y,
                    0, 0,
                    $preImage['width'], $preImage['height'],
                    $preImageData['width'], $preImageData['height'],
                )
            );
        }

        return $image;
    }

    /**
     * Return mime type for current image format
     * 
     * @return string
     */
    public function getMimeType()
    {
        switch ( $this->options->imageFormat )
        {
            case IMG_PNG:
                return 'image/png';
            case IMG_JPEG:
                return 'image/jpeg';
        }
    }

    /**
     * Render image directly to output
     *
     * The method renders the image directly to the standard output. You 
     * normally do not want to use this function, because it makes it harder 
     * to proper cache the generated graphs.
     * 
     * @return void
     */
    public function renderToOutput()
    {
        header( 'Content-Type: ' . $this->getMimeType() );
        $this->render( null );
    }

    /**
     * Finally save image
     * 
     * @param string $file Destination filename
     * @return void
     */
    public function render( $file )
    {
        $destination = imagecreatetruecolor( $this->options->width, $this->options->height );

        // Default to a transparent white background
        $bgColor = imagecolorallocatealpha( $destination, 255, 255, 255, 127 );
        imagealphablending( $destination, true );
        imagesavealpha( $destination, true );
        imagefill( $destination, 1, 1, $bgColor );

        // Apply background if one is defined
        if ( $this->options->background !== false )
        {
            $background = $this->imageCreateFrom( $this->options->background );

            call_user_func_array(
                $this->options->resampleFunction,
                array(
                    $destination,
                    $background['image'],
                    0, 0,
                    0, 0,
                    $this->options->width, $this->options->height,
                    $background['width'], $background['height'],
                )
            );
        }

        // Draw all images to exclude them from supersampling
        $destination = $this->addImages( $destination );

        // Finally merge with graph
        $image = $this->getImage();
        call_user_func_array(
            $this->options->resampleFunction,
            array(
                $destination,
                $image,
                0, 0,
                0, 0,
                $this->options->width, $this->options->height,
                $this->supersample( $this->options->width ), $this->supersample( $this->options->height )
            )
        );

        $this->image = $destination;
        imagedestroy( $image );

        // Draw all texts
        // Reset supersampling during text rendering
        $supersampling = $this->options->supersampling;
        $this->options->supersampling = 1;
        $this->drawAllTexts();
        $this->options->supersampling = $supersampling;

        $image = $this->getImage();
        switch ( $this->options->imageFormat )
        {
            case IMG_PNG:
                if ( $file === null )
                {
                    imagepng( $image );
                }
                else
                {
                    imagepng( $image, $file );
                }
                break;
            case IMG_JPEG:
                imagejpeg( $image, $file, $this->options->jpegQuality );
                break;
            default:
                throw new ezcGraphGdDriverUnsupportedImageTypeException( $this->options->imageFormat );
        }
    }

    /**
     * Get resource of rendered result
     *
     * Return the resource of the rendered result. You should not use this
     * method before you called either renderToOutput() or render(), as the
     * image may not be completely rendered until then.
     * 
     * @return resource
     */
    public function getResource()
    {
        return $this->image;
    }
}

?>
