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