You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

564 lines
18 KiB

<?php
// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
//
// All Rights Reserved. See copyright.txt for details and a complete list of authors.
// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
// $Id$
/* This library is LGPL
* written by Louis-Philippe Huberdeau
*
* vim: fdm=marker tabstop=4 shiftwidth=4 noet:
*
* This file contains the base elements of the graphic
* rendering engine.
*/
/** Rules for implementers!
* You must support all methods.
* Size and locations are float values from 0 to 1
* (0,0) is top left
* Angles are given in degrees, are counter-clockwise and start from (1,0)
* Pie radius size is calculated against the smallest size (width or height)
* All styles located in testRenderer must be supported and have the expected behavior
* Styles are implementation specific and are only generated via getStyle
* Unknown style should return GRenderer::getStyle( $name )
* Use VI fold markers and include the config line in the header
* Prefix internal functions (private or protected) using an underscore
* Text coordinates are top-left corner of the string (bottom left when vertical)
*
* Detailed list in GraphEngineDev
*/
class GRenderer // {{{1
{
public function addLink($target, $left, $top, $right, $bottom, $title = null)
{
die("Abstract Function Call");
}
public function drawLine($x1, $y1, $x2, $y2, $style)
{
die("Abstract Function Call");
}
public function drawRectangle($left, $top, $right, $bottom, $style)
{
die("Abstract Function Call");
}
public function drawPie($centerX, $centerY, $radius, $begin, $end, $style)
{
die("Abstract Function Call");
}
public function drawText($text, $left, $right, $height, $style)
{
die("Abstract Function Call");
}
public function getTextWidth($text, $style)
{
die("Abstract Function Call");
}
public function getTextHeight($style)
{
die("Abstract Function Call");
}
public function getStyle($name)
{
return null;
}
public function httpOutput($filename)
{
die("Abstract Function Call");
}
public function writeToStream($stream)
{
die("Abstract Function Call");
}
public function _getRawColor($name)
{
switch ($name) {
case 'red':
return [ 'r' => 0xCC, 'g' => 0x00, 'b' => 0x00 ];
case 'green':
return [ 'r' => 0x00, 'g' => 0xCC, 'b' => 0x00 ];
case 'blue':
return [ 'r' => 0x00, 'g' => 0x00, 'b' => 0xCC ];
case 'yellow':
return [ 'r' => 0xFF, 'g' => 0xFF, 'b' => 0x00 ];
case 'orange':
return [ 'r' => 0xFF, 'g' => 0x99, 'b' => 0x00 ];
case 'lightgreen':
return [ 'r' => 0x99, 'g' => 0xFF, 'b' => 0x99 ];
case 'lightblue':
return [ 'r' => 0x66, 'g' => 0x99, 'b' => 0xFF ];
case 'black':
return [ 'r' => 0x00, 'g' => 0x00, 'b' => 0x00 ];
case 'white':
return [ 'r' => 0xFF, 'g' => 0xFF, 'b' => 0xFF ];
case 'gray':
return [ 'r' => 0xDD, 'g' => 0xDD, 'b' => 0xDD ];
default:
return [ 'r' => 0x00, 'g' => 0x00, 'b' => 0x00 ];
}
}
}
class Fake_GRenderer extends GRenderer // {{{1
{
public $renderer;
public $left;
public $top;
public $width;
public $height;
public function __construct(&$renderer, $left, $top, $right, $bottom)
{
$this->renderer = &$renderer;
$this->left = $left;
$this->top = $top;
$this->width = $right - $left;
$this->height = $bottom - $top;
}
public function addLink($target, $left, $top, $right, $bottom, $title = null)
{
$this->renderer->addLink(
$target,
$left * $this->width + $this->left,
$top * $this->height + $this->top,
$right * $this->width + $this->left,
$bottom * $this->height + $this->top,
$title
);
}
public function drawLine($x1, $y1, $x2, $y2, $style)
{
$this->renderer->drawLine(
$x1 * $this->width + $this->left,
$y1 * $this->height + $this->top,
$x2 * $this->width + $this->left,
$y2 * $this->height + $this->top,
$style
);
}
public function drawRectangle($left, $top, $right, $bottom, $style)
{
$this->renderer->drawRectangle(
$left * $this->width + $this->left,
$top * $this->height + $this->top,
$right * $this->width + $this->left,
$bottom * $this->height + $this->top,
$style
);
}
public function drawPie($centerX, $centerY, $radius, $begin, $end, $style)
{
$this->renderer->drawPie(
$centerX * $this->width + $this->left,
$centerY * $this->height + $this->top,
$radius * $this->width,
$begin,
$end,
$style
);
}
public function drawText($text, $left, $right, $height, $style)
{
$this->renderer->drawText(
$text,
$left * $this->width + $this->left,
$right * $this->width + $this->left,
$height * $this->height + $this->top,
$style
);
}
public function getTextWidth($text, $style)
{
// Make sure the font size does not get smaller with scale
return $this->renderer->getTextWidth($text, $style) / $this->width;
}
public function getTextHeight($style)
{
// Make sure the font size does not get smaller with scale
return $this->renderer->getTextHeight($style) / $this->height;
}
public function getStyle($name)
{
return $this->renderer->getStyle($name);
}
public function httpOutput($filename)
{
$this->renderer->httpOutput($filename);
}
public function writeToStream($stream)
{
$this->renderer->writeToStream($stream);
}
}
class DataHandler
{
/**
* Provides means to hook into the data display sequence to add decorations
* or external behaviour.
*
* Called every time a data entry is displayed by the engine. Handlers are
* registered by Graphic::addDataHandler(). No implementations are provided
* with the distribution.
*
* @param renderer The renderer instance on which the rendering is performed.
* This instance may not be cached for other purposes.
* @param positionData Indicates the coordinate of the data to be rendered.
* Coordinates are renderer-specific. This value is provided
* as an associative array. (x,y) is provided for single-point
* data. (top,left,bottom,right) is provided for regions.
* @param series The key of the source series of the data.
* @param entryIndex The zero-based index of the entry in the series.
*/
public function handle($renderer, $positionData, $series, $entryIndex)
{
die("Abstract Function Call");
}
}
class Graphic
{
public $legend;
public $title;
public $parameters;
public $dataHandlers;
public function __construct()
{
$this->legend = [];
$this->parameters = [];
$this->dataHandlers = [];
}
public function setTitle($title)
{
$this->title = $title;
}
public function addLegend($color, $value, $url = null)
{
// $color name
// $value label
$this->legend[] = [ $color, $value, $url ];
}
public function addDataHandler($handler)
{
if (is_a($handler, 'DataHandler')) {
$this->dataHandlers[] = $handler;
}
}
public function getRequiredSeries()
{
// Returns an associative array with series name as key
// Value has to be true if the series is required.
die("Abstract Function Call");
}
public function draw(&$renderer)
{
$top = 0;
$left = 0;
$bottom = 1;
$right = 1;
$layout = $this->_layout();
if ($layout['title-active'] && ! empty($this->title)) {
$top += 0.1;
$renderer->drawText($this->title, 0, 1, 0.04, $renderer->getStyle($layout['title-font']));
}
if ($layout['legend-active'] && count($this->legend) > 0 && $layout['legend-location'] != 'static') {
$this->_drawLegend($renderer, $left, $top, $right, $bottom, $layout);
}
$left += $layout['content-margin'];
$right -= $layout['content-margin'];
$top += $layout['content-margin'];
$bottom -= $layout['content-margin'];
$this->_drawContent(new Fake_GRenderer($renderer, $left, $top, $right, $bottom));
if ($layout['legend-active'] && count($this->legend) > 0 && $layout['legend-location'] == 'static') {
$this->_drawLegend($renderer, $left, $top, $right, $bottom, $layout);
}
}
public function setData($data)
{
if (! is_array($data)) {
return false;
}
foreach ($this->getRequiredSeries() as $key => $value) {
if ($value && ( ! array_key_exists($key, $data) || ! is_array($data[$key]) )) {
return false;
}
}
foreach ($data as $key => $values) {
if (! is_array($values)) {
return false;
}
$data[$key] = array_values($values);
}
return $this->_handleData($data);
}
public function setParam($name, $value)
{
$this->parameters[$name] = $value;
}
public function _getColor()
{
static $index = 0;
$colors = [
'Red',
'Green',
'Blue',
'Yellow',
'Orange',
'LightGreen',
'LightBlue'
];
return $colors[$index++ % count($colors)];
}
public function _drawLegend(&$renderer, &$left, &$top, &$right, &$bottom, $layout)
{
$box_size = $layout['legend-box-size'];
$padding = $layout['legend-padding'];
$margin = $layout['legend-margin'];
$legend_font = $renderer->getStyle($layout['legend-font']);
$font_height = $renderer->getTextHeight($legend_font);
// Calculate size {{{3
$item_size = [];
foreach ($this->legend as $key => $value) {
$item_size[$key] = $renderer->getTextWidth($value[1], $legend_font);
}
$width = 0;
$height = 0;
$single_height = max($font_height, $box_size);
switch ($layout['legend-orientation']) {
case 'horizontal':
$height = $single_height + 2 * $padding;
$width =
array_sum($item_size) // text width
+ (1 + count($item_size) ) * $padding // padding between items
+ ( $box_size + $padding ) * count($item_size); // box and box padding
break;
case 'vertical':
$height = $single_height * count($item_size) + (1 + count($item_size) ) * $padding;
$width = max($item_size) + 3 * $padding + $box_size;
break;
}
// Calculate position {{{3
$x = 0;
$y = 0;
switch ($layout['legend-location']) {
case 'left':
$y = $top + ( $bottom - $top ) / 2 - $height / 2;
$x = $left + $margin;
$left += 2 * $margin + $width;
break;
case 'right':
$y = $top + ( $bottom - $top ) / 2 - $height / 2;
$x = $right - $margin - $width;
$right -= 2 * $margin + $width;
break;
case 'bottom':
$x = $left + ( $right - $left ) / 2 - $width / 2;
$y = $bottom - $margin - $height;
$bottom -= 2 * $margin + $height;
break;
case 'top':
$x = $left + ( $right - $left ) / 2 - $width / 2;
$y = $top + $margin;
$top += 2 * $margin + $height;
break;
case 'static':
switch ($layout['legend-location-rel']) {
case 'top-left':
$x = $layout['legend-location-x'] + $margin;
$y = $layout['legend-location-y'] + $margin;
break;
case 'top-right':
$x = $layout['legend-location-x'] - $width - $margin;
$y = $layout['legend-location-y'] + $margin;
break;
case 'bottom-left':
$x = $layout['legend-location-x'] + $margin;
$y = $layout['legend-location-y'] - $height - $margin;
break;
case 'bottom-right':
$x = $layout['legend-location-x'] - $width - $margin;
$y = $layout['legend-location-y'] - $height - $margin;
break;
case 'center':
$x = $layout['legend-location-x'] - $width / 2;
$y = $layout['legend-location-y'] - $height / 2;
break;
}
break;
}
// Draw the thing {{{3
$renderer->drawRectangle($x, $y, $x + $width, $y + $height, $renderer->getStyle($layout['legend-style']));
$box_offset = 0;
$text_offset = 0;
if ($box_size > $font_height) {
$text_offset = ( $box_size - $font_height ) / 2;
} else {
$box_offset = ( $font_height - $box_size ) / 2;
}
$x += $padding;
$y += $padding;
foreach ($this->legend as $key => $info) {
list( $color, $value, $url ) = $info;
$this->_drawLegendBox(
new Fake_GRenderer(
$renderer,
$x,
$y + $box_offset,
$x + $box_size,
$y + $box_size + $box_offset
),
$color
);
switch ($layout['legend-orientation']) {
case 'horizontal':
$renderer->drawText(
$value,
$x + $box_size + $padding,
$x + $box_size + $padding + $item_size[$key],
$y + $text_offset,
$legend_font
);
if (! empty($url)) {
$renderer->addLink(
$url,
$x + $padding,
$y + $box_offset,
$x + $box_size + $padding + $item_size[$key],
$y + $box_offset + $box_size,
$value
);
}
$x += $box_size + $padding * 2 + $item_size[$key];
break;
case 'vertical':
$renderer->drawText(
$value,
$x + $box_size + $padding,
$x - $padding + $width,
$y + $text_offset,
$legend_font
);
if (! empty($url)) {
$renderer->addLink(
$url,
$x,
$y + $box_offset,
$x + $width,
$y + $box_offset + $box_size,
$value
);
}
$y += $padding + $single_height;
break;
}
}
// }}}3
}
public function _drawContent(&$renderer)
{
die("Abstract Function Call");
}
public function _drawLegendBox(&$renderer, $color)
{
die("Abstract Function Call");
}
public function _handleData($data)
{
die("Abstract Function Call");
}
public function _default()
{
return [
'title-active' => true,
'title-font' => 'Normal-Text-Center', // Any Text style name
'legend-active' => true,
'legend-font' => 'Large-Text', // Any Text style name
'legend-location' => 'right', // left, right, bottom, top, static
'legend-location-x' => 1, // static only, x location
'legend-location-y' => 1, // static only, y location
'legend-location-rel' => 'bottom-right', // static only, relative postion (top-left, top-right, bottom-left, bottom-right, center
'legend-orientation' => 'vertical', // horizontal, vertical
'legend-style' => 'FillStroke-White', // Any FillStroke style name
'legend-box-size' => 0.03,
'legend-margin' => 0.01,
'legend-padding' => 0.01,
'content-margin' => 0.02
];
}
public function _layout()
{
// Planning some user-preferences, until then, defaults.
return array_merge($this->_default(), $this->parameters);
}
public function _notify($renderer, $positionData, $series, $index)
{
foreach ($this->dataHandlers as $handler) {
$handler->handle($renderer, $positionData, $series, $index);
}
}
}