| [ Index ] |
PHP Cross Reference of MantisBT |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * File containing the abstract ezcGraphDriver class 4 * 5 * @package Graph 6 * @version 1.5 7 * @copyright Copyright (C) 2005-2009 eZ Systems AS. All rights reserved. 8 * @license http://ez.no/licenses/new_bsd New BSD License 9 */ 10 /** 11 * Abstract class to be extended for ezcGraph output drivers. 12 * 13 * @version 1.5 14 * @package Graph 15 */ 16 abstract class ezcGraphDriver 17 { 18 /** 19 * Driveroptions 20 * 21 * @var ezcDriverOptions 22 */ 23 protected $options; 24 25 /** 26 * Constructor 27 * 28 * @param array $options Default option array 29 * @return void 30 * @ignore 31 */ 32 abstract public function __construct( array $options = array() ); 33 34 /** 35 * Options write access 36 * 37 * @throws ezcBasePropertyNotFoundException 38 * If Option could not be found 39 * @throws ezcBaseValueException 40 * If value is out of range 41 * @param mixed $propertyName Option name 42 * @param mixed $propertyValue Option value; 43 * @return mixed 44 * @ignore 45 */ 46 public function __set( $propertyName, $propertyValue ) 47 { 48 switch ( $propertyName ) { 49 case 'options': 50 if ( $propertyValue instanceof ezcGraphDriverOptions ) 51 { 52 $this->options = $propertyValue; 53 } 54 else 55 { 56 throw new ezcBaseValueException( "options", $propertyValue, "instanceof ezcGraphOptions" ); 57 } 58 break; 59 60 default: 61 throw new ezcBasePropertyNotFoundException( $propertyName ); 62 break; 63 } 64 } 65 66 /** 67 * __get 68 * 69 * @param mixed $propertyName 70 * @throws ezcBasePropertyNotFoundException 71 * If a the value for the property options is not an instance of 72 * @return mixed 73 * @ignore 74 */ 75 public function __get( $propertyName ) 76 { 77 switch ( $propertyName ) 78 { 79 case 'options': 80 return $this->options; 81 default: 82 throw new ezcBasePropertyNotFoundException( $propertyName ); 83 } 84 } 85 86 /** 87 * Reduces the size of a polygon 88 * 89 * The method takes a polygon defined by a list of points and reduces its 90 * size by moving all lines to the middle by the given $size value. 91 * 92 * The detection of the inner side of the polygon depends on the angle at 93 * each edge point. This method will always work for 3 edged polygones, 94 * because the smaller angle will always be on the inner side. For 95 * polygons with more then 3 edges this method may fail. For ezcGraph this 96 * is a valid simplification, because we do not have any polygones which 97 * have an inner angle >= 180 degrees. 98 * 99 * @param array(ezcGraphCoordinate) $points 100 * @param float $size 101 * @throws ezcGraphReducementFailedException 102 * @return array( ezcGraphCoordinate ) 103 */ 104 protected function reducePolygonSize( array $points, $size ) 105 { 106 $pointCount = count( $points ); 107 108 // Build normalized vectors between polygon edge points 109 $vectors = array(); 110 $vectorLength = array(); 111 for ( $i = 0; $i < $pointCount; ++$i ) 112 { 113 $nextPoint = ( $i + 1 ) % $pointCount; 114 $vectors[$i] = ezcGraphVector::fromCoordinate( $points[$nextPoint] ) 115 ->sub( $points[$i] ); 116 117 // Throw exception if polygon is too small to reduce 118 $vectorLength[$i] = $vectors[$i]->length(); 119 if ( $vectorLength[$i] < $size ) 120 { 121 throw new ezcGraphReducementFailedException(); 122 } 123 $vectors[$i]->unify(); 124 125 // Remove point from list if it the same as the next point 126 if ( ( $vectors[$i]->x == $vectors[$i]->y ) && ( $vectors[$i]->x == 0 ) ) 127 { 128 $pointCount--; 129 if ( $i === 0 ) 130 { 131 $points = array_slice( $points, $i + 1 ); 132 } 133 else 134 { 135 $points = array_merge( 136 array_slice( $points, 0, $i ), 137 array_slice( $points, $i + 1 ) 138 ); 139 } 140 $i--; 141 } 142 } 143 144 // Remove vectors and appendant point, if local angle equals zero 145 // dergrees. 146 for ( $i = 0; $i < $pointCount; ++$i ) 147 { 148 $nextPoint = ( $i + 1 ) % $pointCount; 149 150 if ( ( abs( $vectors[$i]->x - $vectors[$nextPoint]->x ) < .0001 ) && 151 ( abs( $vectors[$i]->y - $vectors[$nextPoint]->y ) < .0001 ) ) 152 { 153 $pointCount--; 154 155 $points = array_merge( 156 array_slice( $points, 0, $i + 1 ), 157 array_slice( $points, $i + 2 ) 158 ); 159 $vectors = array_merge( 160 array_slice( $vectors, 0, $i + 1 ), 161 array_slice( $vectors, $i + 2 ) 162 ); 163 $i--; 164 } 165 } 166 167 // No reducements for lines 168 if ( $pointCount <= 2 ) 169 { 170 return $points; 171 } 172 173 // Determine one of the angles - we need to know where the smaller 174 // angle is, to determine if the inner side of the polygon is on 175 // the left or right hand. 176 // 177 // This is a valid simplification for ezcGraph(, for now). 178 // 179 // The sign of the scalar products results indicates on which site 180 // the smaller angle is, when comparing the orthogonale vector of 181 // one of the vectors with the other. Why? .. use pen and paper .. 182 // 183 // It is sufficant to do this once before iterating over the points, 184 // because the inner side of the polygon is on the same side of the 185 // point for each point. 186 $last = 0; 187 $next = 1; 188 189 $sign = ( 190 -$vectors[$last]->y * $vectors[$next]->x + 191 $vectors[$last]->x * $vectors[$next]->y 192 ) < 0 ? 1 : -1; 193 194 // Move points to center 195 $newPoints = array(); 196 for ( $i = 0; $i < $pointCount; ++$i ) 197 { 198 $last = $i; 199 $next = ( $i + 1 ) % $pointCount; 200 201 // Orthogonal vector with direction based on the side of the inner 202 // angle 203 $v = clone $vectors[$next]; 204 if ( $sign > 0 ) 205 { 206 $v->rotateCounterClockwise()->scalar( $size ); 207 } 208 else 209 { 210 $v->rotateClockwise()->scalar( $size ); 211 } 212 213 // get last vector not pointing in reverse direction 214 $lastVector = clone $vectors[$last]; 215 $lastVector->scalar( -1 ); 216 217 // Calculate new point: Move point to the center site of the 218 // polygon using the normalized orthogonal vectors next to the 219 // point and the size as distance to move. 220 // point + v + size / tan( angle / 2 ) * startVector 221 $newPoint = clone $vectors[$next]; 222 $v ->add( 223 $newPoint 224 ->scalar( 225 $size / 226 tan( 227 $lastVector->angle( $vectors[$next] ) / 2 228 ) 229 ) 230 ); 231 232 // A fast guess: If the movement of the point exceeds the length of 233 // the surrounding edge vectors the angle was to small to perform a 234 // valid size reducement. In this case we just reduce the length of 235 // the movement to the minimal length of the surrounding vectors. 236 // This should fit in most cases. 237 // 238 // The correct way to check would be a test, if the calculated 239 // point is still in the original polygon, but a test for a point 240 // in a polygon is too expensive. 241 $movement = $v->length(); 242 if ( ( $movement > $vectorLength[$last] ) && 243 ( $movement > $vectorLength[$next] ) ) 244 { 245 $v->unify()->scalar( min( $vectorLength[$last], $vectorLength[$next] ) ); 246 } 247 248 $newPoints[$next] = $v->add( $points[$next] ); 249 } 250 251 return $newPoints; 252 } 253 254 /** 255 * Reduce the size of an ellipse 256 * 257 * The method returns a the edgepoints and angles for an ellipse where all 258 * borders are moved to the inner side of the ellipse by the give $size 259 * value. 260 * 261 * The method returns an 262 * array ( 263 * 'center' => (ezcGraphCoordinate) New center point, 264 * 'start' => (ezcGraphCoordinate) New outer start point, 265 * 'end' => (ezcGraphCoordinate) New outer end point, 266 * ) 267 * 268 * @param ezcGraphCoordinate $center 269 * @param float $width 270 * @param float $height 271 * @param float $startAngle 272 * @param float $endAngle 273 * @param float $size 274 * @throws ezcGraphReducementFailedException 275 * @return array 276 */ 277 protected function reduceEllipseSize( ezcGraphCoordinate $center, $width, $height, $startAngle, $endAngle, $size ) 278 { 279 $oldStartPoint = new ezcGraphVector( 280 $width * cos( deg2rad( $startAngle ) ) / 2, 281 $height * sin( deg2rad( $startAngle ) ) / 2 282 ); 283 284 $oldEndPoint = new ezcGraphVector( 285 $width * cos( deg2rad( $endAngle ) ) / 2, 286 $height * sin( deg2rad( $endAngle ) ) / 2 287 ); 288 289 // We always need radian values.. 290 $degAngle = abs( $endAngle - $startAngle ); 291 $startAngle = deg2rad( $startAngle ); 292 $endAngle = deg2rad( $endAngle ); 293 294 // Calculate normalized vectors for the lines spanning the ellipse 295 $unifiedStartVector = ezcGraphVector::fromCoordinate( $oldStartPoint )->unify(); 296 $unifiedEndVector = ezcGraphVector::fromCoordinate( $oldEndPoint )->unify(); 297 $startVector = ezcGraphVector::fromCoordinate( $oldStartPoint ); 298 $endVector = ezcGraphVector::fromCoordinate( $oldEndPoint ); 299 300 $oldStartPoint->add( $center ); 301 $oldEndPoint->add( $center ); 302 303 // Use orthogonal vectors of normalized ellipse spanning vectors to 304 $v = clone $unifiedStartVector; 305 $v->rotateClockwise()->scalar( $size ); 306 307 // calculate new center point 308 // center + v + size / tan( angle / 2 ) * startVector 309 $centerMovement = clone $unifiedStartVector; 310 $newCenter = $v->add( $centerMovement->scalar( $size / tan( ( $endAngle - $startAngle ) / 2 ) ) )->add( $center ); 311 312 // Test if center is still inside the ellipse, otherwise the sector 313 // was to small to be reduced 314 $innerBoundingBoxSize = 0.7 * min( $width, $height ); 315 if ( ( $newCenter->x < ( $center->x + $innerBoundingBoxSize ) ) && 316 ( $newCenter->x > ( $center->x - $innerBoundingBoxSize ) ) && 317 ( $newCenter->y < ( $center->y + $innerBoundingBoxSize ) ) && 318 ( $newCenter->y > ( $center->y - $innerBoundingBoxSize ) ) ) 319 { 320 // Point is in inner bounding box -> everything is OK 321 } 322 elseif ( ( $newCenter->x < ( $center->x - $width ) ) || 323 ( $newCenter->x > ( $center->x + $width ) ) || 324 ( $newCenter->y < ( $center->y - $height ) ) || 325 ( $newCenter->y > ( $center->y + $height ) ) ) 326 { 327 // Quick outer boundings check 328 if ( $degAngle > 180 ) 329 { 330 // Use old center for very big angles 331 $newCenter = clone $center; 332 } 333 else 334 { 335 // Do not draw for very small angles 336 throw new ezcGraphReducementFailedException(); 337 } 338 } 339 else 340 { 341 // Perform exact check 342 $distance = new ezcGraphVector( 343 $newCenter->x - $center->x, 344 $newCenter->y - $center->y 345 ); 346 347 // Convert elipse to circle for correct angle calculation 348 $direction = clone $distance; 349 $direction->y *= ( $width / $height ); 350 $angle = $direction->angle( new ezcGraphVector( 0, 1 ) ); 351 352 $outerPoint = new ezcGraphVector( 353 sin( $angle ) * $width / 2, 354 cos( $angle ) * $height / 2 355 ); 356 357 // Point is not in ellipse any more 358 if ( abs( $distance->x ) > abs( $outerPoint->x ) ) 359 { 360 if ( $degAngle > 180 ) 361 { 362 // Use old center for very big angles 363 $newCenter = clone $center; 364 } 365 else 366 { 367 // Do not draw for very small angles 368 throw new ezcGraphReducementFailedException(); 369 } 370 } 371 } 372 373 // Use start spanning vector and its orthogonal vector to calculate 374 // new start point 375 $newStartPoint = clone $oldStartPoint; 376 377 // Create tangent vector from tangent angle 378 379 // Ellipse tangent factor 380 $ellipseTangentFactor = sqrt( 381 pow( $height, 2 ) * 382 pow( cos( $startAngle ), 2 ) + 383 pow( $width, 2 ) * 384 pow( sin( $startAngle ), 2 ) 385 ); 386 $ellipseTangentVector = new ezcGraphVector( 387 $width * -sin( $startAngle ) / $ellipseTangentFactor, 388 $height * cos( $startAngle ) / $ellipseTangentFactor 389 ); 390 391 // Reverse spanning vector 392 $innerVector = clone $unifiedStartVector; 393 $innerVector->scalar( $size )->scalar( -1 ); 394 395 $newStartPoint->add( $innerVector)->add( $ellipseTangentVector->scalar( $size ) ); 396 $newStartVector = clone $startVector; 397 $newStartVector->add( $ellipseTangentVector ); 398 399 // Use end spanning vector and its orthogonal vector to calculate 400 // new end point 401 $newEndPoint = clone $oldEndPoint; 402 403 // Create tangent vector from tangent angle 404 405 // Ellipse tangent factor 406 $ellipseTangentFactor = sqrt( 407 pow( $height, 2 ) * 408 pow( cos( $endAngle ), 2 ) + 409 pow( $width, 2 ) * 410 pow( sin( $endAngle ), 2 ) 411 ); 412 $ellipseTangentVector = new ezcGraphVector( 413 $width * -sin( $endAngle ) / $ellipseTangentFactor, 414 $height * cos( $endAngle ) / $ellipseTangentFactor 415 ); 416 417 // Reverse spanning vector 418 $innerVector = clone $unifiedEndVector; 419 $innerVector->scalar( $size )->scalar( -1 ); 420 421 $newEndPoint->add( $innerVector )->add( $ellipseTangentVector->scalar( $size )->scalar( -1 ) ); 422 $newEndVector = clone $endVector; 423 $newEndVector->add( $ellipseTangentVector ); 424 425 return array( 426 'center' => $newCenter, 427 'start' => $newStartPoint, 428 'end' => $newEndPoint, 429 'startAngle' => rad2deg( $startAngle + $startVector->angle( $newStartVector ) ), 430 'endAngle' => rad2deg( $endAngle - $endVector->angle( $newEndVector ) ), 431 ); 432 } 433 434 /** 435 * Draws a single polygon. 436 * 437 * @param array $points Point array 438 * @param ezcGraphColor $color Polygon color 439 * @param mixed $filled Filled 440 * @param float $thickness Line thickness 441 * @return void 442 */ 443 abstract public function drawPolygon( array $points, ezcGraphColor $color, $filled = true, $thickness = 1. ); 444 445 /** 446 * Draws a line 447 * 448 * @param ezcGraphCoordinate $start Start point 449 * @param ezcGraphCoordinate $end End point 450 * @param ezcGraphColor $color Line color 451 * @param float $thickness Line thickness 452 * @return void 453 */ 454 abstract public function drawLine( ezcGraphCoordinate $start, ezcGraphCoordinate $end, ezcGraphColor $color, $thickness = 1. ); 455 456 /** 457 * Returns boundings of text depending on the available font extension 458 * 459 * @param float $size Textsize 460 * @param ezcGraphFontOptions $font Font 461 * @param string $text Text 462 * @return ezcGraphBoundings Boundings of text 463 */ 464 abstract protected function getTextBoundings( $size, ezcGraphFontOptions $font, $text ); 465 466 /** 467 * Test if string fits in a box with given font size 468 * 469 * This method splits the text up into tokens and tries to wrap the text 470 * in an optimal way to fit in the Box defined by width and height. 471 * 472 * If the text fits into the box an array with lines is returned, which 473 * can be used to render the text later: 474 * array( 475 * // Lines 476 * array( 'word', 'word', .. ), 477 * ) 478 * Otherwise the function will return false. 479 * 480 * @param string $string Text 481 * @param ezcGraphCoordinate $position Topleft position of the text box 482 * @param float $width Width of textbox 483 * @param float $height Height of textbox 484 * @param int $size Fontsize 485 * @return mixed Array with lines or false on failure 486 */ 487 protected function testFitStringInTextBox( $string, ezcGraphCoordinate $position, $width, $height, $size ) 488 { 489 // Tokenize String 490 $tokens = preg_split( '/\s+/', $string ); 491 $initialHeight = $height; 492 493 $lines = array( array() ); 494 $line = 0; 495 foreach ( $tokens as $nr => $token ) 496 { 497 // Add token to tested line 498 $selectedLine = $lines[$line]; 499 $selectedLine[] = $token; 500 501 $boundings = $this->getTextBoundings( $size, $this->options->font, implode( ' ', $selectedLine ) ); 502 // Check if line is too long 503 if ( $boundings->width > $width ) 504 { 505 if ( count( $selectedLine ) == 1 ) 506 { 507 // Return false if one single word does not fit into one line 508 // Scale down font size to fit this word in one line 509 return $width / $boundings->width; 510 } 511 else 512 { 513 // Put word in next line instead and reduce available height by used space 514 $lines[++$line][] = $token; 515 $height -= $size * ( 1 + $this->options->lineSpacing ); 516 } 517 } 518 else 519 { 520 // Everything is ok - put token in this line 521 $lines[$line][] = $token; 522 } 523 524 // Return false if text exceeds vertical limit 525 if ( $size > $height ) 526 { 527 return 1; 528 } 529 } 530 531 // Check width of last line 532 $boundings = $this->getTextBoundings( $size, $this->options->font, implode( ' ', $lines[$line] ) ); 533 if ( $boundings->width > $width ) 534 { 535 return 1; 536 } 537 538 // It seems to fit - return line array 539 return $lines; 540 } 541 542 /** 543 * If it is allow to shortened the string, this method tries to extract as 544 * many chars as possible to display a decent amount of characters. 545 * 546 * If no complete token (word) does fit, the largest possible amount of 547 * chars from the first word are taken. If the amount of chars is bigger 548 * then strlen( shortenedStringPostFix ) * 2 the last chars are replace by 549 * the postfix. 550 * 551 * If one complete word fits the box as many words are taken as possible 552 * including a appended shortenedStringPostFix. 553 * 554 * @param mixed $string 555 * @param ezcGraphCoordinate $position 556 * @param mixed $width 557 * @param mixed $height 558 * @param mixed $size 559 * @access protected 560 * @return void 561 */ 562 protected function tryFitShortenedString( $string, ezcGraphCoordinate $position, $width, $height, $size ) 563 { 564 $tokens = preg_split( '/\s+/', $string ); 565 566 // Try to fit a complete word first 567 $boundings = $this->getTextBoundings( 568 $size, 569 $this->options->font, 570 reset( $tokens ) . ( $postfix = $this->options->autoShortenStringPostFix ) 571 ); 572 573 if ( $boundings->width > $width ) 574 { 575 // Not even one word fits the box 576 $word = reset( $tokens ); 577 578 // Test if first character fits the box 579 $boundigs = $this->getTextBoundings( 580 $size, 581 $this->options->font, 582 $hit = $word[0] 583 ); 584 585 if ( $boundigs->width > $width ) 586 { 587 // That is a really small box. 588 throw new ezcGraphFontRenderingException( $string, $size, $width, $height ); 589 } 590 591 // Try to put more charactes in there 592 $postLength = strlen( $postfix ); 593 $wordLength = strlen( $word ); 594 for ( $i = 2; $i <= $wordLength; ++$i ) 595 { 596 $string = substr( $word, 0, $i ); 597 if ( strlen( $string ) > ( $postLength << 1 ) ) 598 { 599 $string = substr( $string, 0, -$postLength ) . $postfix; 600 } 601 602 $boundigs = $this->getTextBoundings( $size, $this->options->font, $string ); 603 604 if ( $boundigs->width < $width ) 605 { 606 $hit = $string; 607 } 608 else 609 { 610 // Use last string which fit 611 break; 612 } 613 } 614 } 615 else 616 { 617 // Try to use as many words as possible 618 $hit = reset( $tokens ); 619 620 for ( $i = 2; $i < count( $tokens ); ++$i ) 621 { 622 $string = implode( ' ', array_slice( $tokens, 0, $i ) ) . 623 $postfix; 624 625 $boundings = $this->getTextBoundings( $size, $this->options->font, $string ); 626 627 if ( $boundings->width <= $width ) 628 { 629 $hit .= ' ' . $tokens[$i - 1]; 630 } 631 else 632 { 633 // Use last valid hit 634 break; 635 } 636 } 637 638 $hit .= $postfix; 639 } 640 641 return array( array( $hit ) ); 642 } 643 644 /** 645 * Writes text in a box of desired size 646 * 647 * @param string $string Text 648 * @param ezcGraphCoordinate $position Top left position 649 * @param float $width Width of text box 650 * @param float $height Height of text box 651 * @param int $align Alignement of text 652 * @param ezcGraphRotation $rotation 653 * @return void 654 */ 655 abstract public function drawTextBox( $string, ezcGraphCoordinate $position, $width, $height, $align, ezcGraphRotation $rotation = null ); 656 657 /** 658 * Draws a sector of cirlce 659 * 660 * @param ezcGraphCoordinate $center Center of circle 661 * @param mixed $width Width 662 * @param mixed $height Height 663 * @param mixed $startAngle Start angle of circle sector 664 * @param mixed $endAngle End angle of circle sector 665 * @param ezcGraphColor $color Color 666 * @param mixed $filled Filled 667 * @return void 668 */ 669 abstract public function drawCircleSector( ezcGraphCoordinate $center, $width, $height, $startAngle, $endAngle, ezcGraphColor $color, $filled = true ); 670 671 /** 672 * Draws a circular arc 673 * 674 * @param ezcGraphCoordinate $center Center of ellipse 675 * @param integer $width Width of ellipse 676 * @param integer $height Height of ellipse 677 * @param integer $size Height of border 678 * @param float $startAngle Starting angle of circle sector 679 * @param float $endAngle Ending angle of circle sector 680 * @param ezcGraphColor $color Color of Border 681 * @param bool $filled Fill state 682 * @return void 683 */ 684 abstract public function drawCircularArc( ezcGraphCoordinate $center, $width, $height, $size, $startAngle, $endAngle, ezcGraphColor $color, $filled = true ); 685 686 /** 687 * Draw circle 688 * 689 * @param ezcGraphCoordinate $center Center of ellipse 690 * @param mixed $width Width of ellipse 691 * @param mixed $height height of ellipse 692 * @param ezcGraphColor $color Color 693 * @param mixed $filled Filled 694 * @return void 695 */ 696 abstract public function drawCircle( ezcGraphCoordinate $center, $width, $height, ezcGraphColor $color, $filled = true ); 697 698 /** 699 * Draw an image 700 * 701 * @param mixed $file Image file 702 * @param ezcGraphCoordinate $position Top left position 703 * @param mixed $width Width of image in destination image 704 * @param mixed $height Height of image in destination image 705 * @return void 706 */ 707 abstract public function drawImage( $file, ezcGraphCoordinate $position, $width, $height ); 708 709 /** 710 * Return mime type for current image format 711 * 712 * @return string 713 */ 714 abstract public function getMimeType(); 715 716 /** 717 * Render image directly to output 718 * 719 * The method renders the image directly to the standard output. You 720 * normally do not want to use this function, because it makes it harder 721 * to proper cache the generated graphs. 722 * 723 * @return void 724 */ 725 public function renderToOutput() 726 { 727 header( 'Content-Type: ' . $this->getMimeType() ); 728 $this->render( 'php://output' ); 729 } 730 731 /** 732 * Finally save image 733 * 734 * @param string $file Destination filename 735 * @return void 736 */ 737 abstract public function render( $file ); 738 } 739 740 ?>
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated: Thu Jul 28 15:48:31 2011 | Cross-referenced by PHPXref 0.7 |