blob: cdac6ef1bd01338463ab7e9e705c90573a51f9fb [file] [log] [blame]
<?php
/**
* File containing the abstract ezcGraphDriver class
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
* @package Graph
* @version //autogentag//
* @copyright Copyright (C) 2005-2010 eZ Systems AS. All rights reserved.
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
*/
/**
* Abstract class to be extended for ezcGraph output drivers.
*
* @version //autogentag//
* @package Graph
*/
abstract class ezcGraphDriver
{
/**
* Driveroptions
*
* @var ezcDriverOptions
*/
protected $options;
/**
* Constructor
*
* @param array $options Default option array
* @return void
* @ignore
*/
abstract public function __construct( array $options = array() );
/**
* Options write access
*
* @throws ezcBasePropertyNotFoundException
* If Option could not be found
* @throws ezcBaseValueException
* If value is out of range
* @param mixed $propertyName Option name
* @param mixed $propertyValue Option value;
* @return mixed
* @ignore
*/
public function __set( $propertyName, $propertyValue )
{
switch ( $propertyName ) {
case 'options':
if ( $propertyValue instanceof ezcGraphDriverOptions )
{
$this->options = $propertyValue;
}
else
{
throw new ezcBaseValueException( "options", $propertyValue, "instanceof ezcGraphOptions" );
}
break;
default:
throw new ezcBasePropertyNotFoundException( $propertyName );
break;
}
}
/**
* __get
*
* @param mixed $propertyName
* @throws ezcBasePropertyNotFoundException
* If a the value for the property options is not an instance of
* @return mixed
* @ignore
*/
public function __get( $propertyName )
{
switch ( $propertyName )
{
case 'options':
return $this->options;
default:
throw new ezcBasePropertyNotFoundException( $propertyName );
}
}
/**
* Reduces the size of a polygon
*
* The method takes a polygon defined by a list of points and reduces its
* size by moving all lines to the middle by the given $size value.
*
* The detection of the inner side of the polygon depends on the angle at
* each edge point. This method will always work for 3 edged polygones,
* because the smaller angle will always be on the inner side. For
* polygons with more then 3 edges this method may fail. For ezcGraph this
* is a valid simplification, because we do not have any polygones which
* have an inner angle >= 180 degrees.
*
* @param array(ezcGraphCoordinate) $points
* @param float $size
* @throws ezcGraphReducementFailedException
* @return array( ezcGraphCoordinate )
*/
protected function reducePolygonSize( array $points, $size )
{
$pointCount = count( $points );
// Build normalized vectors between polygon edge points
$vectors = array();
$vectorLength = array();
for ( $i = 0; $i < $pointCount; ++$i )
{
$nextPoint = ( $i + 1 ) % $pointCount;
$vectors[$i] = ezcGraphVector::fromCoordinate( $points[$nextPoint] )
->sub( $points[$i] );
// Throw exception if polygon is too small to reduce
$vectorLength[$i] = $vectors[$i]->length();
if ( $vectorLength[$i] < $size )
{
throw new ezcGraphReducementFailedException();
}
$vectors[$i]->unify();
// Remove point from list if it the same as the next point
if ( ( $vectors[$i]->x == $vectors[$i]->y ) && ( $vectors[$i]->x == 0 ) )
{
$pointCount--;
if ( $i === 0 )
{
$points = array_slice( $points, $i + 1 );
}
else
{
$points = array_merge(
array_slice( $points, 0, $i ),
array_slice( $points, $i + 1 )
);
}
$i--;
}
}
// Remove vectors and appendant point, if local angle equals zero
// dergrees.
for ( $i = 0; $i < $pointCount; ++$i )
{
$nextPoint = ( $i + 1 ) % $pointCount;
if ( ( abs( $vectors[$i]->x - $vectors[$nextPoint]->x ) < .0001 ) &&
( abs( $vectors[$i]->y - $vectors[$nextPoint]->y ) < .0001 ) )
{
$pointCount--;
$points = array_merge(
array_slice( $points, 0, $i + 1 ),
array_slice( $points, $i + 2 )
);
$vectors = array_merge(
array_slice( $vectors, 0, $i + 1 ),
array_slice( $vectors, $i + 2 )
);
$i--;
}
}
// No reducements for lines
if ( $pointCount <= 2 )
{
return $points;
}
// Determine one of the angles - we need to know where the smaller
// angle is, to determine if the inner side of the polygon is on
// the left or right hand.
//
// This is a valid simplification for ezcGraph(, for now).
//
// The sign of the scalar products results indicates on which site
// the smaller angle is, when comparing the orthogonale vector of
// one of the vectors with the other. Why? .. use pen and paper ..
//
// It is sufficant to do this once before iterating over the points,
// because the inner side of the polygon is on the same side of the
// point for each point.
$last = 0;
$next = 1;
$sign = (
-$vectors[$last]->y * $vectors[$next]->x +
$vectors[$last]->x * $vectors[$next]->y
) < 0 ? 1 : -1;
// Move points to center
$newPoints = array();
for ( $i = 0; $i < $pointCount; ++$i )
{
$last = $i;
$next = ( $i + 1 ) % $pointCount;
// Orthogonal vector with direction based on the side of the inner
// angle
$v = clone $vectors[$next];
if ( $sign > 0 )
{
$v->rotateCounterClockwise()->scalar( $size );
}
else
{
$v->rotateClockwise()->scalar( $size );
}
// get last vector not pointing in reverse direction
$lastVector = clone $vectors[$last];
$lastVector->scalar( -1 );
// Calculate new point: Move point to the center site of the
// polygon using the normalized orthogonal vectors next to the
// point and the size as distance to move.
// point + v + size / tan( angle / 2 ) * startVector
$newPoint = clone $vectors[$next];
$v ->add(
$newPoint
->scalar(
$size /
tan(
$lastVector->angle( $vectors[$next] ) / 2
)
)
);
// A fast guess: If the movement of the point exceeds the length of
// the surrounding edge vectors the angle was to small to perform a
// valid size reducement. In this case we just reduce the length of
// the movement to the minimal length of the surrounding vectors.
// This should fit in most cases.
//
// The correct way to check would be a test, if the calculated
// point is still in the original polygon, but a test for a point
// in a polygon is too expensive.
$movement = $v->length();
if ( ( $movement > $vectorLength[$last] ) &&
( $movement > $vectorLength[$next] ) )
{
$v->unify()->scalar( min( $vectorLength[$last], $vectorLength[$next] ) );
}
$newPoints[$next] = $v->add( $points[$next] );
}
return $newPoints;
}
/**
* Reduce the size of an ellipse
*
* The method returns a the edgepoints and angles for an ellipse where all
* borders are moved to the inner side of the ellipse by the give $size
* value.
*
* The method returns an
* array (
* 'center' => (ezcGraphCoordinate) New center point,
* 'start' => (ezcGraphCoordinate) New outer start point,
* 'end' => (ezcGraphCoordinate) New outer end point,
* )
*
* @param ezcGraphCoordinate $center
* @param float $width
* @param float $height
* @param float $startAngle
* @param float $endAngle
* @param float $size
* @throws ezcGraphReducementFailedException
* @return array
*/
protected function reduceEllipseSize( ezcGraphCoordinate $center, $width, $height, $startAngle, $endAngle, $size )
{
$oldStartPoint = new ezcGraphVector(
$width * cos( deg2rad( $startAngle ) ) / 2,
$height * sin( deg2rad( $startAngle ) ) / 2
);
$oldEndPoint = new ezcGraphVector(
$width * cos( deg2rad( $endAngle ) ) / 2,
$height * sin( deg2rad( $endAngle ) ) / 2
);
// We always need radian values..
$degAngle = abs( $endAngle - $startAngle );
$startAngle = deg2rad( $startAngle );
$endAngle = deg2rad( $endAngle );
// Calculate normalized vectors for the lines spanning the ellipse
$unifiedStartVector = ezcGraphVector::fromCoordinate( $oldStartPoint )->unify();
$unifiedEndVector = ezcGraphVector::fromCoordinate( $oldEndPoint )->unify();
$startVector = ezcGraphVector::fromCoordinate( $oldStartPoint );
$endVector = ezcGraphVector::fromCoordinate( $oldEndPoint );
$oldStartPoint->add( $center );
$oldEndPoint->add( $center );
// Use orthogonal vectors of normalized ellipse spanning vectors to
$v = clone $unifiedStartVector;
$v->rotateClockwise()->scalar( $size );
// calculate new center point
// center + v + size / tan( angle / 2 ) * startVector
$centerMovement = clone $unifiedStartVector;
$newCenter = $v->add( $centerMovement->scalar( $size / tan( ( $endAngle - $startAngle ) / 2 ) ) )->add( $center );
// Test if center is still inside the ellipse, otherwise the sector
// was to small to be reduced
$innerBoundingBoxSize = 0.7 * min( $width, $height );
if ( ( $newCenter->x < ( $center->x + $innerBoundingBoxSize ) ) &&
( $newCenter->x > ( $center->x - $innerBoundingBoxSize ) ) &&
( $newCenter->y < ( $center->y + $innerBoundingBoxSize ) ) &&
( $newCenter->y > ( $center->y - $innerBoundingBoxSize ) ) )
{
// Point is in inner bounding box -> everything is OK
}
elseif ( ( $newCenter->x < ( $center->x - $width ) ) ||
( $newCenter->x > ( $center->x + $width ) ) ||
( $newCenter->y < ( $center->y - $height ) ) ||
( $newCenter->y > ( $center->y + $height ) ) )
{
// Quick outer boundings check
if ( $degAngle > 180 )
{
// Use old center for very big angles
$newCenter = clone $center;
}
else
{
// Do not draw for very small angles
throw new ezcGraphReducementFailedException();
}
}
else
{
// Perform exact check
$distance = new ezcGraphVector(
$newCenter->x - $center->x,
$newCenter->y - $center->y
);
// Convert elipse to circle for correct angle calculation
$direction = clone $distance;
$direction->y *= ( $width / $height );
$angle = $direction->angle( new ezcGraphVector( 0, 1 ) );
$outerPoint = new ezcGraphVector(
sin( $angle ) * $width / 2,
cos( $angle ) * $height / 2
);
// Point is not in ellipse any more
if ( abs( $distance->x ) > abs( $outerPoint->x ) )
{
if ( $degAngle > 180 )
{
// Use old center for very big angles
$newCenter = clone $center;
}
else
{
// Do not draw for very small angles
throw new ezcGraphReducementFailedException();
}
}
}
// Use start spanning vector and its orthogonal vector to calculate
// new start point
$newStartPoint = clone $oldStartPoint;
// Create tangent vector from tangent angle
// Ellipse tangent factor
$ellipseTangentFactor = sqrt(
pow( $height, 2 ) *
pow( cos( $startAngle ), 2 ) +
pow( $width, 2 ) *
pow( sin( $startAngle ), 2 )
);
$ellipseTangentVector = new ezcGraphVector(
$width * -sin( $startAngle ) / $ellipseTangentFactor,
$height * cos( $startAngle ) / $ellipseTangentFactor
);
// Reverse spanning vector
$innerVector = clone $unifiedStartVector;
$innerVector->scalar( $size )->scalar( -1 );
$newStartPoint->add( $innerVector)->add( $ellipseTangentVector->scalar( $size ) );
$newStartVector = clone $startVector;
$newStartVector->add( $ellipseTangentVector );
// Use end spanning vector and its orthogonal vector to calculate
// new end point
$newEndPoint = clone $oldEndPoint;
// Create tangent vector from tangent angle
// Ellipse tangent factor
$ellipseTangentFactor = sqrt(
pow( $height, 2 ) *
pow( cos( $endAngle ), 2 ) +
pow( $width, 2 ) *
pow( sin( $endAngle ), 2 )
);
$ellipseTangentVector = new ezcGraphVector(
$width * -sin( $endAngle ) / $ellipseTangentFactor,
$height * cos( $endAngle ) / $ellipseTangentFactor
);
// Reverse spanning vector
$innerVector = clone $unifiedEndVector;
$innerVector->scalar( $size )->scalar( -1 );
$newEndPoint->add( $innerVector )->add( $ellipseTangentVector->scalar( $size )->scalar( -1 ) );
$newEndVector = clone $endVector;
$newEndVector->add( $ellipseTangentVector );
return array(
'center' => $newCenter,
'start' => $newStartPoint,
'end' => $newEndPoint,
'startAngle' => rad2deg( $startAngle + $startVector->angle( $newStartVector ) ),
'endAngle' => rad2deg( $endAngle - $endVector->angle( $newEndVector ) ),
);
}
/**
* 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
*/
abstract public function drawPolygon( array $points, ezcGraphColor $color, $filled = true, $thickness = 1. );
/**
* 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
*/
abstract public function drawLine( ezcGraphCoordinate $start, ezcGraphCoordinate $end, ezcGraphColor $color, $thickness = 1. );
/**
* 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
*/
abstract protected function getTextBoundings( $size, ezcGraphFontOptions $font, $text );
/**
* Test if string fits in a box with given font size
*
* This method splits the text up into tokens and tries to wrap the text
* in an optimal way to fit in the Box defined by width and height.
*
* If the text fits into the box an array with lines is returned, which
* can be used to render the text later:
* array(
* // Lines
* array( 'word', 'word', .. ),
* )
* Otherwise the function will return false.
*
* @param string $string Text
* @param ezcGraphCoordinate $position Topleft position of the text box
* @param float $width Width of textbox
* @param float $height Height of textbox
* @param int $size Fontsize
* @return mixed Array with lines or false on failure
*/
protected function testFitStringInTextBox( $string, ezcGraphCoordinate $position, $width, $height, $size )
{
// Tokenize String
$tokens = preg_split( '/\s+/', $string );
$initialHeight = $height;
$lines = array( array() );
$line = 0;
foreach ( $tokens as $nr => $token )
{
// Add token to tested line
$selectedLine = $lines[$line];
$selectedLine[] = $token;
$boundings = $this->getTextBoundings( $size, $this->options->font, implode( ' ', $selectedLine ) );
// Check if line is too long
if ( $boundings->width > $width )
{
if ( count( $selectedLine ) == 1 )
{
// Return false if one single word does not fit into one line
// Scale down font size to fit this word in one line
return $width / $boundings->width;
}
else
{
// Put word in next line instead and reduce available height by used space
$lines[++$line][] = $token;
$height -= $size * ( 1 + $this->options->lineSpacing );
}
}
else
{
// Everything is ok - put token in this line
$lines[$line][] = $token;
}
// Return false if text exceeds vertical limit
if ( $size > $height )
{
return 1;
}
}
// Check width of last line
$boundings = $this->getTextBoundings( $size, $this->options->font, implode( ' ', $lines[$line] ) );
if ( $boundings->width > $width )
{
return 1;
}
// It seems to fit - return line array
return $lines;
}
/**
* If it is allow to shortened the string, this method tries to extract as
* many chars as possible to display a decent amount of characters.
*
* If no complete token (word) does fit, the largest possible amount of
* chars from the first word are taken. If the amount of chars is bigger
* then strlen( shortenedStringPostFix ) * 2 the last chars are replace by
* the postfix.
*
* If one complete word fits the box as many words are taken as possible
* including a appended shortenedStringPostFix.
*
* @param mixed $string
* @param ezcGraphCoordinate $position
* @param mixed $width
* @param mixed $height
* @param mixed $size
* @access protected
* @return void
*/
protected function tryFitShortenedString( $string, ezcGraphCoordinate $position, $width, $height, $size )
{
$tokens = preg_split( '/\s+/', $string );
// Try to fit a complete word first
$boundings = $this->getTextBoundings(
$size,
$this->options->font,
reset( $tokens ) . ( $postfix = $this->options->autoShortenStringPostFix )
);
if ( $boundings->width > $width )
{
// Not even one word fits the box
$word = reset( $tokens );
// Test if first character fits the box
$boundigs = $this->getTextBoundings(
$size,
$this->options->font,
$hit = $word[0]
);
if ( $boundigs->width > $width )
{
// That is a really small box.
throw new ezcGraphFontRenderingException( $string, $size, $width, $height );
}
// Try to put more charactes in there
$postLength = strlen( $postfix );
$wordLength = strlen( $word );
for ( $i = 2; $i <= $wordLength; ++$i )
{
$string = substr( $word, 0, $i );
if ( strlen( $string ) > ( $postLength << 1 ) )
{
$string = substr( $string, 0, -$postLength ) . $postfix;
}
$boundigs = $this->getTextBoundings( $size, $this->options->font, $string );
if ( $boundigs->width < $width )
{
$hit = $string;
}
else
{
// Use last string which fit
break;
}
}
}
else
{
// Try to use as many words as possible
$hit = reset( $tokens );
for ( $i = 2; $i < count( $tokens ); ++$i )
{
$string = implode( ' ', array_slice( $tokens, 0, $i ) ) .
$postfix;
$boundings = $this->getTextBoundings( $size, $this->options->font, $string );
if ( $boundings->width <= $width )
{
$hit .= ' ' . $tokens[$i - 1];
}
else
{
// Use last valid hit
break;
}
}
$hit .= $postfix;
}
return array( array( $hit ) );
}
/**
* 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
*/
abstract public function drawTextBox( $string, ezcGraphCoordinate $position, $width, $height, $align, ezcGraphRotation $rotation = null );
/**
* 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
*/
abstract public function drawCircleSector( ezcGraphCoordinate $center, $width, $height, $startAngle, $endAngle, ezcGraphColor $color, $filled = 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 Fill state
* @return void
*/
abstract public function drawCircularArc( ezcGraphCoordinate $center, $width, $height, $size, $startAngle, $endAngle, ezcGraphColor $color, $filled = true );
/**
* 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
*/
abstract public function drawCircle( ezcGraphCoordinate $center, $width, $height, ezcGraphColor $color, $filled = true );
/**
* Draw an image
*
* @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
*/
abstract public function drawImage( $file, ezcGraphCoordinate $position, $width, $height );
/**
* Return mime type for current image format
*
* @return string
*/
abstract public function getMimeType();
/**
* 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( 'php://output' );
}
/**
* Finally save image
*
* @param string $file Destination filename
* @return void
*/
abstract public function render( $file );
}
?>