[Bio] / Speck / DotCanvas.py Repository:
ViewVC logotype

View of /Speck/DotCanvas.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.1.1.1 - (download) (as text) (annotate) (vendor branch)
Thu Sep 9 19:54:06 2004 UTC (15 years, 2 months ago) by sheradon
Branch: init, MAIN
CVS Tags: v0, HEAD
Changes since 1.1: +0 -0 lines
Initial load to CVS

import math, string, operator
from wxPython.wx			import *
from wxPython.glcanvas	import *
from OpenGL.GL				import *
from OpenGL.GLU			import *
from pydot					import Node, Edge, graph_from_dot_data

from Speck.SpeckCanvas	import SpeckCanvas
import Speck.PixFont        as pixFont


# -----------------------------
class DotCanvas(SpeckCanvas):
# -----------------------------
	"""OpenGL canvas that goes inside the renderWindow. Has all the event-handling
		and drawing functionality of the viewer.
	"""
	# -----------------------------
	def __init__(self, parent, data, **kwargs):
	# -----------------------------
		"""@type d: string
			@rtype: DotCanvas
		"""
		SpeckCanvas.__init__(self, parent, parent.GetSize(), **kwargs)

		# Some default colors. They can be overridden in the colors= keyword in constructor.
		self.colors = {'bg_bottom'	:	(0, 0.2, 0.5),
							'bg_top'		:	(0.08, 0, 0.3),
							'bg_border'	:	(0, 0.4, 0.5),
							'node'		:	(0, 0.7, 0),
							'edge'		:	(0, 0.6, 0),
							'title'		:	(1, 0.7, 0),
							'box'			:	(0, 0, 0.2),
							'box_border':	(0, 0.3, 0.6)
							}
		try:	# if we were passed colors={...} in keywords, store it
			for c in kwargs['colors'].keys(): self.colors[c] = kwargs['colors']
		except: pass

		# Set up initial view for this graph...
		self.dot = graph_from_dot_data(data)
		if self.dot.get_label():	self.dot.label = self.dot.get_label()
		else:								self.dot.label = '' 
		try: self.bb = map(string.atoi, d.get_bb().split(','))
		except:
			# Bounding box wasn't stored. Brute force it.
			nodes = self.dot.get_node_list()
			if nodes:
				N = len(nodes)
				positions = map(getattr, nodes, ['pos']*N)
				positions = filter(bool, positions)
				N = len(positions)
				positions = map(string.split, positions, [',']*N)
				xs, ys = zip(*positions)
				xs, ys = map(string.atoi, xs), map(string.atoi, ys)
				x1, y1, x2, y2 = min(xs), min(ys), max(xs), max(ys)
				self.bb = (x1-144, y1-144, x2+144, y2+144)
			else: self.bb = (0, 0, 16, 16)
		self.boxSize = (self.bb[2]-self.bb[0], self.bb[3]-self.bb[1])

		# Center on the center:
		self.center = ((self.bb[0]+self.bb[2])/2, (self.bb[1]+self.bb[3])/2)
		# Fit graph into window:
		if float(self.boxSize[0])	/ float(self.boxSize[1]) >= float(self.winSize[0])/ float(self.winSize[1]):
			self.zoom = float(self.boxSize[0]) / float(self.winSize[0])
		else:
			self.zoom = float(self.boxSize[1]) / float(self.winSize[1])
		self.dragging = False
		self.dragOrigin = (0, 0)
		self.overlayText = []

		# For searching nodes etc by location
		self.buildHashTable()

		# Mapping from color names to actual values:
		self.XColorMap = self.buildColorMap()

		# Display-list management:
		self.DL = 0
		self.needUpdate = True 

		self.Show(True)


	# FIXME: These hash table bits are hastily thrown together. With a little
	# thought they could be much better and cleaner.
	# ------------------
	def buildHashTable(self):
	# ------------------
		"""Build hash table for searching for nodes/edges by coordinates"""

		self.hash = [[] for i in range(self.hashFunc(self.bb[2], self.bb[3]-1)+1)]
		for n in self.dot.get_node_list():
			try: w, h = n.width, n.height
			except: continue 
			if not n.pos: continue
			[x, y] = map(string.atoi, n.pos.split(','))
			try: self.hash[ self.hashFunc(x, y) ].append(n)
			except: continue
		for e in self.dot.get_edge_list():
			try:
				[x, y] = map(string.atoi, string.split(e.lp, ','))
				self.hash[ self.hashFunc(x, y) ].append(e)
			except: continue

	# ----------------------
	def hashFunc(self, x, y):
	# ----------------------
		return int((y%self.bb[3]) / 100)
	# -------------------------
	def getThingAt(self, x, y):
	# -------------------------
		"""Finds a node or edge at the given coordinates using a hash table lookup
			@type x: float
			@type y: float
			@rtype: Node or Edge
		"""
		# For each nearby node (according to the hash table)...
		idx = self.hashFunc(x, y)
		for t in self.hash[idx]:
			if isinstance(t, Node):
				nx, ny = map(string.atoi, string.split(t.pos, ','))
				w, h = map(string.atof, [t.width, t.height])
				# Did we click within its rectangle?
				if x >= nx-w*36 and x <= nx+w*36 and y >= ny-h*36 and y <= ny+h*36:
					return t
			elif isinstance(t, Edge):
				ex, ey = map(string.atoi, string.split(t.lp, ','))
				if x >= ex-12 and x <= ex+12 and y >= ey-12 and y <= ey+12:
					return t
			else:
				return None

	# ----------------------
	def buildColorMap(self):
	# ----------------------
		"""Reads the X11 color definitions, because Graphviz uses the same ones
			to understand color names in DOT attributes.
			@rtype: {string: (float, float, float)}
		"""
		f = file('/usr/lib/X11/rgb.txt', 'r')
		cm = {}
		f.readline()
		while 1:
			line = f.readline()
			if not line: break
			rgb = line[:11].split()
			rgb = map(string.atoi, rgb)
			rgb = map(float, rgb)
			rgb = map(operator.__div__, rgb, [255.0]*3)
			name = line[12:].strip()
			cm[name] = rgb
		f.close()
		return cm			

	# -------------------------
	def OnKeyDown(self, event):
	# -------------------------
		"""Keyboard handling. Only does zooming right now (+ and -).
			@type event: wxKeyEvent
			@rtype: None
		"""

		key = event.GetKeyCode()
		# + zooms in:
		if key == 61:
			self.zoom = self.zoom / 1.3
			self.OnPaint()
		# - zooms out:
		elif key == 45:
			self.zoom = self.zoom * 1.3
			self.OnPaint()

	# --------------------------
	def OnRightDown(self, event):
	# --------------------------
		"""Start dragging a zoom-box
			@type event: wxMouseEvent
			@rtype: None
		"""
		self.dragging = True
		scaleX = float(self.winSize[0])/float(self.GetSize()[0])
		scaleY = float(self.winSize[1])/float(self.GetSize()[1])
		self.dragOrigin = (scaleX*event.GetX(), scaleY*(self.GetSize()[1]-event.GetY()))

	# ------------------------
	def OnRightUp(self, event):
	# ------------------------
		"""Change the view. Finish dragging around a zoom-area, or just recenter
			@type event: wxMouseEvent
			@rtype: None
		"""
		upCoords = self.cursor
		scaleX = float(self.winSize[0])/float(self.GetSize()[0])
		scaleY = float(self.winSize[1])/float(self.GetSize()[1])
		# Convert screen-coords to world-coords...
		corner1 = gluUnProject(upCoords[0]/scaleX, upCoords[1]/scaleY, 0,
         glGetDoublev(GL_MODELVIEW_MATRIX), glGetDoublev(GL_PROJECTION_MATRIX), glGetIntegerv(GL_VIEWPORT))
		corner2 = gluUnProject(self.dragOrigin[0]/scaleX, self.dragOrigin[1]/scaleY, 0,
         glGetDoublev(GL_MODELVIEW_MATRIX), glGetDoublev(GL_PROJECTION_MATRIX), glGetIntegerv(GL_VIEWPORT))
		boxSize = ( abs(corner1[0]-corner2[0]), abs(corner1[1]-corner2[1]) )
		# If we've dragged any appreciable amount (say 30px), consider it a zoom-to box
		if boxSize[0] > 30*self.zoom and boxSize[1] > 30*self.zoom:
			if float(boxSize[0])	/ float(boxSize[1]) >= float(self.winSize[0])/ float(self.winSize[1]):
				self.zoom = float(boxSize[0]) / float(self.winSize[0])
			else:
				self.zoom = float(boxSize[1]) / float(self.winSize[1])
			self.center = ( (corner1[0]+corner2[0])/2, (corner1[1]+corner2[1])/2 )
		else:
			self.center = corner1
		self.dragging = False
		self.OnPaint()

	# -----------------------
	def OnMotion(self, event):
	# -----------------------
		"""Since with Chromium we don't see a cursor over the canvas, we need to draw it ourselves.
			Also if we're dragging, need to show rubberband box in real time
			@type event: wxMouseEvent
			@rtype: None
		"""
		SpeckCanvas.OnMotion(self, event)
		if self.dragging:
			self.OnPaint()

	# ------------------------------
	def OnPaint(self, event = None):
	# ------------------------------
		"""The window painting callback. Main rendering function. Handles display lists
			and calls out to the subroutines to draw various parts of the graph picture.
			@rtype: None
		"""
		if self.zoom < 0.25: self.zoom = 0.25		# Don't get TOO close or it can crash X
		elif self.zoom * self.winSize[0] > self.boxSize[0] * 2.5: 
			self.zoom = 2.5 * float(self.boxSize[0]) / float(self.winSize[0])
		self.SetCurrent()
		dc = wxPaintDC(self)
		glMatrixMode(GL_PROJECTION)
		glLoadIdentity()
		glOrtho(	self.center[0] - self.zoom * self.winSize[0] / 2,
					self.center[0] + self.zoom * self.winSize[0] / 2,
					self.center[1] - self.zoom * self.winSize[1] / 2,
					self.center[1] + self.zoom * self.winSize[1] / 2, -1, 100)

		glClearColor(0, 0, 0, 1)
		glClear(GL_COLOR_BUFFER_BIT)
		glEnable(GL_BLEND)
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
		glShadeModel(GL_SMOOTH)
		glEnable(GL_POINT_SMOOTH)
		glEnable(GL_LINE_SMOOTH)
		# Put this here so it gets updated @ diff zooms:
		glPointSize(6.0 / self.zoom)
		glLineWidth(2.0 / self.zoom)
		glLineStipple(int(3.0 / self.zoom), 0x3333)

		# Ensure display lists exist and we're using the right one:
		if self.DL == 0: self.DL = glGenLists(1)
		if self.DL == 0: return

		# Only do computationally intensive calculations if something's changed.
		if self.needUpdate:
			wxBeginBusyCursor()
			glNewList(self.DL, GL_COMPILE)
			self.drawBox()
			self.drawGraph(self.dot)
			glEndList()
			wxEndBusyCursor()
			self.needUpdate = False
		glCallList(self.DL)

		self.drawOverlay()
		# If on chromium, we need to draw a mouse cursor.
		if self.cr: self.drawCursor()

		glFlush()
		self.SwapBuffers()





	# ----------------
	def drawBox(self):
	# ----------------
		"""A nice background.
			@rtype: None
		"""
		center = ( (self.bb[2]+self.bb[0])/2, (self.bb[3]+self.bb[1])/2 )
		radius = max(self.boxSize[0], self.boxSize[1]) * 5
		t = 1
		theta, x, y = 0.0, center[0]+radius, center[1]
		while theta < 361:
			x_last, y_last = x, y
			theta += 22.5 
			radians = math.pi * theta / 180.0
			x, y = radius*math.cos(radians), radius*math.sin(radians)	
			x, y = center[0]+x, center[1]+y
			glBegin(GL_TRIANGLES)
			glColor3f(0.1*t, 0, 0.25)
			glVertex2f(center[0], center[1])
			glColor3f(0, 0, 0)
			glVertex2f(x_last, y_last)
			glVertex2f(x, y)
			glEnd()
			t = not t

		try:
			r1, g1, b1 = self.XColorMap[ self.dot.get_bgcolor() ]
			r2, g2, b2 = r1*0.8, g1*0.82, b1*0.85
		except:
			r1, g1, b1 = self.colors['bg_bottom']
			r2, g2, b2 = self.colors['bg_top']
		glBegin(GL_QUADS)
		glColor3f(r1, g1, b1)
		glVertex2i(self.bb[0], self.bb[1])
		glVertex2i(self.bb[2], self.bb[1])
		glColor3f(r2, g2, b2)
		glVertex2i(self.bb[2], self.bb[3])
		glVertex2i(self.bb[0], self.bb[3])
		glEnd()
		# An outline
		r, g, b = self.colors['bg_border']
		glColor3f(r, g, b)
		glBegin(GL_LINE_STRIP)
		glVertex2i(self.bb[0], self.bb[1])
		glVertex2i(self.bb[2], self.bb[1])
		glVertex2i(self.bb[2], self.bb[3])
		glVertex2i(self.bb[0], self.bb[3])
		glVertex2i(self.bb[0], self.bb[1])
		glEnd()
		

	# ---------------------
	def drawOverlay(self):
	# ---------------------
		"""Displays the label/title of the recoElem, and the overlayText
			at the bottom if there is any.
			@rtype: None
		"""

		# Setup coords to be pixely instead of worldly
		glMatrixMode(GL_PROJECTION)
		glPushMatrix()
		glLoadIdentity()
		glOrtho(0, self.winSize[0], 0, self.winSize[1], -1, 1)

		# If we're dragging, show a box to show proposed zoom area
		if self.dragging:
			self.drawTransBox(self.dragOrigin[0], self.dragOrigin[1],
									self.cursor[0], self.cursor[1])
		# Set title height, and # of overlayText lines:
		# Nice to stretch across window, but don't let it take up THAT much space.
		titleHeight = 0.7 * self.winSize[0] / (len(self.dot.label)+1)
		titleHeight = min(titleHeight, self.winSize[1]/16)
		# And height (px) of the text at the bottom:
		height = 5 * max(1, int(self.winSize[1] / 300))
		lines = len(self.overlayText)
		# Draw background of title:
		if self.dot.label: self.drawTransBox(8, self.winSize[1]-8, self.winSize[0]-8, self.winSize[1]-24-titleHeight)
		if lines:
			# Draw background-box at bottom
			self.drawTransBox(8, 8, self.winSize[0]-8, 24 + 1.7*height*lines - 0.7*height)
		r, g, b = self.colors['title']
		glColor3f(r, g, b)
		# recoElem title:
		if self.dot.label: pixFont.drawString(self.dot.label, self.winSize[0]/2,
								self.winSize[1]-titleHeight-16, titleHeight, 'center')
		if lines:
			# The overlayText:
			for i, line in enumerate(self.overlayText):
				pixFont.drawString(line, 16, 16+1.7*height*i, height)
		glPopMatrix()

	# -------------------------------------
	def drawTransBox(self, x1, y1, x2, y2):
	# -------------------------------------
		"""Draws a translucent box with corners at (x1, y1) and (x2, y2).
			Used for textbox-background and zoom-box-dragging.
			@type x1, y1, x2, y2: float
			@rtype: None
		"""
		r, g, b = self.colors['box']
		glColor4f(r, g, b, 0.7)
		glRectf(x1, y1, x2, y2)
		r, g, b = self.colors['box_border']
		glColor3f(r, g, b)
		glLineWidth(1.0)
		glBegin(GL_LINE_STRIP)
		glVertex2f(x1, y1)
		glVertex2f(x2, y1)
		glVertex2f(x2, y2)
		glVertex2f(x1, y2)
		glVertex2f(x1, y1)
		glEnd()



	# ---------------------
	def drawGraph(self, g):
	# ---------------------
		"""Calls edge, node, & subgraph routines for this Dot/Graph object.
			@type g: Dot or Graph
			@rtype: None
		"""
		self.drawEdges(g)
		self.drawNodes(g)
		for s in g.get_subgraph_list():
			self.drawGraph(s)

	# ---------------------
	def drawNodes(self, g):
	# ---------------------
		"""Draws each Node in the graph.
			@type g: Dot or Graph
			@rtype: None
		"""
		for n in g.get_node_list():
			if n.style == 'invis': continue		# Don't draw invisible nodes
			if not n.pos: continue
			pos = map(string.atoi, string.split(n.pos, ','))
			try: r, g, b = self.XColorMap[n.get_color()]
			except: r, g, b = self.colors['node'] 
			try:
				# DOT uses a 72 point-per-inch conversion scale
				size = (string.atof(n.width) * 71, string.atof(n.height) * 71)
			except: # Thing has no size... draw a label there, guessing size
				size = (0, 0)
				 
			if n.shape == 'point':
				glColor3f(r, g, b)
				glBegin(GL_POINTS)
				glVertex2f(pos[0], pos[1])
				glEnd()
			elif n.shape == 'plaintext' or size == (0, 0):
				glColor3f(r, g, b)
			else:
				glColor3f(r, g, b)
				glRectf(pos[0]-size[0]/2.0, pos[1]-size[1]/2.0, pos[0]+size[0]/2.0, pos[1]+size[1]/2.0)
				glColor3f(0, 0, 0)
			if not n.label: continue
			nLines = len(string.split(n.label, r'\n'))
			if size == (0, 0): size = (24.0, 12.0)
			pixFont.drawString(n.label, pos[0], pos[1], 0.5*size[1]/nLines, align='center', valign='center')
				


	# ---------------------
	def drawEdges(self, g):
	# ---------------------
		"""Draws each Edge in the graph... splines, arrowheads, & label.
			@type g: Dot
			@rtype: None
		"""
		for e in g.get_edge_list():
			try: posstr = string.split(e.pos, ' ')
			except AttributeError: continue
			pos = []
			startArrowpos, endArrowpos = None, None
			for p in posstr:
				cs = string.split(p, ',')
				if p[0] == 's': startArrowpos = (string.atof(cs[1]), string.atof(cs[2]))
				elif p[0] == 'e': endArrowpos = (string.atof(cs[1]), string.atof(cs[2]))
				else: pos.append( (string.atof(cs[0]), string.atof(cs[1])) )

			if startArrowpos: pos.insert(0, startArrowpos)
			if endArrowpos: pos.append(endArrowpos)
		
			# Draw line
			try: r, g, b = self.XColorMap[e.get_color()]
			except: r, g, b = self.colors['edge'] 
			glColor3f(r, g, b)
			if e.get_style(): style = e.get_style()
			else: style = 'solid'
			if style == 'dotted' or style == 'dashed': glEnable(GL_LINE_STIPPLE)
			if not style == 'invis':
				glBegin(GL_LINE_STRIP)
				for p in self.bezier(pos):
					glVertex2f(p[0], p[1])
				glEnd()
			glDisable(GL_LINE_STIPPLE)
			
			if startArrowpos: self.drawArrowhead(pos[1], pos[0])
			if endArrowpos:   self.drawArrowhead(pos[-2], pos[-1])

			# Reaction label:
			if e.lp:
				lps = string.split(e.lp, ',')
				lp = (string.atoi(lps[0]), string.atoi(lps[1]))
				pixFont.drawString(e.label, lp[0], lp[1], 10, 'center') 


	# --------------------------------------
	def drawArrowhead(self, source, target):
	# --------------------------------------
		"""On an edge, we need to draw arrows to show direction. This draws a triangle
			at target, pointed away from source.
			@type source: (float, float)
			@type target: (float, float)
			@rtype: None
		"""
		if source[0] == target[0] and source[1] == target[1]: return

		lineVec = (target[0]-source[0], target[1]-source[1])
		lineLen = math.sqrt( lineVec[0]*lineVec[0] + lineVec[1]*lineVec[1] )
		lineDir = (lineVec[0]/lineLen, lineVec[1]/lineLen)
		basePoint = self.lerp(target, source, 12.0/lineLen)
		normalVec = (-4.0*lineDir[1], 4.0*lineDir[0])
		
		glBegin(GL_TRIANGLES)
		glVertex2f(target[0]+2.0*lineDir[0], target[1]+2.0*lineDir[1])
		glVertex2f(basePoint[0]-normalVec[0], basePoint[1]-normalVec[1])
		glVertex2f(basePoint[0]+normalVec[0], basePoint[1]+normalVec[1])
		glEnd()


	# -----------------------------------
	def bezier(self, original, steps=12):
	# -----------------------------------
		"""Given a list of control points, calculates intermediate coordinates on the
			corresponding cubic Bezier spline.
			@type original: [ (float, float) ]
			@type steps: integer
			@rtype: [ (float, float) ]
		"""

		a, c = original[0], original[-1]
		for b in original[1:-1]:
			# If there's anything out of line, move on to rest of spline-generation.
			if (b[0]-a[0]) * (c[1]-a[1]) != (b[1]-a[1]) * (c[0]-a[0]): break
		else:
			# They were all in a line, so just simplify to 2 points.
			return [original[0], original[-1]]

		control = original[:]	# Copy point-list.
		# Must be even length for this algorithm. Make sure:
		if len(control) % 2 == 1: control.insert(1, self.lerp(control[0], control[1]))

		# Insert intermediate between every 2 steps so that we can have 4 ctlpoints each time
		for i in range(len(control)-4, 1, -2):
			control.insert(i+1, self.lerp(control[i], control[i+1]))

		# Calculate each point by deCasteljau's algorithm
		bez = [control[0]]
		lerp = self.lerp
		for start in range(0, len(control)-3, 3):
			[a, b, c, d] = control[start:start+4]
			for i in range(1, steps+1):
				t = float(i) / float(steps)
				ab = lerp (a,b,t);           # point between a and b (green)
				bc = lerp (b,c,t);           # point between b and c (green)
				cd = lerp (c,d,t);           # point between c and d (green)
				abbc = lerp (ab,bc,t);       # point between ab and bc (blue)
				bccd = lerp (bc,cd,t);       # point between bc and cd (blue)
				dest = lerp (abbc,bccd,t);   # point on the bezier-curve (black)
				bez.append(dest)
		return bez


MCS Webmaster
ViewVC Help
Powered by ViewVC 1.0.3