"""Implements axis for graphite."""
 
import math
from property import *
import constants as C
from constants import LINEAR, AUTO, X, Y, Z
from primitive import Text, LineStyle, TextStyle, Line
from copy import copy, deepcopy
import pid as PID
import Num
import types
 
 
try:
	math.log(2.0, 3.0)
	log = math.log
except TypeError, x:
	assert '1' in str(x) and '2' in str(x)
	# flexible log function (built-in for python 2.3, not 2.2)
	def log(num, base=math.e):
		return math.log(num)/math.log(base)
 
 
class TickMarks(PropHolder):
	"""Class TickMarks
	Purpose: keeps information about a set of tick marks -- how big
			 they are, the labels, etc.
	Notes: we may have to deal better with overlapping tickmarks later.
	"""
	# declare properties of this class
	_properties = {
		'inextent': FloatProperty(0.02,
				"length (towards center) of marks, in view coordinates",minval=0,maxval=1),
 
		'outextent': FloatProperty(0,
				"length (away from center) of marks, in view coordinates",minval=0,maxval=1),
 
		'spacing': FloatProperty(0,
				"distance between marks, in dataset coordinates (0=auto)",minval=0),
 
		'labeldist': FloatProperty(-0.04,
				"distance from axis in view coordinates where labels should be placed",minval=-1,maxval=1),
 
		# OFI: reconsider the exact definition of 'offset'
		'offset': FloatProperty(0,
				"distance from the origin (in data coordinates) before the tick marks start",minval=0),
 
 
		'lineStyle': ClassProperty(LineStyle, LineStyle(), "style of the tickmark lines"),
 
		'labelStyle': ClassProperty(TextStyle, TextStyle(), "text style of tickmark labels"),
 
		'labels': Property(None,
				"tickmark labels: None, AUTO, a format string, list of strings, or function")  \
		}
 
 
	def __init__(self, inextent=.02, outextent=0,	# extents in frame coords 
				offset=0, 	# offset in data coords
				spacing=0, 	# default=auto mark spacing (in data coords)
				lineStyle=None, labels=None, labelStyle=None):
		PropHolder.__init__(self)
		self.inextent, self.outextent = inextent, outextent
		self.offset = offset
		self.spacing = spacing
		if lineStyle is not None:
			self.lineStyle = lineStyle
		else:
			self.lineStyle = LineStyle()
		if labelStyle is not None:
			self.labelStyle = labelStyle
		else:
			self.labelStyle = TextStyle(font=PID.Font(size=10))
		self.labels = labels
 
 
	def __repr__(self):
		return "TickMarks()"
 
 
	def exportString(self, selfname): 
		"Returns a string with all field assignments."
 
		retval = self.exportStringFunc(selfname,
						proplist = ('inextent','outextent',
								'offset',
            							'lineStyle','labelStyle','labels'
								)
						)
 
		return retval
 
 
 
 
 
	def submitLabel( self, primitives, ticknum, value, pos ):
		"submit a label for the 'ticknum'-th tick mark, having data value 'value', " \
		"at view coordinates 'pos'"
		if callable(self.labels):
			primitives.append( Text(self.labels(ticknum, value), pos, style=self.labelStyle) )
		elif type(self.labels) == types.StringType:
			primitives.append( Text(self.labels % value, pos, style=self.labelStyle) )
		else:
			primitives.append( Text(self.labels[ticknum % len(self.labels)],
							pos, style=self.labelStyle)
					)
 
#----------------------------------------------------------------------
 
class Axis(PropHolder):
	"""Class: Axis
	Purpose: defines the extent, type, and appearance of an axis
	"""
 
	# declare properties of this class
	_properties = {
 
		# OFI: make a typed-list property type
		#'range': Property([0,1], "data range of the axis; [None,None] means auto range"),
		'range': Property([AUTO, AUTO], "data range of the axis; [AUTO,AUTO] means auto range"),
 
		# OFI: make a typed-tuple property type (?)
		'drawPos': Property([((0,0,0),(1,0,0))],
				"locations in view coordinates where the axis should be drawn"),
 
		'label': ClassProperty(Text, None, "axis label"),
 
		'visible': BoolProperty(True, "whether or not the axis should be drawn at all"),
		'tickMarks': ListProperty( ClassProperty(TickMarks, None, ''), None, 
				"list of TickMarks objects attached to this axis") \
 
		# LATER: line style, etc. etc.
		}
 
 
	def __init__(self, whichAxis='X', logbase=LINEAR):
		"Constructor -- initializes member variables with sensible defaults " \
		"for the given axis type (X, Y, or Z)."
 
		PropHolder.__init__(self)
 
		# mapping info
		# OFI: allow axis labels to be repositioned
		#self.range = [0,1]		# range of the axis; [None,None] means auto
		self.range = copy(self.range)
		startpos = (0,0,0)
		if whichAxis=='X' or whichAxis==X:
			endpos = (1,0,0)
			self.label = "X Axis"
			self.label.points[0] = (0,-0.2,0)
		elif whichAxis=='Y' or whichAxis==Y:
			endpos = (0,1,0)
			self.label = "Y Axis"
			self.label.points[0] = (-0.15,0,0)
			self.label.angle = 90
		if self.label:
			# self.label.style.font.bold = 1
			# self.label.style.font.face = 'serif'
			pass
 
		elif whichAxis=='Z' or whichAxis==Z:
			endpos = (0,0,1)
		else: raise TypeError, "Axis constructor did not receive one of 'X', 'Y', 'Z'"
		self.drawPos = [(startpos,endpos)]
		#self.logbase = logbase
 
		# appearance properties
		#self.visible = 1		# is axis visible (0/1)?
		self.tickMarks = [TickMarks()]
 
	def __repr__(self):
		return "Axis()"
 
	def exportString(self, selfname): 
		"Returns a string with all field assignments."
 
		retval = self.exportStringFunc(selfname,
					proplist = ('range', 'drawPos', 'label',
							'visible', 'tickMarks'),
					compProplist = ['tickMarks']
					)
 
		return retval
 
 
	def scale(self):
		"return the scale factor needed to map our range into 0-1"
		return 1.0/(self.actualRange()[1] - self.actualRange()[0])
 
	def origin(self):
		"return the start of our range"
		return self.actualRange()[0]
 
	def viewScale(self, whichAxis):
		"Return the extent of this graph axis in *view* space, along the given " \
		"coordinate axis (X, Y, or Z) -- normally, this extent is 1.  Note that " \
		"only the first drawPos of the axis is considered."
		drawPos = self.drawPos[0]
		return drawPos[1][whichAxis] - drawPos[0][whichAxis]
 
	def viewOrigin(self, whichAxis):
		"Return the origin of this graph axis in *view* space, along the given " \
		"coordinate axis (X, Y, or Z) -- normally, this origin is 0.  Note that " \
		"only the first drawPos of the axis is considered."
		return self.drawPos[0][0][whichAxis]
 
	def setActualRange(self, datarange):
		"Placeholder: Do nothing for now"
		rangestart, rangeend = self.range[:2]
 
	def submit(self, primitives):
		"append any drawing primitives for this axis onto the given list"
		pass
 
	def _submitLabel(self, primitives):
		"""Submit the axis label (if any).
		It adds primitives to the list of primitives that is passed in."""
 
		if self.label:
			# adjust position of the label relative to the midpoint of the axis
			# (actually, midpoint of the union of all drawing positions)
			# ...to do this, we must make a copy
			adjustedText = deepcopy(self.label)
			adjustedPos = [0, 0, 0]
			for axis in (X,Y,Z):
				dwp = [ pos[0][axis] for pos in self.drawPos ] + \
					[ pos[1][axis] for pos in self.drawPos ]
				bmin = min( dwp )
				bmax = max( dwp )
				center = float( bmin + bmax )/2.0
				#print "adjusting label; coordinate", axis, "=", bmin, bmax, center,
				adjustedPos[axis] = self.label.points[0][axis] + center
				#print adjustedPos[axis]
			adjustedText.points[0] = adjustedPos
			#print adjustedText.points
			primitives.append(adjustedText)
 
 
 
class LinearAxis(Axis):
 
	def logorigin(self):
		"return the start of our range, transformed by the log base"
		return self.actualRange()[0]
 
 
	def logscale(self):
		"return the scale factor needed to map our range into 0-1, after a log transformation"
		return self.scale()
 
 
	def transform(self, x):
		return lambda x: x
 
	def dfltLabelFormat(self):
		# print 'rs,re=', self._rangestart, self._rangeend, 'ts=', self._tickspacing
		rel = math.log10((abs(self._rangestart)+abs(self._rangeend))/abs(self._tickspacing))
		a = int(math.floor(math.log10(abs(self._tickspacing))))
		# print 'dflLabel:', self._rangestart, self._rangeend, self._tickspacing, 'rel, a:', rel, a
		if a < 0 and a>-5 and rel+2>abs(a):
			fmtstring = '%%.%df' % -a
			# print 'fmtstring1', fmtstring
			return fmtstring
		if a >= 0 and a<5 and rel+2>a:
			# print 'fmtstring2', '%1f'
			return '%.0f'
		b = int(math.floor(math.log10(abs(self._rangestart) + abs(self._rangeend))))
		b10 = math.pow(10.0, -b)
		# print 'rel=', rel, 'a=', a, 'b=', b
		if rel > 0:
			fmts = '%%.%dfe%d' % (int(math.ceil(rel)), b)
		else:
			fmts = '%%.0fe%d' % b
		# print 'fmtstring34', fmts
		return lambda tn, x, bb=b10, fs=fmts: fs % (x*b10)
 
 
	def setActualRange(self, datarange):
		"""Depending upon AUTO settings set the actual bounds and
		tickmark spacing for this axis"""
		rangestart, rangeend = self.range[:2]
		# for now using a very simple AUTO range algorithm
 
		# 1) determine any AUTO ranges
		if self.range[0] == AUTO:
			rangestart = datarange[0]
 
		if self.range[1] == AUTO:
			rangeend = datarange[1]
 
		# OFI: get rid of this is possible
		# checking to see if this axis has the same start and end
		if rangestart == rangeend:
			rangeend = rangestart + 1
 
		# print "Rstart,Re=", rangestart, rangeend
		# 2) find out what magnitude (power of 10) we are dealing with
		magnitude = math.floor(math.log10(abs(rangeend-rangestart)))
		magnitude = magnitude - 1 # want to deal with next lower magnitude
		# print "magnitude = ", magnitude
		powmag = math.pow(10.0, magnitude)
		if powmag*50 < abs(rangeend - rangestart):
			powmag = powmag * 10
		elif powmag*20 < abs(rangeend - rangestart):
			powmag = powmag * 5
		elif powmag*10 < abs(rangeend - rangestart):
			powmag = powmag * 2
		# print "powmag = ", powmag
 
		# 3) if any AUTO ranges slightly expand the range
		delta = 0.01*powmag
		if rangestart > rangeend:
			delta = -delta
		if self.range[0] == AUTO:
			# print "Rs=", rangestart, "FA=", (rangestart-0.01*powmag)/powmag
			rangestart = math.floor((rangestart-delta)/powmag) * powmag
		if self.range[1] == AUTO:
			rangeend = math.ceil((rangeend+delta)/powmag) * powmag
 
		self._tickspacing = powmag
		self._rangestart = rangestart
		self._rangeend = rangeend
		# print "self._actualRange = ",self._actualRange
 
 
 
	def actualRange(self):
		if self._rangestart > self._rangeend:
			return (self._rangeend, self._rangestart)
		return (self._rangestart, self._rangeend)
 
 
	def submit(self, primitives):
		"append any drawing primitives for this axis onto the given list"
		if not self.visible: return		# if invisible, don't submit anything
 
		for t in self.tickMarks:
			if t.labels == AUTO:
				t.labels = self.dfltLabelFormat()
 
		# submit a single line along the axis
		for positions in self.drawPos:
			startpos = Num.array(positions[0], Num.Float)
			endpos = Num.array(positions[1], Num.Float)
			#print "plotting axis %s from %s to %s" % (self, startpos, endpos)
			primitives.append( Line(startpos, endpos) )
 
			# then submit tickmarks, if any
			# Notes: this is a bit of work; we must account for data range,
			#	drawing of the labels, etc.
 
			for marks in self.tickMarks:
				labelStyle = deepcopy(marks.labelStyle)	# want to keep original settings
				# adjust text justification depending on axis orientation
				# LATER: do this only when existing justification is AUTO
				if startpos[1] == endpos[1]:
					labelStyle.vjust = C.TOP
					labelStyle.hjust = C.CENTER
				else:
					labelStyle.vjust = C.CENTER
					labelStyle.hjust = C.RIGHT
 
				if marks.offset: raise NotImplementedError, "TickMarks.offset"
				style = marks.lineStyle
				pos = self.actualRange()[0]+marks.offset # position along axis, in data coords
 
				# if mapping linear, just copy the axis endpoint
				rangestart = self.actualRange()[0]
				rangeend = self.actualRange()[1]
 
				# Depending on where the axis is located, "in" and "out" may need
				# to be reverse in sign so that "in" is always towards the center.
				innies = [marks.inextent] * 3
				outies = [marks.outextent] * 3
				labeldist = marks.labeldist		# distance of label from axis, in view coords
				for axis in range(0,3):			# check each of x, y, and z coordinate...
					if startpos[axis] > 0.5:	# reverse if axis is more than halfway over
						innies[axis] = innies[axis] * -1
						outies[axis] = outies[axis] * -1
						if startpos[axis] == endpos[axis]:
							labeldist = labeldist * -1
							if labelStyle.hjust == C.RIGHT:
								labelStyle.hjust = C.LEFT
							if labelStyle.vjust == C.TOP:
								labelStyle.vjust = C.BOTTOM
 
				tickcount = 0
				tickspacing = marks.spacing
				if tickspacing <= 0:
					tickspacing = self._tickspacing
				while pos <= self.actualRange()[1]:
					# Convert position along axis to frame coordinates (0-1).
					# Start by calculating the fraction of the way along the axis.
					fpos = (pos - rangestart) * self.scale()
 
					# Then linearly interpolate between the axis endpoints.
					v = (endpos-startpos) * fpos + startpos
					#print fpos, v
 
					# submit marks perpendicular to our axis
					if startpos[X] == endpos[X]:
						primitives.append( Line(
								(v[X]+innies[X], v[Y],v[Z]),
								(v[X]-outies[X],v[Y],v[Z]), style ))
					if startpos[Y] == endpos[Y]:
						primitives.append( Line(
								(v[X],v[Y]+innies[Y], v[Z]),
								(v[X],v[Y]-outies[Y],v[Z]), style ))
					if startpos[Z] == endpos[Z]:
						primitives.append( Line(
								(v[X],v[Y],v[Z]+innies[Z] ),
								(v[X],v[Y],v[Z]-outies[Z]), style ))
					# then, submit tickmark label, "out" in both directions from
					# the axis
					if marks.labels != None:
						labelpos = ( \
								v[X] + (startpos[X]==endpos[X]) * labeldist,
								v[Y] + (startpos[Y]==endpos[Y]) * labeldist,
								v[Z] + (startpos[Z]==endpos[Z]) * labeldist )
						marks.submitLabel( primitives, tickcount, pos, labelpos )
 
					# next tickmark
					pos += tickspacing
					tickcount += 1
 
		self._submitLabel(primitives)
 
 
 
 
 
 
class LogAxis(Axis):
	_properties = Axis._properties.copy()
	_properties['logbase'] = FloatProperty(LINEAR, "LINEAR, or a log base (e.g., 10 or math.e)",
						minval=1.0)
	_properties['logsteps'] = IntProperty(9,
	                                "number of sub-steps within one base cycle on a log axis, or 0 for default",
					minval=0)
 
 
 
 
 
	def logorigin(self):
		"return the start of our range, transformed by the log base"
		return log(self.actualRange()[0], self.logbase)
 
 
	def logscale(self):
		"return the scale factor needed to map our range into 0-1, after a log transformation"
		return 1.0/(log(self.actualRange()[1], self.logbase) - log(self.actualRange()[0], self.logbase))
 
	def exportString(self, selfname): 
		"Returns a string with all field assignments."
 
		retval = self.formatFunc('logbase', selfname, self.logbase)
 
		retval += Axis.exportStringFunc(self, selfname, proplist = ['logbase'])
		retval += Axis.exportString(self, selfname)
 
		return retval
 
 
	def transform(self, x):
		return lambda x, b=self.logbase: log(x, b)
 
	def setActualRange(self, datarange):
		"Placeholder: Do nothing for now"
		rangestart, rangeend = self.range[:2]
 
	def submit(self, primitives):
		"append any drawing primitives for this axis onto the given list"
		if not self.visible: return		# if invisible, don't submit anything
 
		# submit a single line along the axis
		for positions in self.drawPos:
			startpos = Num.array(positions[0], Num.Float)
			endpos = Num.array(positions[1], Num.Float)
			#print "plotting axis %s from %s to %s" % (self, startpos, endpos)
			primitives.append( Line(startpos, endpos) )
 
			# then submit tickmarks, if any
			# Notes: this is a bit of work; we must account for data range,
			#	drawing of the labels, etc.
 
			for marks in self.tickMarks:
				labelStyle = deepcopy(marks.labelStyle)	# want to keep original settings
				# adjust text justification depending on axis orientation
				# LATER: do this only when existing justification is AUTO
				if startpos[1] == endpos[1]:
					labelStyle.vjust = C.TOP
					labelStyle.hjust = C.CENTER
				else:
					labelStyle.vjust = C.CENTER
					labelStyle.hjust = C.RIGHT
 
				if marks.offset:
					raise NotImplementedError, "TickMarks.offset"
				style = marks.lineStyle
				pos = self.actualRange()[0]+marks.offset # position along axis, in data coords
 
				# determine the range based on the logbase
				# Calculate the substep used in plotting log tickmarks
				if marks.logsteps < 1:
					raise NotImplementedError, "Can't agree on defaults!"
				# find the starting point, in log space, truncated
				assert(pos > 0)
				a = log(pos, self.logbase)
				substep = (math.pow(self.logbase, a) - math.pow(self.logbase, a-1)) / marks.logsteps
				# print "substep: ", substep
 
				# also, calculate log transform of axis endpoints
				rangestart = log(self.actualRange()[0], self.logbase)
				rangeend = log(self.actualRange()[1], self.logbase)
 
				# Depending on where the axis is located, "in" and "out" may need
				# to be reverse in sign so that "in" is always towards the center.
				innies = [marks.inextent] * 3
				outies = [marks.outextent] * 3
				labeldist = marks.labeldist		# distance of label from axis, in view coords
				for axis in range(0,3):			# check each of x, y, and z coordinate...
					if startpos[axis] > 0.5:	# reverse if axis is more than halfway over
						innies[axis] = innies[axis] * -1
						outies[axis] = outies[axis] * -1
						if startpos[axis] == endpos[axis]:
							labeldist = labeldist * -1
							if labelStyle.hjust == C.RIGHT:
								labelStyle.hjust = C.LEFT
							if labelStyle.vjust == C.TOP:
								labelStyle.vjust = C.BOTTOM
 
				tickcount = 0
				while pos <= self.actualRange()[1]:
					# Convert position along axis to frame coordinates (0-1).
					# Start by calculating the fraction of the way along the axis.
					fpos = (log(pos, self.logbase) - rangestart) * self.logscale()
 
					# Then linearly interpolate between the axis endpoints.
					v = (endpos-startpos) * fpos + startpos
					#print fpos, v
 
					# submit marks perpendicular to our axis
					if startpos[X] == endpos[X]:
						primitives.append( Line(
								(v[X]+innies[X], v[Y],v[Z]),
								(v[X]-outies[X],v[Y],v[Z]), style ))
					if startpos[Y] == endpos[Y]:
						primitives.append( Line(
								(v[X],v[Y]+innies[Y], v[Z]),
								(v[X],v[Y]-outies[Y],v[Z]), style ))
					if startpos[Z] == endpos[Z]:
						primitives.append( Line(
								(v[X],v[Y],v[Z]+innies[Z] ),
								(v[X],v[Y],v[Z]-outies[Z]), style ))
					# then, submit tickmark label, "out" in both directions from
					# the axis
					if marks.labels != None:
						labelpos = ( \
								v[X] + (startpos[X]==endpos[X]) * labeldist,
								v[Y] + (startpos[Y]==endpos[Y]) * labeldist,
								v[Z] + (startpos[Z]==endpos[Z]) * labeldist )
						marks.submitLabel( primitives, tickcount, pos, labelpos )
 
					# next tickmark
					#print "tickcount=%s, pos=%s" % (tickcount, pos)
					if tickcount % (marks.logsteps) == 0:
						substep = substep * self.logbase
					pos = pos + substep
					tickcount = tickcount + 1
 
		self._submitLabel(primitives)