#!/usr/bin/env python

# A class that creates an opengl widget.
# Mike Hartshorn
# Department of Chemistry
# University of York, UK
# 

from OpenGL.GL import *
from OpenGL.GLU import *
from Tkinter import _default_root
from Tkinter import *
import math
import os,sys

# Keith Junius <junius@chem.rug.nl> provided many changes to Togl
TOGL_NORMAL = 1
TOGL_OVERLAY = 2

def v3distsq(a,b):
  d = ( a[0] - b[0], a[1] - b[1], a[2] - b[2] )
  return d[0]*d[0] + d[1]*d[1] + d[2]*d[2]

# new version from Daniel Faken (Daniel_Faken@brown.edu) for static 
# loading comptability
if _default_root is None:
  _default_root = Tk()

# add this file's directory to Tcl's search path
# on Linux Togl is installed in ./linux2-tk8.0
# on Windows (Python2.0) in ./win32-tk8.3  
_default_root.tk.call('lappend', 'auto_path', os.path.join( \
                  os.path.dirname(__file__), \
                  sys.platform + "-tk" + _default_root.getvar("tk_version")))
try:
    _default_root.tk.eval('load {} Togl')
except TclError:
    pass
_default_root.tk.call('package', 'require', 'Togl')

# This code is needed to avoid faults on sys.exit()
# [DAA, Jan 1998]
import sys
oldexitfunc = None
if hasattr(sys, 'exitfunc'):
    oldexitfunc = sys.exitfunc
def cleanup():
    from Tkinter import _default_root, TclError
    import Tkinter
    try: 
	if _default_root: _default_root.destroy()
    except TclError:
	pass
    Tkinter._default_root = None
    if oldexitfunc: oldexitfunc()
sys.exitfunc = cleanup
# [end DAA]

class Togl(Widget):
    """
    Togl Widget
    Keith Junius
    Department of Biophysical Chemistry
    University of Groningen, The Netherlands
    Very basic widget which provides access to Togl functions.
    N.B. this requires a modified version of Togl 1.5 to gain access to the
    extra functionality. This support should be included in Togl 1.6, I hope.
    """
    def __init__(self, master=None, cnf={}, **kw):
        Widget.__init__(self, master, 'togl', cnf, kw)
    def render(self):
        return
    def swapbuffers(self):
        self.tk.call(self._w, 'swapbuffers')
    def makecurrent(self):
        self.tk.call(self._w, 'makecurrent')
    def alloccolor(self, red, green, blue):
        return self.tk.getint(self.tk.call(self._w, 'alloccolor', red, green, blue))
    def freecolor(self, index):
        self.tk.call(self._w, 'freecolor', index)
    def setcolor(self, index, red, green, blue):
        self.tk.call(self._w, 'setcolor', index, red, green, blue)
    def loadbitmapfont(self, fontname):
        return self.tk.getint(self.tk.call(self._w, 'loadbitmapfont', fontname))
    def unloadbitmapfont(self, fontbase):
        self.tk.call(self._w, 'unloadbitmapfont', fontbase)
    def uselayer(self, layer):
        self.tk.call(self._w, 'uselayer', layer)
    def showoverlay(self):
        self.tk.call(self._w, 'showoverlay')
    def hideoverlay(self):
        self.tk.call(self._w, 'hideoverlay')
    def existsoverlay(self):
        return self.tk.getboolean(self.tk.call(self._w, 'existsoverlay'))
    def getoverlaytransparentvalue(self):
        return self.tk.getint(self.tk.call
                (self._w, 'getoverlaytransparentvalue'))
    def ismappedoverlay(self):
        return self.tk.getboolean(self.tk.call(self._w, 'ismappedoverlay'))
    def alloccoloroverlay(self, red, green, blue):
        return self.tk.getint(self.tk.call(self._w, 'alloccoloroverlay',
                red, green, blue))
    def freecoloroverlay(self, index):
        self.tk.call(self._w, 'freecoloroverlay', index)

class RawOpengl(Widget, Misc):
  """Widget without any sophisticated bindings\
     by Tom Schwaller"""

  def __init__(self, master=None, cnf={}, **kw):
    Widget.__init__(self, master, 'togl', cnf, kw)
    self.bind('<Map>', self.tkMap)
    self.bind('<Expose>', self.tkExpose)
    self.bind('<Configure>', self.tkExpose)

  def tkRedraw(self, *dummy):
    self.tk.call(self._w, 'makecurrent')
    _mode = glGetDouble(GL_MATRIX_MODE)
    glMatrixMode(GL_PROJECTION)
    glPushMatrix()
    self.update_idletasks()
    self.redraw()
    glFlush()
    glPopMatrix()
    glMatrixMode(_mode)
    self.tk.call(self._w, 'swapbuffers')

  def tkMap(self, *dummy):
    self.tkExpose()

  def tkExpose(self, *dummy):
    self.tkRedraw()

class Opengl(RawOpengl):
  """\
Tkinter bindings for an Opengl widget.
Mike Hartshorn
Department of Chemistry
University of York, UK
http://www.yorvic.york.ac.uk/~mjh/
"""

  def __init__(self, master=None, cnf={}, **kw):
    """\
    Create an opengl widget.
    Arrange for redraws when the window is exposed or when
    it changes size."""

    #Widget.__init__(self, master, 'togl', cnf, kw)
    apply(RawOpengl.__init__, (self, master, cnf), kw)
    self.initialised = 0

    # Current coordinates of the mouse.
    self.xmouse = 0
    self.ymouse = 0

    # Where we are centering.
    self.xcenter = 0.0
    self.ycenter = 0.0
    self.zcenter = 0.0

    # The _back color
    self.r_back = 1.
    self.g_back = 0.
    self.b_back = 1.

    # Where the eye is
    self.distance = 10.0

    # Field of view in y direction
    self.fovy = 30.0

    # Position of clipping planes.
    self.near = 0.1
    self.far = 1000.0

    # Is the widget allowed to autospin?
    self.autospin_allowed = 0

    # Is the widget currently autospinning?
    self.autospin = 0

    # Basic bindings for the virtual trackball
    self.bind('<Map>', self.tkMap)
    self.bind('<Expose>', self.tkExpose)
    self.bind('<Configure>', self.tkExpose)
    self.bind('<Shift-Button-1>', self.tkHandlePick)
    #self.bind('<Button-1><ButtonRelease-1>', self.tkHandlePick)
    self.bind('<Button-1>', self.tkRecordMouse)
    self.bind('<B1-Motion>', self.tkTranslate)
    self.bind('<Button-2>', self.StartRotate)
    self.bind('<B2-Motion>', self.tkRotate)
    self.bind('<ButtonRelease-2>', self.tkAutoSpin)
    self.bind('<Button-3>', self.tkRecordMouse)
    self.bind('<B3-Motion>', self.tkScale)

  def help(self):
    """Help for the widget."""

    import Dialog
    d = Dialog.Dialog(None, {'title': 'Viewer help',
			     'text':
			     'Button-1: Translate\n'
			     'Button-2: Rotate\n'
			     'Button-3: Zoom\n'
			     'Reset: Resets transformation to identity\n',
			     'bitmap': 'questhead',
			     'default': 0,
			     'strings': ('Done', 'Ok')})

  def activate(self):
    """Cause this Opengl widget to be the current destination for drawing."""

    self.tk.call(self._w, 'makecurrent')

  # This should almost certainly be part of some derived class.
  # But I have put it here for convenience.

  def basic_lighting(self):
    """\
    Set up some basic lighting (single infinite light source).

    Also switch on the depth buffer."""
   
    self.activate()
    light_position = (1, 1, 1, 0);
    glLightf(GL_LIGHT0, GL_POSITION, light_position);
    glEnable(GL_LIGHTING);
    glEnable(GL_LIGHT0);
    glDepthFunc(GL_LESS);
    glEnable(GL_DEPTH_TEST);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity()

  def report_opengl_errors(message = "OpenGL error:"):
    """Report any opengl errors that occured while drawing."""

    while 1:
      err_value = glGetError()
      if not err_value: break     
      print message, gluErrorString(err_value)

  def set_background(self, r, g, b):
    """Change the background colour of the widget."""

    self.r_back = r
    self.g_back = g
    self.b_back = b

    self.tkRedraw()

  def set_centerpoint(self, x, y, z):
    """Set the new center point for the model.
    This is where we are looking."""

    self.xcenter = x
    self.ycenter = y
    self.zcenter = z

    self.tkRedraw()

  def set_eyepoint(self, distance):
    """Set how far the eye is from the position we are looking."""

    self.distance = distance
    self.tkRedraw()

  def reset(self):
    """Reset rotation matrix for this widget."""

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity()
    self.tkRedraw()

  def tkHandlePick(self, event):
    """Handle a pick on the scene."""

    if hasattr(self, 'pick'):
      # here we need to use glu.UnProject

      # Tk and X have their origin top left, 
      # while Opengl has its origin bottom left.
      # So we need to subtract y from the window height to get
      # the proper pick position for Opengl

      realy = self.winfo_height() - event.y

      p1 = gluUnProject(event.x, realy, 0.)
      p2 = gluUnProject(event.x, realy, 1.)

      if self.pick(self, p1, p2):
	"""If the pick method returns true we redraw the scene."""

	self.tkRedraw()

  def tkRecordMouse(self, event):
    """Record the current mouse position."""

    self.xmouse = event.x
    self.ymouse = event.y

  def StartRotate(self, event):

    # Switch off any autospinning if it was happening

    self.autospin = 0

    self.tkRecordMouse(event)

  def tkScale(self, event):
    """Scale the scene.  Achieved by moving the eye position."""

    scale = 1 - 0.01 * (event.y - self.ymouse)
    self.distance = self.distance * scale
    self.tkRedraw()
    self.tkRecordMouse(event)

  def do_AutoSpin(self):
    s = 0.5
    self.activate()

    glRotateScene(0.5,
		   self.xcenter, self.ycenter, self.zcenter,
		   self.yspin, self.xspin, 0, 0)
    self.tkRedraw()

    if self.autospin:
      self.after(10, self.do_AutoSpin)

  def tkAutoSpin(self, event):
    """Perform autospin of scene."""

    self.after(4)
    self.update_idletasks()

    # This could be done with one call to pointerxy but I'm not sure
    # it would any quicker as we would have to split up the resulting
    # string and then conv

    x = self.tk.getint(self.tk.call('winfo', 'pointerx', self._w))
    y = self.tk.getint(self.tk.call('winfo', 'pointery', self._w))

    if self.autospin_allowed:
      if x != event.x_root and y != event.y_root:
	self.autospin = 1

	self.yspin = x - event.x_root
	self.xspin = y - event.y_root

	self.after(10, self.do_AutoSpin)

  def tkRotate(self, event):
    """Perform rotation of scene."""

    self.activate()
    glRotateScene(0.5,
		   self.xcenter, self.ycenter, self.zcenter,
		   event.x, event.y, self.xmouse, self.ymouse)
    self.tkRedraw()
    self.tkRecordMouse(event)

  def tkTranslate(self, event):
    """Perform translation of scene."""

    self.activate()

    # Scale mouse translations to object viewplane so object tracks with mouse
    win_height = max( 1,self.winfo_height() )
    obj_c      = ( self.xcenter, self.ycenter, self.zcenter )
    win        = gluProject( obj_c[0], obj_c[1], obj_c[2] )
    obj        = gluUnProject( win[0], win[1] + 0.5 * win_height, win[2] )
    dist       = math.sqrt( v3distsq( obj, obj_c ) )
    scale      = abs( dist / ( 0.5 * win_height ) )

    glTranslateScene(scale, event.x, event.y, self.xmouse, self.ymouse)
    self.tkRedraw()
    self.tkRecordMouse(event)

  def tkRedraw(self, *dummy):
    """Cause the opengl widget to redraw itself."""

    if not self.initialised: return
    self.activate()

    glPushMatrix()			# Protect our matrix
    self.update_idletasks()
    w = self.winfo_width()
    h = self.winfo_height()
    glViewport(0, 0, w, h)

    # Clear the background and depth buffer.
    glClearColor(self.r_back, self.g_back, self.b_back, 0.)
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity()
    gluPerspective(self.fovy, float(w)/float(h), self.near, self.far)

    if 0:
      # Now translate the scene origin away from the world origin
      glMatrixMode(GL_MODELVIEW);
      mat = glGetDoublev(GL_MODELVIEW_MATRIX)
      glLoadIdentity()
      glTranslatef(-self.xcenter, -self.ycenter,
		  -(self.zcenter+self.distance))
      glMultMatrixd(mat)
    else:
      gluLookAt(self.xcenter, self.ycenter, self.zcenter + self.distance,
		 self.xcenter, self.ycenter, self.zcenter,
		 0., 1., 0.)
      glMatrixMode(GL_MODELVIEW);

    # Call objects redraw method.
    self.redraw(self)
    glFlush()				# Tidy up
    glPopMatrix()			# Restore the matrix

    self.tk.call(self._w, 'swapbuffers')

  def tkMap(self, *dummy):
    """Cause the opengl widget to redraw itself."""

    self.tkExpose()

  def tkExpose(self, *dummy):
    """Redraw the widget.
    Make it active, update tk events, call redraw procedure and
    swap the buffers.  Note: swapbuffers is clever enough to
    only swap double buffered visuals."""

    self.activate()
    if not self.initialised:
      self.basic_lighting()
      self.initialised = 1
    self.tkRedraw()


  def tkPrint(self, file):
    """Turn the current scene into PostScript via the feedback buffer."""

    self.activate()

    
