#!/usr/local/bin/pythonw
# coding: iso-8859-1
# Copyright © 2008 by Amos Newcombe

'''Implement the Axis class for Graph.py.
'''

import sys
from Grid import Grid

class Axis:
	'''An Axis keeps track of its direction (horizontal or vertical), its data and its grid, and calculates the transform from data coordinates found in fsData, to the "pixel" coordinates used in Drawing.
	
	A grid can be specified as a list of numbers, which then become the grid coordinates. If grid evaluates to false, an empty grid instance is created and no grid will be drawn. The client can also specify a Grid instance, which will be the typical use.
	'''
	
	def __init__(self, sDirection, fsData, lsFields=None, grid=None, isDataIn=False):
# 		print '%s(%s, data, %s, %s, %s)' % \
# 			(self.__class__.__name__, sDirection, lsFields, grid, isDataIn)
		self.setDirection(sDirection)
		self.setData(fsData, lsFields)
# 		print self.getData()
# 		print 'grid %s of type %s' % (grid, type(grid).__name__)
		try:
			if issubclass(grid, Grid): grid = grid(self.getData())
# 			print 'grid is a subclass'
		except TypeError, ex: pass #print ex
		if isinstance(grid, list): 
			grid = Grid(self.getData(), [float(o) for o in grid])
		self.setGrid(grid or Grid(self.getData()))
		self.getGrid().setExtremes(isDataIn)
# 		print 'grid:', self.getGrid()
	
	def setDirection(self, sDirection):
		'''Calculate and store the direction using sDirection, which can be (case-insensitive) x or y (for X and Y axes) or h or v (for horizontal and vertical axes), or any string beginning with those characters.
		'''
# 		print '%s.setDirection(%s)' % (`self`, sDirection)
		if   sDirection[0].lower() in 'hx':
			sDirection = 'horizontal'
		elif sDirection[0].lower() in 'vy':
			sDirection = 'vertical'
		self.sDirection = sDirection
	
	def getDirection(self):
		'Recall the stored direction.'
		return self.sDirection
	
	def setData(self, fsData, lsFields):
		'''Copy and store the data, using just the relevant data fields as found in lsFields.
		
		Doing this here is better than sending irelevant data to Grid.setDataExtremes() which it then has to filter out. This way, we have to simple methods instead of one complicated one.
		'''
		self.setFields(lsFields or [])
		self.ldxDataFields = [
			dict([(s, d.get(s)) for s in self.lsFields]) 
			for d in fsData.getData()
		]
	
	def getData(self):
		'Recall the stored data.'
		return self.ldxDataFields
	
	def setFields(self, ls): self.lsFields = ls
	def getFields(self): return self.lsFields
	
	def setGrid(self, grid): self.grid = grid
	def getGrid(self): return self.grid
	
	def __str__(self):
		'a compact string representation useful for debugging'
		return '%s(data, %s, %s)' % (
			self.__class__.__name__, 
			self.getFields(), 
			self.getGrid()
		)
	
	def setTransform(self, pxLength=None, pxUnit=None):
		'''Set the parameters that map data coordinates to pixel coordinates using either the number of pixels long the entire axis is to be (pxLength), or the number of pixels cooresponding to one data unit.
		
		The client calls axis.setTransform() after the axis is created. It can specify neither, one or both of pxLength and pxUnit. If neither, pxLength defaults to 256. If both, then they should be compatible with the axis' grid's range in data coordinates, otherwise a warning is printed to stderr and pxUnit is overridden.
		'''
		if not pxLength: # 0 is also a bad value for both arguments
			if pxUnit:
				pxLength = pxUnit * self.grid.getRange()
			else:
				pxLength = 256.
				pxUnit = self.getUnit(pxLength)
		else:
			pxUnitT = self.getUnit(pxLength)
			if pxUnit and (pxUnit != pxUnitT):
				print >>sys.stderr, \
					'WARNING: pxUnit overridden; change %.4g%%' % 100*(pxUnitT/pxUnit-1)
			pxUnit = pxUnitT
		self.pxLength = pxLength
		self.pxUnit   = pxUnit
		return self
	
	def getUnit(self, pxLength):
		'''Return the pixels/data unit parameter, as calculated from the length of the axis = self.grid.getRange().
		'''
		pxUnit = float(pxLength)
		try: pxUnit /= self.grid.getRange()
		except ZeroDivisionError: pass
		return pxUnit
	
	def Transform(self, x):
		'Transform a number in data coordinates into pixel coordinates.'
		try: return int(self.pxUnit * (x - self.grid.getMin()))
		except AttributeError:
			raise ValueError, 'transform not set yet'
	
	def getPxGrid(self):
		'Return the grid coordinates in pixels.'
		return [self.Transform(x) for x in self.getGrid().getGrid()]
	
	def getPxExtremes(self):
		'Return the extremes of the axis in pixels.'
		return [self.Transform(x) for x in self.getGrid().getExtremes()]

if __name__ == '__main__':
	
	from math import sin
	from mathlib.FiniteSeries.FiniteSeries import FiniteSeries
	from Grid import Round125Grid
	
	ldxData = [{'x': x, 'y': sin(x)} for x in [n*0.25 for n in range(14)]]
	for dx in ldxData: print dx
	fs = FiniteSeries(ldxData, purpose='Axis test')
	axisH = Axis('H', fs, ['x'], Round125Grid).setTransform(pxLength=240)
	axisV = Axis('V', fs, ['y'], Round125Grid).setTransform(pxUnit=axisH.pxUnit)
	print axisH
	print axisV
