<?php
/*
 * This work is hereby released into the Public Domain.
 * To view a copy of the public domain dedication,
 * visit http://creativecommons.org/licenses/publicdomain/ or send a letter to
 * Creative Commons, 559 Nathan Abbott Way, Stanford, California 94305, USA.
 *
 */

require_once dirname(__FILE__)."/../Graph.class.php";

/**
 * Handle axis
 *
 * @package Artichow
 */
class awAxis {

	/**
	 * Axis line
	 *
	 * @var Line
	 */
	public $line;

	/**
	 * Axis labels
	 *
	 * @var Label
	 */
	public $label;

	/**
	 * Axis title
	 *
	 * @var Label
	 */
	public $title;

	/**
	 * Title position
	 *
	 * @var float
	 */
	protected $titlePosition = 0.5;

	/**
	 * Labels number
	 *
	 * @var int
	 */
	protected $labelNumber;

	/**
	 * Axis ticks
	 *
	 * @var array
	 */
	protected $ticks = array();

	/**
	 * Axis and ticks color
	 *
	 * @var Color
	 */
	protected $color;

	/**
	 * Axis left and right padding
	 *
	 * @var Side
	 */
	protected $padding;

	/**
	 * Axis range
	 *
	 * @var array
	 */
	protected $range;

	/**
	 * Hide axis
	 *
	 * @var bool
	 */
	protected $hide = FALSE;

	/**
	 * Auto-scaling mode
	 *
	 * @var bool
	 */
	protected $auto = TRUE;

	/**
	 * Axis range callback function
	 *
	 * @var array
	 */
	protected $rangeCallback = array(
		'toValue' => 'toProportionalValue',
		'toPosition' => 'toProportionalPosition'
	);

	/**
	 * Build the axis
	 *
	 * @param float $min Begin of the range of the axis
	 * @param float $max End of the range of the axis
	 */
	public function __construct($min = NULL, $max = NULL) {

		$this->line = new awVector(
			new awPoint(0, 0),
			new awPoint(0, 0)
		);

		$this->label = new awLabel;
		$this->padding = new awSide;

		$this->title = new awLabel(
			NULL,
			NULL,
			NULL,
			0
		);

		$this->setColor(new awBlack);

		if ($min !== NULL and $max !== NULL) {
			$this->setRange($min, $max);
		}

	}

	/**
	 * Enable/disable auto-scaling mode
	 *
	 * @param bool $auto
	 */
	public function auto($auto) {
		$this->auto = (bool)$auto;
	}

	/**
	 * Get auto-scaling mode status
	 *
	 * @return bool
	 */
	public function isAuto() {
		return $this->auto;
	}

	/**
	 * Hide axis
	 *
	 * @param bool $hide
	 */
	public function hide($hide = TRUE) {
		$this->hide = (bool)$hide;
	}

	/**
	 * Show axis
	 *
	 * @param bool $show
	 */
	public function show($show = TRUE) {
		$this->hide = !(bool)$show;
	}

	/**
	 * Return a tick object from its name
	 *
	 * @param string $name Tick object name
	 * @return Tick
	 */
	public function tick($name) {
		
		return array_key_exists($name, $this->ticks) ? $this->ticks[$name] : NULL;
		
			}

	/**
	 * Add a tick object
	 *
	 * @param string $name Tick object name
	 * @param awTick $tick Tick object
	 */
	public function addTick($name, awTick $tick) {
		
		$this->ticks[$name] = $tick;
		
			}

	/**
	 * Delete a tick object
	 *
	 * @param string $name Tick object name
	 */
	public function deleteTick($name) {
		if (array_key_exists($name, $this->ticks)) {
			unset($this->ticks[$name]);
		}
	}

	/**
	 * Hide all ticks
	 *
	 * @param bool $hide Hide or not ?
	 */
	public function hideTicks($hide = TRUE) {
		
		foreach($this->ticks as $tick) {
			$tick->hide($hide);
		}
		
			}

	/**
	 * Change ticks style
	 *
	 * @param int $style Ticks style
	 */
	public function setTickStyle($style) {
		
		foreach($this->ticks as $tick) {
			$tick->setStyle($style);
		}
		
			}

	/**
	 * Change ticks interval
	 *
	 * @param int $interval Ticks interval
	 */
	public function setTickInterval($interval) {
		
		foreach($this->ticks as $tick) {
			$tick->setInterval($interval);
		}
		
			}

	/**
	 * Change number of ticks relative to others ticks
	 *
	 * @param awTick $to Change number of theses ticks
	 * @param awTick $from Ticks reference
	 * @param float $number Number of ticks by the reference
	 */
	public function setNumberByTick($to, $from, $number) {
		$this->ticks[$to]->setNumberByTick($this->ticks[$from], $number);
	}

	/**
	 * Reverse ticks style
	 */
	public function reverseTickStyle() {
		
		foreach($this->ticks as $tick) {
			if ($tick->getStyle() === awTick::IN) {
				$tick->setStyle(awTick::OUT);
			} elseif ($tick->getStyle() === awTick::OUT) {
				$tick->setStyle(awTick::IN);
			}
		}
		
			}

	/**
	 * Change interval of labels
	 *
	 * @param int $interval Interval
	 */
	public function setLabelInterval($interval) {
		$this->auto(FALSE);
		$this->setTickInterval($interval);
		$this->label->setInterval($interval);
	}

	/**
	 * Change number of labels
	 *
	 * @param int $number Number of labels to display (can be NULL)
	 */
	public function setLabelNumber($number) {
		$this->auto(FALSE);
		$this->labelNumber = is_null($number) ? NULL : (int)$number;
	}

	/**
	 * Get number of labels
	 *
	 * @return int
	 */
	public function getLabelNumber() {
		return $this->labelNumber;
	}

	/**
	 * Change precision of labels
	 *
	 * @param int $precision Precision
	 */
	public function setLabelPrecision($precision) {
		$this->auto(FALSE);
		$function = 'axis'.time().'_'.(microtime() * 1000000);
		eval('function '.$function.'($value) {
			return sprintf("%.'.(int)$precision.'f", $value);
		}');
		$this->label->setCallbackFunction($function);
	}

	/**
	 * Change text of labels
	 *
	 * @param array $texts Some texts
	 */
	public function setLabelText($texts) {
		if (is_array($texts)) {
			$this->auto(FALSE);
			$function = 'axis'.time().'_'.(microtime() * 1000000);
			eval('function '.$function.'($value) {
				$texts = '.var_export($texts, TRUE).';
				return isset($texts[$value]) ? $texts[$value] : \'?\';
			}');
			$this->label->setCallbackFunction($function);
		}
	}

	/**
	 * Get the position of a point
	 *
	 * @param awAxis $xAxis X axis
	 * @param awAxis $yAxis Y axis
	 * @param awPoint $p Position of the point
	 * @return Point Position on the axis
	 */
	public static function toPosition(awAxis $xAxis, awAxis $yAxis, awPoint $p) {

		$p1 = $xAxis->getPointFromValue($p->x);
		$p2 = $yAxis->getPointFromValue($p->y);

		return new awPoint(
			round($p1->x),
			round($p2->y)
		);

	}

	/**
	 * Change title alignment
	 *
	 * @param int $alignment New Alignment
	 */
	public function setTitleAlignment($alignment) {

		switch($alignment) {

			case awLabel::TOP :
				$this->setTitlePosition(1);
				$this->title->setAlign(NULL, awLabel::BOTTOM);
				break;

			case awLabel::BOTTOM :
				$this->setTitlePosition(0);
				$this->title->setAlign(NULL, awLabel::TOP);
				break;

			case awLabel::LEFT :
				$this->setTitlePosition(0);
				$this->title->setAlign(awLabel::LEFT);
				break;

			case awLabel::RIGHT :
				$this->setTitlePosition(1);
				$this->title->setAlign(awLabel::RIGHT);
				break;

		}

	}

	/**
	 * Change title position on the axis
	 *
	 * @param float $position A new awposition between 0 and 1
	 */
	public function setTitlePosition($position) {
		$this->titlePosition = (float)$position;
	}

	/**
	 * Change axis and axis title color
	 *
	 * @param awColor $color
	 */
	public function setColor(awColor $color) {
		$this->color = $color;
		$this->title->setColor($color);
	}

	/**
	 * Change axis padding
	 *
	 * @param int $left Left padding in pixels
	 * @param int $right Right padding in pixels
	 */
	public function setPadding($left, $right) {
		$this->padding->set($left, $right);
	}

	/**
	 * Get axis padding
	 *
	 * @return Side
	 */
	public function getPadding() {
		return $this->padding;
	}

	/**
	 * Change axis range
	 *
	 * @param float $min
	 * @param float $max
	 */
	public function setRange($min, $max) {
		if ($min !== NULL) {
			$this->range[0] = (float)$min;
		}
		if ($max !== NULL) {
			$this->range[1] = (float)$max;
		}
	}

	/**
	 * Get axis range
	 *
	 * @return array
	 */
	public function getRange() {
		return $this->range;
	}

	/**
	 * Change axis range callback function
	 *
	 * @param string $toValue Transform a position between 0 and 1 to a value
	 * @param string $toPosition Transform a value to a position between 0 and 1 on the axis
	 */
	public function setRangeCallback($toValue, $toPosition) {
		$this->rangeCallback = array(
			'toValue' => (string)$toValue,
			'toPosition' => (string)$toPosition
		);
	}

	/**
	 * Center X values of the axis
	 *
	 * @param awAxis $axis An axis
	 * @param float $value The reference value on the axis
	 */
	public function setXCenter(awAxis $axis, $value) {

		// Check vector angle
		if ($this->line->isVertical() === FALSE) {
			awImage::drawError("Class Axis: setXCenter() can only be used on vertical axes.");
		}

		$p = $axis->getPointFromValue($value);

		$this->line->setX(
			$p->x,
			$p->x
		);

	}

	/**
	 * Center Y values of the axis
	 *
	 * @param awAxis $axis An axis
	 * @param float $value The reference value on the axis
	 */
	public function setYCenter(awAxis $axis, $value) {

		// Check vector angle
		if ($this->line->isHorizontal() === FALSE) {
			awImage::drawError("Class Axis: setYCenter() can only be used on horizontal axes.");
		}

		$p = $axis->getPointFromValue($value);

		$this->line->setY(
			$p->y,
			$p->y
		);

	}

	/**
	 * Get the distance between to values on the axis
	 *
	 * @param float $from The first value
	 * @param float $to The last value
	 * @return Point
	 */
	public function getDistance($from, $to) {

		$p1 = $this->getPointFromValue($from);
		$p2 = $this->getPointFromValue($to);

		return $p1->getDistance($p2);

	}

	/**
	 * Get a point on the axis from a value
	 *
	 * @param float $value
	 * @return Point
	 */
	protected function getPointFromValue($value) {

		$callback = $this->rangeCallback['toPosition'];

		list($min, $max) = $this->range;
		$position = $callback($value, $min, $max);

		return $this->getPointFromPosition($position);

	}

	/**
	 * Get a point on the axis from a position
	 *
	 * @param float $position A position between 0 and 1
	 * @return Point
	 */
	protected function getPointFromPosition($position) {

		$vector = $this->getVector();

		$angle = $vector->getAngle();
		$size = $vector->getSize();

		return $vector->p1->move(
			cos($angle) * $size * $position,
			-1 * sin($angle) * $size * $position
		);

	}

	/**
	 * Draw axis
	 *
	 * @param awDriver $driver A driver
	 */
	public function draw(awDriver $driver) {

		if ($this->hide) {
			return;
		}

		$vector = $this->getVector();

		// Draw axis ticks
		$this->drawTicks($driver, $vector);

		// Draw axis line
		$this->line($driver);

		// Draw labels
		$this->drawLabels($driver);

		// Draw axis title
		$p = $this->getPointFromPosition($this->titlePosition);
		$this->title->draw($driver, $p);

	}

	public function autoScale() {

		if ($this->isAuto() === FALSE) {
			return;
		}

		list($min, $max) = $this->getRange();
		$interval = $max - $min;

		if ($interval > 0) {
			$partMax = $max / $interval;
			$partMin = $min / $interval;
		} else {
			$partMax = 0;
			$partMin = 0;
		}

		$difference = log($interval) / log(10);
		$difference = floor($difference);

		$pow = pow(10, $difference);

		if ($pow > 0) {
			$intervalNormalize = $interval / $pow;
		} else {
			$intervalNormalize = 0;
		}

		if ($difference <= 0) {

			$precision = $difference * -1 + 1;

			if ($intervalNormalize > 2) {
				$precision--;
			}

		} else {
			$precision = 0;
		}

		if ($min != 0 and $max != 0) {
			$precision++;
		}

		if ($this->label->getCallbackFunction() === NULL) {
			$this->setLabelPrecision($precision);
		}

		if ($intervalNormalize <= 1.5) {
			$intervalReal = 1.5;
			$labelNumber = 4;
		} elseif ($intervalNormalize <= 2) {
			$intervalReal = 2;
			$labelNumber = 5;
		} elseif ($intervalNormalize <= 3) {
			$intervalReal = 3;
			$labelNumber = 4;
		} elseif ($intervalNormalize <= 4) {
			$intervalReal = 4;
			$labelNumber = 5;
		} elseif ($intervalNormalize <= 5) {
			$intervalReal = 5;
			$labelNumber = 6;
		} elseif ($intervalNormalize <= 8) {
			$intervalReal = 8;
			$labelNumber = 5;
		} elseif ($intervalNormalize <= 10) {
			$intervalReal = 10;
			$labelNumber = 6;
		}

		if ($min == 0) {

			$this->setRange(
				$min,
				$intervalReal * $pow
			);

		} elseif ($max == 0) {

			$this->setRange(
				$intervalReal * $pow * -1,
				0
			);

		}

		$this->setLabelNumber($labelNumber);

	}

	protected function line(awDriver $driver) {

		$driver->line(
			$this->color,
			$this->line
		);

	}

	protected function drawTicks(awDriver $driver, awVector $vector) {

		foreach($this->ticks as $tick) {
			$tick->setColor($this->color);
			$tick->draw($driver, $vector);
		}

	}

	protected function drawLabels($driver) {

		if ($this->labelNumber !== NULL) {
			list($min, $max) = $this->range;
			$number = $this->labelNumber - 1;
			if ($number < 1) {
				return;
			}
			$function = $this->rangeCallback['toValue'];
			$labels = array();
			for ($i = 0; $i <= $number; $i++) {
				$labels[] = $function($i / $number, $min, $max);
			}
			$this->label->set($labels);
		}

		$labels = $this->label->count();

		for ($i = 0; $i < $labels; $i++) {

			$p = $this->getPointFromValue($this->label->get($i));
			$this->label->draw($driver, $p, $i);

		}

	}

	protected function getVector() {

		$angle = $this->line->getAngle();

		// Compute paddings
		$vector = new awVector(
			$this->line->p1->move(
				cos($angle) * $this->padding->left,
				-1 * sin($angle) * $this->padding->left
			),
			$this->line->p2->move(
				-1 * cos($angle) * $this->padding->right,
				-1 * -1 * sin($angle) * $this->padding->right
			)
		);

		return $vector;

	}

	public function __clone() {

		$this->label = clone $this->label;
		$this->line = clone $this->line;
		$this->title = clone $this->title;

		foreach($this->ticks as $name => $tick) {
			$this->ticks[$name] = clone $tick;
		}

	}

}

registerClass('Axis');

function toProportionalValue($position, $min, $max) {
	return $min + ($max - $min) * $position;
}

function toProportionalPosition($value, $min, $max) {
	if ($max - $min == 0) {
		return 0;
	}
	return ($value - $min) / ($max - $min);
}
