# --- UCSF Chimera Copyright ---
# Copyright (c) 2000-2006 Regents of the University of California.
# All rights reserved. This software provided pursuant to a
# license agreement containing restrictions on its disclosure,
# duplication and use. This notice must be embedded in or
# attached to all copies, including partial copies, of the
# software or any revisions or derivations thereof.
# --- UCSF Chimera Copyright ---
#
# $Id: ColorKey.py 29408 2009-11-23 19:54:11Z gregc $
import chimera
from math import sqrt
from SimpleSession import SAVE_SESSION
class Arrow(object):
HEAD_SOLID = "solid"
HEAD_POINTY = "pointy"
HEAD_BLOCKY = "blocky"
HEAD_POINTER = "pointer"
headStyles = [HEAD_SOLID, HEAD_POINTY, HEAD_BLOCKY, HEAD_POINTER]
HEAD, MIDDLE, TAIL = range(3)
STD_HALF_WIDTH = 0.01
def __init__(self, start, end, color="white", weight=1.0, head=HEAD_SOLID,
shown=True, ident=None):
self._start = start
self._end = end
self.ident = ident
self._shown = shown
self._weight = weight
self._head = head
if isinstance(color, basestring):
try:
color = chimera.Color.lookup(color)
except KeyError:
raise chimera.UserError("No such color: '%s'" % color)
self.color = color
self._opacity = 1.0
def getRestoreInfo(self):
return (self.start, self.end), {
'ident': self.ident,
'shown': self.shown,
'weight': self.weight,
'head': self.head,
'color': self.color
}
def __str__(self):
return u"%.2f,%.2f \N{RIGHTWARDS ARROW} %.2f,%.2f" % (
self.start[0], self.start[1], self.end[0], self.end[1])
posString = property(__str__)
def getColor(self):
return self.rgba
def setColor(self, color):
if hasattr(color, 'rgba'):
rgba = color.rgba()
else:
rgba = color
if len(rgba) == 3:
rgba = rgba + (1.0,)
self.rgba = rgba
ArrowsModel().setMajorChange()
color = property(getColor, setColor)
def getEnd(self):
return self._end
def setEnd(self, end):
if self._end == end:
return
self._end = end
ArrowsModel().setMajorChange()
end = property(getEnd, setEnd)
def getHead(self):
return self._head
def setHead(self, head):
if head == self._head:
return
if head not in self.headStyles:
raise ValueError("Arrowhead style must be one of: %s"
% ", ".join(self.headStyles))
self._head = head
ArrowsModel().setMajorChange()
head = property(getHead, setHead)
def getOpacity(self):
return self._opacity
def setOpacity(self, opacity):
if opacity == self._opacity:
return
self._opacity = opacity
ArrowsModel().setMajorChange()
opacity = property(getOpacity, setOpacity)
def getShown(self):
return self._shown
def setShown(self, shown):
if self._shown == shown:
return
self._shown = shown
ArrowsModel().setMajorChange()
shown = property(getShown, setShown)
def getStart(self):
return self._start
def setStart(self, start):
if self._start == start:
return
self._start = start
ArrowsModel().setMajorChange()
start = property(getStart, setStart)
def getWeight(self):
return self._weight
def setWeight(self, weight):
try:
weight = float(weight)
except ValueError:
raise ValueError("Arrow weight must be numeric")
if weight == self._weight:
return
self._weight = weight
ArrowsModel().setMajorChange()
weight = property(getWeight, setWeight)
def animInterp(self, startData, where, endData):
if startData is None:
ekw = endData[-1]
self.opacity = where
self.shown = ekw["shown"] and not where == 0.0
return
if endData is None:
skw = startData[-1]
self.opacity = (1.0 - where)
self.shown = skw["shown"] and not where == 1.0
return
sargs, skw = startData
eargs, ekw = endData
self.shown = skw["shown"] or ekw["shown"]
if not self.shown:
return
sopacity = eopacity = 1.0
if not skw["shown"]:
sopacity = 0.0
if not ekw["shown"]:
eopacity = 0.0
self.opacity = sopacity + where * (eopacity - sopacity)
sstart, send = sargs
estart, eend = eargs
self.start = tuple([sstart[i] + where * (estart[i] - sstart[i])
for i in range(2)])
self.end = tuple([send[i] + where * (eend[i] - send[i])
for i in range(2)])
if where < 0.5:
self.head = skw["head"]
else:
self.head = ekw["head"]
if where < 0.5:
self.weight = skw["weight"]
else:
self.weight = ekw["weight"]
def triangleStrips(self, width, height):
start, end = self.start, self.end
sx, sy = start[0] * width, start[1] * height
ex, ey = end[0] * width, end[1] * height
scaleFactor = min(width, height)
v = (ex - sx, ey - sy)
arrowLen2 = v[0] * v[0] + v[1] * v[1]
arrowLen = sqrt(arrowLen2)
nv = (v[0]/arrowLen, v[1]/arrowLen)
perpv = (nv[1], -nv[0])
# half base
halfWidth = scaleFactor * self.STD_HALF_WIDTH * self.weight
ext1, ext2 = perpv[0] * halfWidth, perpv[1] * halfWidth
base1, base2 = (sx + ext1, sy + ext2), (sx - ext1, sy - ext2)
# inside arrowhead
x1, y1 = base1
arrowHeadWidth = 4 * halfWidth
cutBack = arrowLen - 2 * halfWidth
extx, exty = nv[0] * cutBack, nv[1] * cutBack
x2, y2 = base2
inside1, inside2 = (x1 + extx, y1 + exty), (x2 + extx, y2 + exty)
# arrowhead edge
backLen = arrowLen - arrowHeadWidth
ext1, ext2 = perpv[0] * arrowHeadWidth, perpv[1] * arrowHeadWidth
base = sx + nv[0] * backLen, sy + nv[1] * backLen
edge1, edge2 = (base[0] + ext1, base[1] + ext2), (base[0] - ext1, base[1] - ext2)
# tip
tip = (ex, ey)
# avoid overlapping triangles for "solid" arrowhead
proj = nv[0] * (edge1[0] - base1[0]) + nv[1] * (edge1[1] - base1[1])
solidInside1 = (base1[0] + proj * nv[0], base1[1] + proj * nv[1])
solidInside2 = (base2[0] + proj * nv[0], base2[1] + proj * nv[1])
# clockwise vs. anti-clockwise order matters
if self.head == self.HEAD_SOLID:
return [[solidInside1, solidInside2, base1, base2],
[tip, edge2, edge1]]
elif self.head == self.HEAD_POINTY:
return [[edge1, tip, inside1, inside2, base1, base2],
[tip, edge2, inside2]]
elif self.head == self.HEAD_POINTER:
return [[inside1, inside2, base1, base2],
[tip, inside2, inside1]]
# HEAD_BLOCKY
# avoid triangle overlaps so that fading looks good...
root2 = sqrt(2.0)
flangeWidth = 1.5 * halfWidth
fwRoot2 = flangeWidth / root2
v1 = (fwRoot2 * (-nv[0] - perpv[0]), fwRoot2 * (-nv[1] - perpv[1]))
v2 = (fwRoot2 * (perpv[0] - nv[0]), fwRoot2 * (perpv[1] - nv[1]))
innerTip = (tip[0] - 2 * fwRoot2 * nv[0], tip[1] - 2 * fwRoot2 * nv[1])
flange1 = edge1[0] + v1[0], edge1[1] + v1[1]
flange2 = edge2[0] + v2[0], edge2[1] + v2[1]
innerFlange1 = (innerTip[0] + halfWidth * (-nv[0] + perpv[0]),
innerTip[1] + halfWidth * (-nv[1] + perpv[1]))
innerFlange2 = (innerTip[0] + halfWidth * (-nv[0] - perpv[0]),
innerTip[1] + halfWidth * (-nv[1] - perpv[1]))
return [[innerTip, innerFlange2, innerFlange1, base2, base1],
[innerTip, flange1, tip, edge1],
[innerTip, tip, flange2, edge2]]
def within(self, pos, slop):
def dot(a, b):
return a[0]*b[0] + a[1]*b[1]
def distSq(a, b):
dx = a[0] - b[0]
dy = a[1] - b[1]
return dx*dx + dy*dy
def diff(x, y):
return (x[0]-y[0], x[1]-y[1])
# solve for nearest point on arrow in parameterized form
v = diff(self.end, self.start)
if v == (0, 0):
if distSq(pos, self.start) < slop * slop:
return True, self.HEAD, diff(self.end, pos)
return False, None, None
near = dot(v, diff(pos, self.start)) / dot(v, v)
if near > 1:
if distSq(pos, self.end) < slop * slop:
return True, self.HEAD, diff(self.end, pos)
return False, None, None
if near < 0:
if distSq(pos, self.start) < slop * slop:
return True, self.TAIL, diff(self.start, pos)
return False, None, None
nearPt = (self.start[0] + near*v[0], self.start[1] + near*v[1])
testWidth = self.weight * self.STD_HALF_WIDTH + slop
if distSq(pos, nearPt) < testWidth * testWidth:
if near <= 0.25:
return True, self.TAIL, diff(self.start, pos)
if near >= 0.75:
return True, self.HEAD, diff(self.start, pos)
return True, self.MIDDLE, diff((self.start[0]+self.end[0]/2.0,
self.start[1]+self.end[1]/2.0), pos)
return False, None, None
from PythonModel.PythonModel import PythonModel
class _ArrowsModel(PythonModel):
def __init__(self):
PythonModel.__init__(self)
self.arrows = []
chimera.openModels.add([self], baseId=-1, hidden=True)
self._handlerIDs = {}
for trigName, handler in [(SAVE_SESSION, self._saveSession),
(chimera.SCENE_TOOL_SAVE, self._saveScene)]:
self._handlerIDs[trigName] = chimera.triggers.addHandler(trigName, handler, None)
def addArrow(self, *args, **kw):
if kw.get('ident', None) is None:
idents = set([a.ident for a in self.arrows])
num = 1
while ("anon_arrow_%d" % num) in idents:
num += 1
from copy import copy
kw = copy(kw)
kw['ident'] = "anon_arrow_%d" % num
self.arrows.append(Arrow(*args, **kw))
self.setMajorChange()
return self.arrows[-1]
def removeArrow(self, arrow):
self.arrows.remove(arrow)
self.setMajorChange()
def getRestoreInfo(self):
return {'arrows': [a.getRestoreInfo() for a in self.arrows]}
def restore(self, info):
self.arrows = []
if info is not None:
for args, kw in info['arrows']:
if isinstance(kw.get('weight', 1.0), basestring):
kw['weight'] = {
'thin': 0.5,
'medium': 1.0,
'thick': 1.5
}.get(kw['weight'], 1.0)
self.addArrow(*args, **kw)
self.setMajorChange()
def destroy(self, *args):
for trigName, handlerID in self._handlerIDs.items():
chimera.triggers.deleteHandler(trigName, handlerID)
PythonModel.destroy(self, True)
global _arrowsModel
_arrowsModel = None
def pickArrow(self, pos, w, h):
for arrow in self.arrows:
if not arrow.shown:
continue
within, part, delta = arrow.within(pos, 0.005)
if within:
break
else:
arrow = None
delta = (0, 0)
part = Arrow.HEAD
return arrow, part, delta
def computeBounds(self, sphere, bbox):
return False
def validXform(self):
return False
def draw(self, lens, viewer, passType):
if passType != chimera.LensViewer.Overlay2d:
return
self._draw("opengl", viewer)
def _draw(self, mode, viewer):
w, h = viewer.windowSize
openGL = mode == "opengl"
if openGL:
from OpenGL import GL
else:
# X3D mostly cribbed from ILabelModel's x3dWrite
prefix = self.x3dPrefix
output = self.x3dOutput
TRANSCALE = "\n"
ENDTRANS = "\n"
OVERLAY = "\n"
# translate to hither plane and scale to match pixels
cam = viewer.camera
view = 0
eyePos = cam.eyePos(view)
left, right, bottom, top, hither, yon, focal = \
cam.window(view)
scale = (right - left) / w
xlate_scale = (eyePos[0] + left, eyePos[1] + bottom,
eyePos[2] - hither - 0.0001, scale, scale, 1)
output.extend([ prefix, OVERLAY,
prefix, TRANSCALE % xlate_scale ])
if openGL:
GL.glPushMatrix()
for arrow in self.arrows:
if arrow.shown:
if arrow.opacity < 1.0:
color = arrow.color[:3] + (arrow.opacity * arrow.color[3],)
else:
color = arrow.color
self._layoutTriangleStrips(openGL, arrow.triangleStrips(w, h), color)
if openGL:
GL.glPopMatrix()
else:
output.extend([prefix, ENDTRANS])
def x3dNeeds(self, scene):
if not self.arrows:
return
# for TriangleStripSet
scene.needComponent(chimera.X3DScene.Rendering, 3)
# for ColorRGBA
scene.needComponent(chimera.X3DScene.Rendering, 4)
def x3dWrite(self, indent, scene):
if not self.arrows:
return
self.x3dPrefix = " " * indent
self.x3dOutput = []
self._draw("x3d", chimera.viewer)
return ''.join(self.x3dOutput)
def _animStart(self, transition):
pass
def _animStep(self, startData, transition):
animMapping, startIDs, endIDs = self._establishAnimMapping(startData, transition)
where = transition.frameCount / float(transition.frames)
endData = transition.scene().tool_settings.get("2D Labels (arrows)", None)
for arrow, change in animMapping.items():
start, end = change
if start is not None:
start = startIDs[start]
if end is not None:
end = endIDs[end]
arrow.animInterp(start, where, end)
def _animFinish(self, transition):
endData = transition.scene().tool_settings.get("2D Labels (arrows)", None)
if endData:
endIDs = set(self._setupAnimIDs(endData["arrows"]).keys())
removals = []
for arrow in self.arrows:
if arrow.ident not in endIDs:
removals.append(arrow)
for arrow in removals:
self.removeArrow(arrow)
for arrow in self.arrows:
arrow.opacity = 1.0
def _establishAnimMapping(self, startData, transition):
animMapping = startData.setdefault('animMapping', {})
# arrows in old scenes can have ID None; allow for that
startIDs = self._setupAnimIDs(startData["arrows"])
endData = transition.scene().tool_settings.get("2D Labels (arrows)", None)
if endData:
endIDs = self._setupAnimIDs(endData["arrows"])
else:
endIDs = {}
if None in startIDs or None in endIDs:
startIDs, endIDs = self._rectifyForIDNone(startData["arrows"], endData["arrows"])
deletions = []
for arrow in self.arrows:
if arrow.ident not in startIDs and arrow.ident not in endIDs:
deletions.append(arrow)
for arrow in deletions:
self.removeArrow(arrow)
nearStart = transition.frameCount < 0.5 * transition.frames
arrowMap = dict([(a.ident, a) for a in self.arrows])
for startID, arrowData in startIDs.items():
if startID in endIDs:
change = (startID, startID)
else:
change = (startID, None)
try:
arrow = arrowMap[startID]
except KeyError:
if startID in endIDs and not nearStart:
arrowData = endIDs[startID]
args, kw = arrowData
arrow = Arrow(*args, **kw)
self.arrows.append(arrow)
animMapping[arrow] = change
for endID, arrowData in endIDs.items():
if endID in startIDs:
continue
try:
arrow = arrowMap[endID]
except KeyError:
args, kw = arrowData
arrow = Arrow(*args, **kw)
self.arrows.append(arrow)
animMapping[arrow] = (None, endID)
return animMapping, startIDs, endIDs
def _layoutTriangleStrips(self, openGL, triangleStrips, color):
if openGL:
from OpenGL import GL
GL.glColor4f(*tuple(color))
for ts in triangleStrips:
GL.glBegin(GL.GL_TRIANGLE_STRIP)
for x, y in ts:
GL.glVertex2f(x, y)
GL.glEnd()
else:
output = self.x3dOutput
prefix = self.x3dPrefix
SHAPE = "\n"
ENDSHAPE = "\n"
APPEAR = "\n"
ENDAPPEAR = "\n"
TRIANGLESTRIP = "\n"
ENDTRIANGLESTRIP = "\n"
COORD = "\n"
MATTRANS = "\n"
rgbt = color[0:3] + (1 - color[3],)
output.extend([
prefix, ' ', SHAPE,
prefix, ' ', APPEAR,
prefix, ' ', MATTRANS % rgbt,
prefix, ' ', ENDAPPEAR])
vertices = []
for ts in triangleStrips:
vertices.extend(list(ts))
output.extend([prefix, ' ', TRIANGLESTRIP % " ".join(
[str(len(ts)) for ts in triangleStrips])])
output.extend([prefix, ' ', COORD])
for x, y in vertices:
output.extend(["%g " % c for c in [x, y, 0]])
output.extend([prefix, ' ', ENDCOORD])
output.extend([prefix, ' ', ENDTRIANGLESTRIP])
output.extend([prefix, ' ', ENDSHAPE])
def _restoreScene(self, scene):
self.restore(scene.tool_settings.get('2D Labels (arrows)', None))
def _saveScene(self, trigName, myData, scene):
scene.tool_settings['2D Labels (arrows)'] = self.getRestoreInfo()
def _saveSession(self, triggerName, myData, sessionFile):
print>>sessionFile, """
try:
from Ilabel.Arrows import ArrowsModel
ArrowsModel().restore(%s)
except:
reportRestoreError("Error restoring 2D arrows in session")
""" % repr(self.getRestoreInfo())
def _setupAnimIDs(self, arrowData):
mapping = {}
num = 1
for args, kw in arrowData:
if kw['ident'] is None:
start, end = args
from copy import copy
kw = copy(kw)
for a in self.arrows:
if a.start == start and a.end == end:
kw['ident'] = a.ident
break
else:
kw['ident'] = 'anim_%d_%d' % (num, id(arrowData))
num += 1
mapping[kw['ident']] = (args, kw)
return mapping
_arrowsModel = None
def ArrowsModel(create=True):
global _arrowsModel
if not _arrowsModel:
if not create:
return None
_arrowsModel = _ArrowsModel()
return _arrowsModel