1from __future__ import absolute_import 2# #START_LICENSE########################################################### 3# 4# 5# This file is part of the Environment for Tree Exploration program 6# (ETE). http://etetoolkit.org 7# 8# ETE is free software: you can redistribute it and/or modify it 9# under the terms of the GNU General Public License as published by 10# the Free Software Foundation, either version 3 of the License, or 11# (at your option) any later version. 12# 13# ETE is distributed in the hope that it will be useful, but WITHOUT 14# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 15# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 16# License for more details. 17# 18# You should have received a copy of the GNU General Public License 19# along with ETE. If not, see <http://www.gnu.org/licenses/>. 20# 21# 22# ABOUT THE ETE PACKAGE 23# ===================== 24# 25# ETE is distributed under the GPL copyleft license (2008-2015). 26# 27# If you make use of ETE in published work, please cite: 28# 29# Jaime Huerta-Cepas, Joaquin Dopazo and Toni Gabaldon. 30# ETE: a python Environment for Tree Exploration. Jaime BMC 31# Bioinformatics 2010,:24doi:10.1186/1471-2105-11-24 32# 33# Note that extra references to the specific methods implemented in 34# the toolkit may be available in the documentation. 35# 36# More info at http://etetoolkit.org. Contact: huerta@embl.de 37# 38# 39# #END_LICENSE############################################################# 40import random 41from .qt import QRectF, QGraphicsSimpleTextItem, QGraphicsPixmapItem, \ 42 QGraphicsRectItem, QTransform, QBrush, QPen, QColor, QGraphicsItem 43 44from .main import FACE_POSITIONS, _leaf 45from .node_gui_actions import _NodeActions as _ActionDelegator 46from math import pi, cos, sin 47import six 48from six.moves import range 49 50class _TextFaceItem(QGraphicsSimpleTextItem, _ActionDelegator): 51 def __init__(self, face, node, text): 52 QGraphicsSimpleTextItem.__init__(self, text) 53 _ActionDelegator.__init__(self) 54 self.node = node 55 self.face = face 56 self._bounding_rect = self.face.get_bounding_rect() 57 self._real_rect = self.face.get_real_rect() 58 def boundingRect(self): 59 return self._bounding_rect 60 61class _ImgFaceItem(QGraphicsPixmapItem, _ActionDelegator): 62 def __init__(self, face, node, pixmap): 63 QGraphicsPixmapItem.__init__(self, pixmap) 64 _ActionDelegator.__init__(self) 65 self.node = node 66 67class _BackgroundFaceItem(QGraphicsRectItem): 68 def __init__(self, face, node): 69 QGraphicsRectItem.__init__(self) 70 self.node = node 71 72 def paint(self, painter, option, index): 73 return 74 75class _FaceGroupItem(QGraphicsRectItem): # I was about to name this FaceBookItem :) 76 def paint(self, painter, option, index): 77 "Avoid little dots in acrobat reader" 78 return 79 80 def __init__(self, faces, node, as_grid=False): 81 # This caused seg. faults. in some computers. No idea why. 82 QGraphicsRectItem.__init__(self, 0, 0, 0, 0) 83 84 self.as_grid = as_grid 85 self.c2max_w = {} 86 self.r2max_h = {} 87 self.node = node 88 self.column2faces = faces 89 self.column2size = {} 90 self.columns = sorted(set(self.column2faces.keys())) 91 92 # Two dictionaries containing min column size. Can be used to 93 # reserve some space to specific columns and draw FaceBlocks 94 # like tables. 95 self.column_widths = {} 96 self.row_heights = {} 97 98 self.w = 0 99 self.h = 0 100 # updates the size of this grid 101 self.update_columns_size() 102 103 def set_min_column_widths(self, column_widths): 104 # column_widths is a dictionary of min column size. Can be 105 # used to reserve horizontal space to specific columns 106 self.column_widths = column_widths 107 self.columns = sorted(set(list(self.column2faces.keys()) + list(self.column_widths.keys()))) 108 109 def set_min_column_heights(self, column_heights): 110 # column_widths is a dictionary of min column size. Can be 111 # used to reserve vertical space to specific columns 112 self.row_heights = column_heights 113 114 #def paint(self, painter, option, index): 115 # return 116 117 def boundingRect(self): 118 return QRectF(0,0, self.w, self.h) 119 120 def rect(self): 121 return QRectF(0,0, self.w, self.h) 122 123 def get_size(self): 124 return self.w, self.h 125 126 def update_columns_size(self, norender=False): 127 self.sizes = {} 128 self.c2height = {} 129 130 for c in self.columns: 131 faces = self.column2faces.get(c, []) 132 self.sizes[c] = {} 133 total_height = 0 134 for r, f in enumerate(faces): 135 f.node = self.node 136 if f.type == "pixmap" and not norender: 137 f.update_pixmap() 138 elif f.type == "item" and not norender: 139 f.update_items() 140 elif f.type == "text" and f.rotation: 141 f.tight_text = False 142 143 width = f._width() + f.margin_right + f.margin_left 144 height = f._height() + f.margin_top + f.margin_bottom 145 146 if f.rotation: 147 if f.rotation == 90 or f.rotation == 270: 148 width, height = height, width 149 elif f.rotation == 180: 150 pass 151 else: 152 x0 = width/2.0 153 y0 = height/2.0 154 theta = (f.rotation * pi)/180 155 trans = lambda x, y: (x0+(x-x0)*cos(theta) + (y-y0)*sin(theta), y0-(x-x0)*sin(theta)+(y-y0)*cos(theta)) 156 coords = (trans(0,0), trans(0,height), trans(width,0), trans(width,height)) 157 x_coords = [e[0] for e in coords] 158 y_coords = [e[1] for e in coords] 159 width = max(x_coords) - min(x_coords) 160 height = max(y_coords) - min(y_coords) 161 162 self.sizes[c][r] = [width, height] 163 self.c2max_w[c] = max(self.c2max_w.get(c, 0), width) 164 self.r2max_h[r] = max(self.r2max_h.get(r, 0), height) 165 total_height += height 166 self.c2height[c] = total_height 167 168 if not self.sizes: 169 return 170 171 if self.as_grid: 172 self.h = max( [sum([self.r2max_h[r] for r in six.iterkeys(rows)]) for c, rows in six.iteritems(self.sizes)]) 173 else: 174 self.h = max( [self.c2height[c] for c in six.iterkeys(self.sizes)]) 175 176 self.w = sum(self.c2max_w.values()) 177 #self.setRect(0, 0, self.w+random.randint(1,5), self.h) 178 #pen = QPen() 179 #pen.setColor(QColor("red")) 180 #self.setPen(pen) 181 182 def setup_grid(self, c2max_w=None, r2max_h=None, as_grid=True): 183 if c2max_w: 184 self.c2max_w = c2max_w 185 186 if r2max_h: 187 self.r2max_h = r2max_h 188 189 # complete missing face columns 190 if self.columns: 191 self.columns = list(range(min(self.c2max_w), max(self.c2max_w)+1)) 192 193 self.as_grid = as_grid 194 self.update_columns_size(norender=True) 195 return self.c2max_w, self.r2max_h 196 197 198 def render(self): 199 x = 0 200 for c in self.columns: 201 max_w = self.c2max_w[c] 202 faces = self.column2faces.get(c, []) 203 204 if self.as_grid: 205 y = 0 206 else: 207 y = (self.h - self.c2height.get(c,0)) / 2 208 209 for r, f in enumerate(faces): 210 w, h = self.sizes[c][r] 211 if self.as_grid: 212 max_h = self.r2max_h[r] 213 else: 214 max_h = h 215 216 f.node = self.node 217 if f.type == "text": 218 obj = _TextFaceItem(f, self.node, f.get_text()) 219 font = f._get_font() 220 obj.setFont(font) 221 obj.setBrush(QBrush(QColor(f.fgcolor))) 222 elif f.type == "item": 223 obj = f.item 224 else: 225 # Loads the pre-generated pixmap 226 obj = _ImgFaceItem(f, self.node, f.pixmap) 227 228 obj.setAcceptHoverEvents(True) 229 obj.setParentItem(self) 230 231 x_offset, y_offset = 0, 0 232 233 if max_w > w: 234 # Horizontally at the left 235 if f.hz_align == 0: 236 x_offset = 0 237 elif f.hz_align == 1: 238 # Horizontally centered 239 x_offset = (max_w - w) / 2 240 elif f.hz_align == 2: 241 # At the right 242 x_offset = (max_w - w) 243 244 if max_h > h: 245 if f.vt_align == 0: 246 # Vertically on top 247 y_offset = 0 248 elif f.vt_align == 1: 249 # Vertically centered 250 y_offset = (max_h - h) / 2 251 elif f.hz_align == 2: 252 # Vertically at bottom 253 y_offset = (max_h - h) 254 255 # Correct cases in which object faces has negative 256 # starting points 257 #obj_rect = obj.boundingRect() 258 #_pos = obj_rect.topLeft() 259 #_x = abs(_pos.x()) if _pos.x() < 0 else 0 260 #_y = abs(_pos.y()) if _pos.y() < 0 else 0 261 262 text_y_offset = -obj.boundingRect().y() if f.type == "text" else 0 263 264 obj.setPos(x + f.margin_left + x_offset, 265 y + y_offset + f.margin_top + text_y_offset) 266 267 if f.rotation and f.rotation != 180: 268 fake_rect = obj.boundingRect() 269 fake_w, fake_h = fake_rect.width(), fake_rect.height() 270 self._rotate_item(obj, f.rotation) 271 #wcorr = fake_w/2.0 - w/2.0 272 #ycorr = fake_h/2.0 - h/2.0 273 #print "Correctopm", fake_w/2.0 - w/2.0, fake_h/2.0 - h/2 274 #obj.moveBy(-wcorr, -ycorr) 275 obj.moveBy(((w/2) - fake_w/2.0), (h/2) - (fake_h/2.0)) 276 #r = QGraphicsRectItem(0, 0, w, h) 277 #r.setParentItem(self) 278 279 obj.rotable = f.rotable 280 f.inner_background.apply(obj) 281 f.inner_border.apply(obj) 282 283 bg = f.background.apply(obj) 284 border = f.border.apply(obj) 285 if border: 286 border.setRect(x, y, max_w, max_h) 287 border.setParentItem(self) 288 if bg: 289 bg.setRect(x, y, max_w, max_h) 290 bg.setParentItem(self) 291 292 if f.opacity < 1: 293 obj.setOpacity(f.opacity) 294 295 if self.as_grid: 296 y += max_h 297 else: 298 y += h 299 300 # set label of the face if possible 301 try: 302 obj.face_label = f.label 303 except AttributeError: 304 pass 305 obj.face_type = str(type(f)).split(".")[-1] 306 307 x += max_w 308 309 def _rotate_item(self, item, rotation): 310 if hasattr(item, "_real_rect"): 311 rect = item._real_rect 312 else: 313 rect = item.boundingRect() 314 x = rect.width()/2 315 y = rect.height()/2 316 matrix = item.transform() 317 #item.setTransform(QTransform().translate(x, y).rotate(rotation).translate(-x, -y)) 318 item.setTransform(matrix.translate(x, y).rotate(rotation).translate(-x, -y)) 319 320 def rotate(self, rotation): 321 "rotates item over its own center" 322 for obj in self.childItems(): 323 if hasattr(obj, "rotable") and obj.rotable: 324 if hasattr(obj, "_real_rect"): 325 # to avoid incorrect rotation of tightgly wrapped 326 # text items we need to rotate using the real 327 # wrapping rect and revert y_text correction. 328 yoff = obj.boundingRect().y() 329 rect = obj._real_rect 330 # OJO!! this only works for the rotation of text 331 # faces in circular mode. Other cases are 332 # unexplored!! 333 obj.moveBy(0, yoff*2) 334 else: 335 yoff = None 336 rect = obj.boundingRect() 337 x = rect.width() / 2 338 y = rect.height() / 2 339 matrix = obj.transform() 340 #obj.setTransform(QTransform().translate(x, y).rotate(rotation).translate(-x, -y)) 341 obj.setTransform(matrix.translate(x, y).rotate(rotation).translate(-x, -y)) 342 343 def flip_hz(self): 344 for obj in self.childItems(): 345 rect = obj.boundingRect() 346 x = rect.width() / 2 347 y = rect.height() / 2 348 matrix = obj.transform() 349 #obj.setTransform(QTransform().translate(x, y).scale(-1,1).translate(-x, -y)) 350 obj.setTransform(matrix.translate(x, y).scale(-1,1).translate(-x, -y)) 351 352 def flip_vt(self): 353 for obj in self.childItems(): 354 rect = obj.boundingRect() 355 x = rect.width() / 2 356 y = rect.height() / 2 357 matrix = obj.transform() 358 #obj.setTransform(QTransform().translate(x, y).scale(1,-1).translate(-x, -y)) 359 obj.setTransform(matrix.translate(x, y).scale(1,-1).translate(-x, -y)) 360 361 362 363def update_node_faces(node, n2f, img): 364 365 # Organize all faces of this node in FaceGroups objects 366 # (tables of faces) 367 faceblock = {} 368 369 n2f[node] = faceblock 370 for position in FACE_POSITIONS: 371 # _temp_faces.position = 372 # 1: [f1, f2, f3], 373 # 2: [f4, f4], 374 # ... 375 376 # In case there are fixed faces 377 fixed_faces = getattr(getattr(node, "faces", None) , position, {}) 378 379 # _temp_faces should be initialized by the set_style funcion 380 all_faces = getattr(node._temp_faces, position) 381 for column, values in six.iteritems(fixed_faces): 382 all_faces.setdefault(column, []).extend(values) 383 384 if position == "aligned" and img.draw_aligned_faces_as_table: 385 as_grid = False 386 else: 387 as_grid = False 388 389 faceblock[position] = _FaceGroupItem(all_faces, node, as_grid=as_grid) 390 391 # all temp and fixed faces are now referenced by the faceblock, so 392 # we can clear the node temp faces (don't want temp faces to be 393 # replicated with copy or dumped with cpickle) 394 node._temp_faces = None 395 396 return faceblock 397