src/plot/WindrosePlot.php
<?php
/**
* JPGraph v4.0.3
*/
namespace Amenadiel\JpGraph\Plot;
use Amenadiel\JpGraph\Graph;
use Amenadiel\JpGraph\Image;
use Amenadiel\JpGraph\Text;
use Amenadiel\JpGraph\Util;
define('WINDROSE_TYPE4', 1);
define('WINDROSE_TYPE8', 2);
define('WINDROSE_TYPE16', 3);
define('WINDROSE_TYPEFREE', 4);
/*
* How should the labels for the circular grids be aligned
*/
define('LBLALIGN_CENTER', 1);
define('LBLALIGN_TOP', 2);
/*
* How should the labels around the plot be align
*/
define('LBLPOSITION_CENTER', 1);
define('LBLPOSITION_EDGE', 2);
/*
* Interpretation of ordinal values in the data
*/
define('KEYENCODING_CLOCKWISE', 1);
define('KEYENCODING_ANTICLOCKWISE', 2);
// Internal debug flag
define('__DEBUG', false);
define('RANGE_OVERLAPPING', 0);
define('RANGE_DISCRETE', 1);
/**
* @class WindrosePlot
*/
class WindrosePlot
{
private $iAntiAlias = true;
private $iData = [];
public $iX = 0.5;
public $iY = 0.5;
public $iSize = 0.55;
private $iGridColor1 = 'gray';
private $iGridColor2 = 'darkgreen';
private $iRadialColorArray = [];
private $iRadialWeightArray = [];
private $iRadialStyleArray = [];
private $iRanges = [1, 2, 3, 5, 6, 10, 13.5, 99.0];
private $iRangeStyle = RANGE_OVERLAPPING;
public $iCenterSize = 60;
private $iType = WINDROSE_TYPE16;
public $iFontFamily = FF_VERDANA;
public $iFontStyle = FS_NORMAL;
public $iFontSize = 10;
public $iFontColor = 'darkgray';
private $iRadialGridStyle = 'longdashed';
private $iAllDirectionLabels = ['E', 'ENE', 'NE', 'NNE', 'N', 'NNW', 'NW', 'WNW', 'W', 'WSW', 'SW', 'SSW', 'S', 'SSE', 'SE', 'ESE'];
private $iStandardDirections = [];
private $iCircGridWeight = 3;
private $iRadialGridWeight = 1;
private $iLabelMargin = 12;
private $iLegweights = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20];
private $iLegColors = ['orange', 'black', 'blue', 'red', 'green', 'purple', 'navy', 'yellow', 'brown'];
private $iLabelFormatString = '';
private $iLabels = [];
private $iLabelPositioning = LBLPOSITION_EDGE;
private $iColor = 'white';
private $iShowBox = false;
private $iBoxColor = 'black';
private $iBoxWeight = 1;
private $iBoxStyle = 'solid';
private $iOrdinalEncoding = KEYENCODING_ANTICLOCKWISE;
public $legend;
public function __construct($aData)
{
$this->iData = $aData;
$this->legend = new LegendStyle();
// Setup the scale
$this->scale = new Graph\WindrosePlotScale($this->iData);
// default label for free type i agle and a degree sign
$this->iLabelFormatString = '%.1f' . Graph\SymChar::Get('degree');
$delta = 2 * M_PI / 16;
for ($i = 0, $a = 0; $i < 16; ++$i, $a += $delta) {
$this->iStandardDirections[$this->iAllDirectionLabels[$i]] = $a;
}
}
// Dummy method to make window plots have the same signature as the
// layout classes since windrose plots are "leaf" classes in the hierarchy
public function LayoutSize()
{
return 1;
}
public function SetSize($aSize)
{
$this->iSize = $aSize;
}
public function SetDataKeyEncoding($aEncoding)
{
$this->iOrdinalEncoding = $aEncoding;
}
public function SetColor($aColor)
{
$this->iColor = $aColor;
}
public function SetRadialColors($aColors)
{
$this->iRadialColorArray = $aColors;
}
public function SetRadialWeights($aWeights)
{
$this->iRadialWeightArray = $aWeights;
}
public function SetRadialStyles($aStyles)
{
$this->iRadialStyleArray = $aStyles;
}
public function SetBox($aColor = 'black', $aWeight = 1, $aStyle = 'solid', $aShow = true)
{
$this->iShowBox = $aShow;
$this->iBoxColor = $aColor;
$this->iBoxWeight = $aWeight;
$this->iBoxStyle = $aStyle;
}
public function SetLabels($aLabels)
{
$this->iLabels = $aLabels;
}
public function SetLabelMargin($aMarg)
{
$this->iLabelMargin = $aMarg;
}
public function SetLabelFormat($aLblFormat)
{
$this->iLabelFormatString = $aLblFormat;
}
public function SetCompassLabels($aLabels)
{
if (safe_count($aLabels) != 16) {
Util\JpGraphError::RaiseL(22004); //('Label specification for windrose directions must have 16 values (one for each compass direction).');
}
$this->iAllDirectionLabels = $aLabels;
$delta = 2 * M_PI / 16;
for ($i = 0, $a = 0; $i < 16; ++$i, $a += $delta) {
$this->iStandardDirections[$this->iAllDirectionLabels[$i]] = $a;
}
}
public function SetCenterSize($aSize)
{
$this->iCenterSize = $aSize;
}
// Alias for SetCenterSize
public function SetZCircleSize($aSize)
{
$this->iCenterSize = $aSize;
}
public function SetFont($aFFam, $aFStyle = FS_NORMAL, $aFSize = 10)
{
$this->iFontFamily = $aFFam;
$this->iFontStyle = $aFStyle;
$this->iFontSize = $aFSize;
}
public function SetFontColor($aColor)
{
$this->iFontColor = $aColor;
}
public function SetGridColor($aColor1, $aColor2)
{
$this->iGridColor1 = $aColor1;
$this->iGridColor2 = $aColor2;
}
public function SetGridWeight($aGrid1 = 1, $aGrid2 = 2)
{
$this->iCircGridWeight = $aGrid1;
$this->iRadialGridWeight = $aGrid2;
}
public function SetRadialGridStyle($aStyle)
{
$aStyle = strtolower($aStyle);
if (!in_array($aStyle, ['solid', 'dotted', 'dashed', 'longdashed'], true)) {
Util\JpGraphError::RaiseL(22005); //("Line style for radial lines must be on of ('solid','dotted','dashed','longdashed') ");
}
$this->iRadialGridStyle = $aStyle;
}
public function SetRanges($aRanges)
{
$this->iRanges = $aRanges;
}
public function SetRangeStyle($aStyle)
{
$this->iRangeStyle = $aStyle;
}
public function SetRangeColors($aLegColors)
{
$this->iLegColors = $aLegColors;
}
public function SetRangeWeights($aWeights)
{
$n = safe_count($aWeights);
for ($i = 0; $i < $n; ++$i) {
$aWeights[$i] = floor($aWeights[$i] / 2);
}
$this->iLegweights = $aWeights;
}
public function SetType($aType)
{
if ($aType < WINDROSE_TYPE4 || $aType > WINDROSE_TYPEFREE) {
Util\JpGraphError::RaiseL(22006); //('Illegal windrose type specified.');
}
$this->iType = $aType;
}
// Alias for SetPos()
public function SetCenterPos($aX, $aY)
{
$this->iX = $aX;
$this->iY = $aY;
}
public function SetPos($aX, $aY)
{
$this->iX = $aX;
$this->iY = $aY;
}
public function SetAntiAlias($aFlag)
{
$this->iAntiAlias = $aFlag;
if (!$aFlag) {
$this->iCircGridWeight = 1;
}
}
public function _ThickCircle($aImg, $aXC, $aYC, $aRad, $aWeight, $aColor)
{
$aImg->SetColor($aColor);
$aRad *= 2;
$aImg->Ellipse($aXC, $aYC, $aRad, $aRad);
if ($aWeight > 1) {
$aImg->Ellipse($aXC, $aYC, $aRad + 1, $aRad + 1);
$aImg->Ellipse($aXC, $aYC, $aRad + 2, $aRad + 2);
if ($aWeight > 2) {
$aImg->Ellipse($aXC, $aYC, $aRad + 3, $aRad + 3);
$aImg->Ellipse($aXC, $aYC, $aRad + 3, $aRad + 4);
$aImg->Ellipse($aXC, $aYC, $aRad + 4, $aRad + 3);
}
}
}
public function _StrokeWindLeg($aImg, $xc, $yc, $a, $ri, $r, $weight, $color)
{
// If less than 1 px long then we assume this has been caused by rounding problems
// and should not be stroked
if ($r < 1) {
return;
}
$xt = $xc + cos($a) * $ri;
$yt = $yc - sin($a) * $ri;
$xxt = $xc + cos($a) * ($ri + $r);
$yyt = $yc - sin($a) * ($ri + $r);
$x1 = $xt - $weight * sin($a);
$y1 = $yt - $weight * cos($a);
$x2 = $xxt - $weight * sin($a);
$y2 = $yyt - $weight * cos($a);
$x3 = $xxt + $weight * sin($a);
$y3 = $yyt + $weight * cos($a);
$x4 = $xt + $weight * sin($a);
$y4 = $yt + $weight * cos($a);
$pts = [$x1, $y1, $x2, $y2, $x3, $y3, $x4, $y4];
$aImg->SetColor($color);
$aImg->FilledPolygon($pts);
}
public function _StrokeLegend($aImg, $x, $y, $scaling = 1, $aReturnWidth = false)
{
if (!$this->legend->iShow) {
return 0;
}
$nlc = safe_count($this->iLegColors);
$nlw = safe_count($this->iLegweights);
// Setup font for ranges
$value = new Text\Text();
$value->SetAlign('center', 'bottom');
$value->SetFont(
$this->legend->iLblFontFamily,
$this->legend->iLblFontStyle,
$this->legend->iLblFontSize * $scaling
);
$value->SetColor($this->legend->iLblFontColor);
// Remember x-center
$xcenter = $x;
// Construct format string
$fmt = $this->legend->iFormatString . '-' . $this->legend->iFormatString;
// Make sure that the length of each range is enough to cover the
// size of the labels
$tst = sprintf($fmt, $this->iRanges[0], $this->iRanges[1]);
$value->Set($tst);
$w = $value->GetWidth($aImg);
$l = round(max($this->legend->iLength * $scaling, $w * 1.5));
$r = $this->legend->iCircleRadius * $scaling;
$len = 2 * $r + $this->scale->iMaxNum * $l;
// We are called just to find out the width
if ($aReturnWidth) {
return $len;
}
$x -= round($len / 2);
$x += $r;
// 4 pixels extra vertical margin since the circle sometimes is +/- 1 pixel of the
// theorethical radius due to imperfection in the GD library
//$y -= round(max($r,$scaling*$this->iLegweights[($this->scale->iMaxNum-1) % $nlw])+4*$scaling);
$y -= ($this->legend->iCircleRadius + 2) * $scaling + $this->legend->iBottomMargin * $scaling;
// Adjust for bottom text
if ($this->legend->iTxt != '') {
// Setup font for text
$value->Set($this->legend->iTxt);
$y -= /*$this->legend->iTxtMargin + */$value->GetHeight($aImg);
}
// Stroke 0-circle
$this->_ThickCircle(
$aImg,
$x,
$y,
$r,
$this->legend->iCircleWeight,
$this->legend->iCircleColor
);
// Remember the center of the circe
$xc = $x;
$yc = $y;
$value->SetAlign('center', 'bottom');
$x += $r + 1;
// Stroke all used ranges
$txty = $y -
round($this->iLegweights[($this->scale->iMaxNum - 1) % $nlw] * $scaling) - 4 * $scaling;
if ($this->scale->iMaxNum >= safe_count($this->iRanges)) {
Util\JpGraphError::RaiseL(22007); //('To few values for the range legend.');
}
$i = 0;
$idx = 0;
while ($i < $this->scale->iMaxNum) {
$y1 = $y - round($this->iLegweights[$i % $nlw] * $scaling);
$y2 = $y + round($this->iLegweights[$i % $nlw] * $scaling);
$x2 = $x + $l;
$aImg->SetColor($this->iLegColors[$i % $nlc]);
$aImg->FilledRectangle($x, $y1, $x2, $y2);
if ($this->iRangeStyle == RANGE_OVERLAPPING) {
$lbl = sprintf($fmt, $this->iRanges[$idx], $this->iRanges[$idx + 1]);
} else {
$lbl = sprintf($fmt, $this->iRanges[$idx], $this->iRanges[$idx + 1]);
++$idx;
}
$value->Set($lbl);
$value->Stroke($aImg, $x + $l / 2, $txty);
$x = $x2;
++$i;
++$idx;
}
// Setup circle font
$value->SetFont(
$this->legend->iCircleFontFamily,
$this->legend->iCircleFontStyle,
$this->legend->iCircleFontSize * $scaling
);
$value->SetColor($this->legend->iCircleFontColor);
// Stroke 0-circle text
$value->Set($this->legend->iZCircleTxt);
$value->SetAlign('center', 'center');
$value->ParagraphAlign('center');
$value->Stroke($aImg, $xc, $yc);
// Setup circle font
$value->SetFont(
$this->legend->iTxtFontFamily,
$this->legend->iTxtFontStyle,
$this->legend->iTxtFontSize * $scaling
);
$value->SetColor($this->legend->iTxtFontColor);
// Draw the text under the legend
$value->Set($this->legend->iTxt);
$value->SetAlign('center', 'top');
$value->SetParagraphAlign('center');
$value->Stroke($aImg, $xcenter, $y2 + $this->legend->iTxtMargin * $scaling);
}
public function SetAutoScaleAngle($aIsRegRose = true)
{
// If the user already has manually set an angle don't
// trye to find a position
if (is_numeric($this->scale->iAngle)) {
return;
}
if ($aIsRegRose) {
// Create a complete data for all directions
// and translate string directions to ordinal values.
// This will much simplify the logic below
for ($i = 0; $i < 16; ++$i) {
$dtxt = $this->iAllDirectionLabels[$i];
if (!empty($this->iData[$dtxt])) {
$data[$i] = $this->iData[$dtxt];
} elseif (!empty($this->iData[strtolower($dtxt)])) {
$data[$i] = $this->iData[strtolower($dtxt)];
} elseif (!empty($this->iData[$i])) {
$data[$i] = $this->iData[$i];
} else {
$data[$i] = [];
}
}
// Find the leg which has the lowest weighted sum of number of data around it
$c0 = array_sum($data[0]);
$c1 = array_sum($data[1]);
$found = 1;
$min = $c0 + $c1 * 100; // Initialize to a high value
for ($i = 1; $i < 15; ++$i) {
$c2 = array_sum($data[$i + 1]);
// Weight the leg we will use more to give preference
// to a short middle leg even if the 3 way sum is similair
$w = $c0 + 3 * $c1 + $c2;
if ($w < $min) {
$min = $w;
$found = $i;
}
$c0 = $c1;
$c1 = $c2;
}
$this->scale->iAngle = $found * 22.5;
} else {
$n = safe_count($this->iData);
foreach ($this->iData as $dir => $leg) {
if (!is_numeric($dir)) {
$pos = array_search(strtoupper($dir), $this->iAllDirectionLabels, true);
if ($pos !== false) {
$dir = $pos * 22.5;
}
}
$data[round($dir)] = $leg;
}
// Get all the angles for the data and sort it
$keys = array_keys($data);
sort($keys, SORT_NUMERIC);
$n = safe_count($data);
$found = false;
$max = 0;
for ($i = 0; $i < 15; ++$i) {
$try_a = round(22.5 * $i);
if ($try_a > $keys[$n - 1]) {
break;
}
if (in_array($try_a, $keys, true)) {
continue;
}
// Find the angle just lower than this
$j = 0;
while ($j < $n && $keys[$j] <= $try_a) {
++$j;
}
if ($j == 0) {
$kj = 0;
$keys[$n - 1];
$d1 = 0;
abs($kj - $try_a);
} else {
--$j;
$kj = $keys[$j];
$d1 = abs($kj - $try_a);
}
// Find the angle just larger than this
$l = $n - 1;
while ($l >= 0 && $keys[$l] >= $try_a) {
--$l;
}
if ($l == $n - 1) {
$kl = $keys[0];
$d2 = abs($kl - $try_a);
} else {
++$l;
$kl = $keys[$l];
$d2 = abs($kl - $try_a);
}
// Weight the distance so that legs with large spread
// gets a better weight
$w = $d1 + $d2;
if ($i == 0) {
$w = round(1.4 * $w);
}
$diff = abs($d1 - $d2);
$w *= (360 - $diff);
if ($w > $max) {
$found = $i;
$max = $w;
}
}
$a = $found * 22.5;
// Some heuristics to have some preferred positions
if ($keys[$n - 1] < 25) {
$a = 45;
} elseif ($keys[0] > 60) {
$a = 45;
} elseif ($keys[0] > 25 && $keys[$n - 1] < 340) {
$a = 0;
} elseif ($keys[$n - 1] < 75) {
$a = 90;
} elseif ($keys[$n - 1] < 120) {
$a = 135;
} elseif ($keys[$n - 1] < 160) {
$a = 180;
}
$this->scale->iAngle = $a;
}
}
public function NormAngle($a)
{
while ($a > 360) {
$a -= 360;
}
return $a;
}
public function SetLabelPosition($aPos)
{
$this->iLabelPositioning = $aPos;
}
public function _StrokeFreeRose($dblImg, $value, $scaling, $xc, $yc, $r, $ri)
{
// Plot radial grid lines and remember the end position
// and the angle for later use when plotting the labels
if ($this->iType != WINDROSE_TYPEFREE) {
Util\JpGraphError::RaiseL(22008); //('Internal error: Trying to plot free Windrose even though type is not a free windorose');
}
// Check if we should auto-position the angle for the
// labels. Basically we try to find a firection with smallest
// (or none) data.
$this->SetAutoScaleAngle(false);
$nlc = safe_count($this->iLegColors);
$nlw = safe_count($this->iLegweights);
// Stroke grid lines for directions and remember the
// position for the labels
$txtpos = [];
$num = safe_count($this->iData);
$keys = array_keys($this->iData);
foreach ($this->iData as $dir => $legdata) {
if (in_array($dir, $this->iAllDirectionLabels, true) === true) {
$a = $this->iStandardDirections[strtoupper($dir)];
if (in_array($a * 180 / M_PI, $keys, true)) {
Util\JpGraphError::RaiseL(22009, round($a * 180 / M_PI));
//('You have specified the same direction twice, once with an angle and once with a compass direction ('.$a*180/M_PI.' degrees.)');
}
} elseif (is_numeric($dir)) {
$this->NormAngle($dir);
if ($this->iOrdinalEncoding == KEYENCODING_CLOCKWISE) {
$dir = 360 - $dir;
}
$a = $dir * M_PI / 180;
} else {
Util\JpGraphError::RaiseL(22010); //('Direction must either be a numeric value or one of the 16 compass directions');
}
$xxc = round($xc + cos($a) * $ri);
$yyc = round($yc - sin($a) * $ri);
$x = round($xc + cos($a) * $r);
$y = round($yc - sin($a) * $r);
if (empty($this->iRadialColorArray[$dir])) {
$dblImg->SetColor($this->iGridColor2);
} else {
$dblImg->SetColor($this->iRadialColorArray[$dir]);
}
if (empty($this->iRadialWeightArray[$dir])) {
$dblImg->SetLineWeight($this->iRadialGridWeight);
} else {
$dblImg->SetLineWeight($this->iRadialWeightArray[$dir]);
}
if (empty($this->iRadialStyleArray[$dir])) {
$dblImg->SetLineStyle($this->iRadialGridStyle);
} else {
$dblImg->SetLineStyle($this->iRadialStyleArray[$dir]);
}
$dblImg->StyleLine($xxc, $yyc, $x, $y);
$txtpos[] = [$x, $y, $a];
}
$dblImg->SetLineWeight(1);
// Setup labels
$lr = $scaling * $this->iLabelMargin;
if ($this->iLabelPositioning == LBLPOSITION_EDGE) {
$value->SetAlign('left', 'top');
} else {
$value->SetAlign('center', 'center');
$value->SetMargin(0);
}
for ($i = 0; $i < $num; ++$i) {
list($x, $y, $a) = $txtpos[$i];
// Determine the label
$da = $a * 180 / M_PI;
if ($this->iOrdinalEncoding == KEYENCODING_CLOCKWISE) {
$da = 360 - $da;
}
//$da = 360-$da;
if (!empty($this->iLabels[$keys[$i]])) {
$lbl = $this->iLabels[$keys[$i]];
} else {
$lbl = sprintf($this->iLabelFormatString, $da);
}
if ($this->iLabelPositioning == LBLPOSITION_CENTER) {
$dx = $dy = 0;
} else {
// LBLPOSIITON_EDGE
if ($a >= 7 * M_PI / 4 || $a <= M_PI / 4) {
$dx = 0;
}
if ($a >= M_PI / 4 && $a <= 3 * M_PI / 4) {
$dx = ($a - M_PI / 4) * 2 / M_PI;
}
if ($a >= 3 * M_PI / 4 && $a <= 5 * M_PI / 4) {
$dx = 1;
}
if ($a >= 5 * M_PI / 4 && $a <= 7 * M_PI / 4) {
$dx = (1 - ($a - M_PI * 5 / 4) * 2 / M_PI);
}
if ($a >= 7 * M_PI / 4) {
$dy = (($a - M_PI) - 3 * M_PI / 4) * 2 / M_PI;
}
if ($a <= M_PI / 4) {
$dy = (0.5 + $a * 2 / M_PI);
}
if ($a >= M_PI / 4 && $a <= 3 * M_PI / 4) {
$dy = 1;
}
if ($a >= 3 * M_PI / 4 && $a <= 5 * M_PI / 4) {
$dy = (1 - ($a - 3 * M_PI / 4) * 2 / M_PI);
}
if ($a >= 5 * M_PI / 4 && $a <= 7 * M_PI / 4) {
$dy = 0;
}
}
$value->Set($lbl);
$th = $value->GetHeight($dblImg);
$tw = $value->GetWidth($dblImg);
$xt = round($lr * cos($a) + $x) - $dx * $tw;
$yt = round($y - $lr * sin($a)) - $dy * $th;
$value->Stroke($dblImg, $xt, $yt);
}
if (__DEBUG) {
$dblImg->SetColor('red');
$dblImg->Circle($xc, $yc, $lr + $r);
}
// Stroke all the legs
reset($this->iData);
$i = 0;
foreach ($this->iData as $dir => $legdata) {
$legdata = array_slice($legdata, 1);
$nn = safe_count($legdata);
$a = $txtpos[$i][2];
$rri = $ri / $scaling;
for ($j = 0; $j < $nn; ++$j) {
// We want the non scaled original radius
$legr = $this->scale->RelTranslate($legdata[$j], $r / $scaling, $ri / $scaling);
$this->_StrokeWindLeg(
$dblImg,
$xc,
$yc,
$a,
$rri * $scaling,
$legr * $scaling,
$this->iLegweights[$j % $nlw] * $scaling,
$this->iLegColors[$j % $nlc]
);
$rri += $legr;
}
++$i;
}
}
// Translate potential string specified compass labels to their
// corresponding index.
public function FixupIndexes($aDataArray, $num)
{
$ret = [];
$keys = array_keys($aDataArray);
foreach ($aDataArray as $idx => $data) {
if (is_string($idx)) {
$idx = strtoupper($idx);
$res = array_search($idx, $this->iAllDirectionLabels, true);
if ($res === false) {
Util\JpGraphError::RaiseL(22011, $idx); //('Windrose index must be numeric or direction label. You have specified index='.$idx);
}
$idx = $res;
if ($idx % (16 / $num) !== 0) {
Util\JpGraphError::RaiseL(22012); //('Windrose radial axis specification contains a direction which is not enabled.');
}
$idx /= (16 / $num);
if (in_array($idx, $keys, 1)) {
Util\JpGraphError::RaiseL(22013, $idx); //('You have specified the look&feel for the same compass direction twice, once with text and once with index (Index='.$idx.')');
}
}
if ($idx < 0 || $idx > 15) {
Util\JpGraphError::RaiseL(22014); //('Index for copmass direction must be between 0 and 15.');
}
$ret[$idx] = $data;
}
return $ret;
}
public function _StrokeRegularRose($dblImg, $value, $scaling, $xc, $yc, $r, $ri)
{
// _StrokeRegularRose($dblImg,$xc,$yc,$r,$ri)
// Plot radial grid lines and remember the end position
// and the angle for later use when plotting the labels
switch ($this->iType) {
case WINDROSE_TYPE4:
$num = 4;
break;
case WINDROSE_TYPE8:
$num = 8;
break;
case WINDROSE_TYPE16:
$num = 16;
break;
default:
Util\JpGraphError::RaiseL(22015); //('You have specified an undefined Windrose plot type.');
}
// Check if we should auto-position the angle for the
// labels. Basically we try to find a firection with smallest
// (or none) data.
$this->SetAutoScaleAngle(true);
$nlc = safe_count($this->iLegColors);
$nlw = safe_count($this->iLegweights);
$this->iRadialColorArray = $this->FixupIndexes($this->iRadialColorArray, $num);
$this->iRadialWeightArray = $this->FixupIndexes($this->iRadialWeightArray, $num);
$this->iRadialStyleArray = $this->FixupIndexes($this->iRadialStyleArray, $num);
$txtpos = [];
$a = 2 * M_PI / $num;
$dblImg->SetColor($this->iGridColor2);
$dblImg->SetLineStyle($this->iRadialGridStyle);
$dblImg->SetLineWeight($this->iRadialGridWeight);
// Translate any name specified directions to the index
// so we can easily use it in the loop below
for ($i = 0; $i < $num; ++$i) {
$xxc = round($xc + cos($a * $i) * $ri);
$yyc = round($yc - sin($a * $i) * $ri);
$x = round($xc + cos($a * $i) * $r);
$y = round($yc - sin($a * $i) * $r);
if (empty($this->iRadialColorArray[$i])) {
$dblImg->SetColor($this->iGridColor2);
} else {
$dblImg->SetColor($this->iRadialColorArray[$i]);
}
if (empty($this->iRadialWeightArray[$i])) {
$dblImg->SetLineWeight($this->iRadialGridWeight);
} else {
$dblImg->SetLineWeight($this->iRadialWeightArray[$i]);
}
if (empty($this->iRadialStyleArray[$i])) {
$dblImg->SetLineStyle($this->iRadialGridStyle);
} else {
$dblImg->SetLineStyle($this->iRadialStyleArray[$i]);
}
$dblImg->StyleLine($xxc, $yyc, $x, $y);
$txtpos[] = [$x, $y, $a * $i];
}
$dblImg->SetLineWeight(1);
$lr = $scaling * $this->iLabelMargin;
if ($this->iLabelPositioning == LBLPOSITION_CENTER) {
$value->SetAlign('center', 'center');
} else {
$value->SetAlign('left', 'top');
$value->SetMargin(0);
$lr /= 2;
}
for ($i = 0; $i < $num; ++$i) {
list($x, $y, $a) = $txtpos[$i];
// Set the position of the label
if ($this->iLabelPositioning == LBLPOSITION_CENTER) {
$dx = $dy = 0;
} else {
// LBLPOSIITON_EDGE
if ($a >= 7 * M_PI / 4 || $a <= M_PI / 4) {
$dx = 0;
}
if ($a >= M_PI / 4 && $a <= 3 * M_PI / 4) {
$dx = ($a - M_PI / 4) * 2 / M_PI;
}
if ($a >= 3 * M_PI / 4 && $a <= 5 * M_PI / 4) {
$dx = 1;
}
if ($a >= 5 * M_PI / 4 && $a <= 7 * M_PI / 4) {
$dx = (1 - ($a - M_PI * 5 / 4) * 2 / M_PI);
}
if ($a >= 7 * M_PI / 4) {
$dy = (($a - M_PI) - 3 * M_PI / 4) * 2 / M_PI;
}
if ($a <= M_PI / 4) {
$dy = (0.5 + $a * 2 / M_PI);
}
if ($a >= M_PI / 4 && $a <= 3 * M_PI / 4) {
$dy = 1;
}
if ($a >= 3 * M_PI / 4 && $a <= 5 * M_PI / 4) {
$dy = (1 - ($a - 3 * M_PI / 4) * 2 / M_PI);
}
if ($a >= 5 * M_PI / 4 && $a <= 7 * M_PI / 4) {
$dy = 0;
}
}
$value->Set($this->iAllDirectionLabels[$i * (16 / $num)]);
$th = $value->GetHeight($dblImg);
$tw = $value->GetWidth($dblImg);
$xt = round($lr * cos($a) + $x) - $dx * $tw;
$yt = round($y - $lr * sin($a)) - $dy * $th;
$value->Stroke($dblImg, $xt, $yt);
}
if (__DEBUG) {
$dblImg->SetColor('red');
$dblImg->Circle($xc, $yc, $lr + $r);
}
// Stroke all the legs
reset($this->iData);
$keys = array_keys($this->iData);
foreach ($this->iData as $idx => $legdata) {
$legdata = array_slice($legdata, 1);
$nn = safe_count($legdata);
if (is_string($idx)) {
$idx = strtoupper($idx);
$idx = array_search($idx, $this->iAllDirectionLabels, true);
if ($idx === false) {
Util\JpGraphError::RaiseL(22016); //('Windrose leg index must be numeric or direction label.');
}
if ($idx % (16 / $num) !== 0) {
Util\JpGraphError::RaiseL(22017); //('Windrose data contains a direction which is not enabled. Please adjust what labels are displayed.');
}
$idx /= (16 / $num);
if (in_array($idx, $keys, 1)) {
Util\JpGraphError::RaiseL(22018, $idx); //('You have specified data for the same compass direction twice, once with text and once with index (Index='.$idx.')');
}
}
if ($idx < 0 || $idx > 15) {
Util\JpGraphError::RaiseL(22019); //('Index for direction must be between 0 and 15. You can\'t specify angles for a Regular Windplot, only index and compass directions.');
}
$a = $idx * (360 / $num);
$a *= M_PI / 180.0;
$rri = $ri / $scaling;
for ($j = 0; $j < $nn; ++$j) {
// We want the non scaled original radius
$legr = $this->scale->RelTranslate($legdata[$j], $r / $scaling, $ri / $scaling);
$this->_StrokeWindLeg(
$dblImg,
$xc,
$yc,
$a,
$rri * $scaling,
$legr * $scaling,
$this->iLegweights[$j % $nlw] * $scaling,
$this->iLegColors[$j % $nlc]
);
$rri += $legr;
}
}
}
public function getWidth($aImg)
{
$scaling = 1; //$this->iAntiAlias ? 2 : 1 ;
if ($this->iSize > 0 && $this->iSize < 1) {
$this->iSize *= min($aImg->width, $aImg->height);
}
$value = new Text\Text();
$value->SetFont($this->iFontFamily, $this->iFontStyle, $this->iFontSize * $scaling);
$value->SetColor($this->iFontColor);
// Setup extra size around the graph needed so that the labels
// doesn't get cut. For this we need to find the largest label.
// The code below gives a possible a little to large margin. The
// really, really proper way would be to account for what angle
// the label are at
$n = safe_count($this->iLabels);
if ($n > 0) {
$maxh = 0;
$maxw = 0;
foreach ($this->iLabels as $key => $lbl) {
$value->Set($lbl);
$maxw = max($maxw, $value->GetWidth($aImg));
}
} else {
$value->Set('888.888'); // Dummy value to get width/height
$maxw = $value->GetWidth($aImg);
}
// Add an extra margin of 50% the font size
$maxw += round($this->iFontSize * $scaling * 0.4);
$valxmarg = 1.5 * $maxw + 2 * $this->iLabelMargin * $scaling;
$w = round($this->iSize * $scaling + $valxmarg);
// Make sure that the width of the legend fits
$legendwidth = $this->_StrokeLegend($aImg, 0, 0, $scaling, true) + 10 * $scaling;
$w = max($w, $legendwidth);
return $w;
}
public function getHeight($aImg)
{
$scaling = 1; //$this->iAntiAlias ? 2 : 1 ;
if ($this->iSize > 0 && $this->iSize < 1) {
$this->iSize *= min($aImg->width, $aImg->height);
}
$value = new Text\Text();
$value->SetFont($this->iFontFamily, $this->iFontStyle, $this->iFontSize * $scaling);
$value->SetColor($this->iFontColor);
// Setup extra size around the graph needed so that the labels
// doesn't get cut. For this we need to find the largest label.
// The code below gives a possible a little to large margin. The
// really, really proper way would be to account for what angle
// the label are at
$n = safe_count($this->iLabels);
if ($n > 0) {
$maxh = 0;
$maxw = 0;
foreach ($this->iLabels as $key => $lbl) {
$value->Set($lbl);
$maxh = max($maxh, $value->GetHeight($aImg));
}
} else {
$value->Set('180.8'); // Dummy value to get width/height
$maxh = $value->GetHeight($aImg);
}
// Add an extra margin of 50% the font size
//$maxh += round($this->iFontSize*$scaling * 0.5) ;
$valymarg = 2 * $maxh + 2 * $this->iLabelMargin * $scaling;
$legendheight = round($this->legend->iShow ? 1 : 0);
$legendheight *= max($this->legend->iCircleRadius * 2, $this->legend->iTxtFontSize * 2) +
$this->legend->iMargin + $this->legend->iBottomMargin + 2;
$legendheight *= $scaling;
$h = round($this->iSize * $scaling + $valymarg) + $legendheight;
return $h;
}
public function Stroke($aGraph)
{
$aImg = $aGraph->img;
if ($this->iX > 0 && $this->iX < 1) {
$this->iX = round($aImg->width * $this->iX);
}
if ($this->iY > 0 && $this->iY < 1) {
$this->iY = round($aImg->height * $this->iY);
}
if ($this->iSize > 0 && $this->iSize < 1) {
$this->iSize *= min($aImg->width, $aImg->height);
}
if ($this->iCenterSize > 0 && $this->iCenterSize < 1) {
$this->iCenterSize *= $this->iSize;
}
$this->scale->AutoScale(($this->iSize - $this->iCenterSize) / 2, round(2.5 * $this->scale->iFontSize));
$scaling = $this->iAntiAlias ? 2 : 1;
$value = new Text\Text();
$value->SetFont($this->iFontFamily, $this->iFontStyle, $this->iFontSize * $scaling);
$value->SetColor($this->iFontColor);
$legendheight = round($this->legend->iShow ? 1 : 0);
$legendheight *= max($this->legend->iCircleRadius * 2, $this->legend->iTxtFontSize * 2) +
$this->legend->iMargin + $this->legend->iBottomMargin + 2;
$legendheight *= $scaling;
$w = $scaling * $this->getWidth($aImg);
$h = $scaling * $this->getHeight($aImg);
// Copy back the double buffered image to the proper canvas
$ww = $w / $scaling;
$hh = $h / $scaling;
// Create the double buffer
if ($this->iAntiAlias) {
$dblImg = new Image\RotImage($w, $h);
// Set the background color
$dblImg->SetColor($this->iColor);
$dblImg->FilledRectangle(0, 0, $w, $h);
} else {
$dblImg = $aImg;
// Make sure the ix and it coordinates correpond to the new top left center
$dblImg->SetTranslation($this->iX - $w / 2, $this->iY - $h / 2);
}
if (__DEBUG) {
$dblImg->SetColor('red');
$dblImg->Rectangle(0, 0, $w - 1, $h - 1);
}
$dblImg->SetColor('black');
if ($this->iShowBox) {
$dblImg->SetColor($this->iBoxColor);
$old = $dblImg->SetLineWeight($this->iBoxWeight);
$dblImg->SetLineStyle($this->iBoxStyle);
$dblImg->Rectangle(0, 0, $w - 1, $h - 1);
$dblImg->SetLineWeight($old);
}
$xc = round($w / 2);
$yc = round(($h - $legendheight) / 2);
if (__DEBUG) {
$dblImg->SetColor('red');
$old = $dblImg->SetLineWeight(2);
$dblImg->Line($xc - 5, $yc - 5, $xc + 5, $yc + 5);
$dblImg->Line($xc + 5, $yc - 5, $xc - 5, $yc + 5);
$dblImg->SetLineWeight($old);
}
$this->iSize *= $scaling;
// Inner circle size
$ri = $this->iCenterSize / 2;
// Full circle radius
$r = round($this->iSize / 2);
// Get number of grid circles
$n = $this->scale->GetNumCirc();
// Plot circle grids
$ri *= $scaling;
$rr = round(($r - $ri) / $n);
for ($i = 1; $i <= $n; ++$i) {
$this->_ThickCircle(
$dblImg,
$xc,
$yc,
$rr * $i + $ri,
$this->iCircGridWeight,
$this->iGridColor1
);
}
$num = 0;
if ($this->iType == WINDROSE_TYPEFREE) {
$this->_StrokeFreeRose($dblImg, $value, $scaling, $xc, $yc, $r, $ri);
} else {
// Check if we need to re-code the interpretation of the ordinal
// number in the data. Internally ordinal value 0 is East and then
// counted anti-clockwise. The user might choose an encoding
// that have 0 being the first axis to the right of the "N" axis and then
// counted clock-wise
if ($this->iOrdinalEncoding == KEYENCODING_CLOCKWISE) {
if ($this->iType == WINDROSE_TYPE16) {
$const1 = 19;
$const2 = 16;
} elseif ($this->iType == WINDROSE_TYPE8) {
$const1 = 9;
$const2 = 8;
} else {
$const1 = 4;
$const2 = 4;
}
$tmp = [];
$n = safe_count($this->iData);
foreach ($this->iData as $key => $val) {
if (is_numeric($key)) {
$key = ($const1 - $key) % $const2;
}
$tmp[$key] = $val;
}
$this->iData = $tmp;
}
$this->_StrokeRegularRose($dblImg, $value, $scaling, $xc, $yc, $r, $ri);
}
// Stroke the labels
$this->scale->iFontSize *= $scaling;
$this->scale->iZFontSize *= $scaling;
$this->scale->StrokeLabels($dblImg, $xc, $yc, $ri, $rr);
// Stroke the inner circle again since the legs
// might have written over it
$this->_ThickCircle($dblImg, $xc, $yc, $ri, $this->iCircGridWeight, $this->iGridColor1);
if ($ww > $aImg->width) {
Util\JpGraphError::RaiseL(22020);
//('Windrose plot is too large to fit the specified Graph size. Please use WindrosePlot::SetSize() to make the plot smaller or increase the size of the Graph in the initial WindroseGraph() call.');
}
$x = $xc;
$y = $h;
$this->_StrokeLegend($dblImg, $x, $y, $scaling);
if ($this->iAntiAlias) {
$aImg->Copy($dblImg->img, $this->iX - $ww / 2, $this->iY - $hh / 2, 0, 0, $ww, $hh, $w, $h);
}
// We need to restore the translation matrix
$aImg->SetTranslation(0, 0);
}
}