Back to index

python-biopython  1.60
_AbstractDrawer.py
Go to the documentation of this file.
00001 # Copyright 2003-2008 by Leighton Pritchard.  All rights reserved.
00002 # Revisions copyright 2008-2009 by Peter Cock.
00003 # This code is part of the Biopython distribution and governed by its
00004 # license.  Please see the LICENSE file that should have been included
00005 # as part of this package.
00006 #
00007 # Contact:       Leighton Pritchard, Scottish Crop Research Institute,
00008 #                Invergowrie, Dundee, Scotland, DD2 5DA, UK
00009 #                L.Pritchard@scri.ac.uk
00010 ################################################################################
00011 
00012 """ AbstractDrawer module (considered to be a private module, the API may change!)
00013 
00014     Provides:
00015 
00016     o AbstractDrawer -    Superclass for methods common to *Drawer objects
00017 
00018     o page_sizes -          Method that returns a ReportLab pagesize when passed
00019                             a valid ISO size
00020 
00021     o draw_box -            Method that returns a closed path object when passed
00022                             the proper co-ordinates.  For HORIZONTAL boxes only.
00023 
00024     o angle2trig -          Method that returns a tuple of values that are the
00025                             vector for rotating a point through a passed angle,
00026                             about an origin
00027 
00028     o intermediate_points - Method that returns a list of values intermediate
00029                             between the points in a passed dataset
00030     
00031     For drawing capabilities, this module uses reportlab to draw and write
00032     the diagram:
00033 
00034     http://www.reportlab.com
00035 
00036     For dealing with biological information, the package expects BioPython
00037     objects:
00038 
00039     http://www.biopython.org
00040 """
00041 
00042 # ReportLab imports
00043 from reportlab.lib import pagesizes
00044 from reportlab.lib import colors
00045 from reportlab.graphics.shapes import *
00046 
00047 from math import pi
00048 
00049 ################################################################################
00050 # METHODS
00051 ################################################################################
00052 # Utility method to translate strings to ISO page sizes
00053 def page_sizes(size):
00054     """ page_sizes(size)
00055 
00056         o size        A string representing a standard page size
00057 
00058         Returns a ReportLab pagesize when passed a valid size string
00059     """
00060     sizes = {'A0': pagesizes.A0,    # ReportLab pagesizes, keyed by ISO string
00061              'A1': pagesizes.A1,
00062              'A2': pagesizes.A2,
00063              'A3': pagesizes.A3,
00064              'A4': pagesizes.A4,
00065              'A5': pagesizes.A5,
00066              'A6': pagesizes.A6,
00067              'B0': pagesizes.B0,
00068              'B1': pagesizes.B1,
00069              'B2': pagesizes.B2,
00070              'B3': pagesizes.B3,
00071              'B4': pagesizes.B4,
00072              'B5': pagesizes.B5,
00073              'B6': pagesizes.B6,
00074              'ELEVENSEVENTEEN': pagesizes.ELEVENSEVENTEEN,
00075              'LEGAL': pagesizes.LEGAL,
00076              'LETTER': pagesizes.LETTER
00077              }
00078     try:
00079         return sizes[size]
00080     except:
00081         raise ValueError, "%s not in list of page sizes" % size
00082 
00083 
00084 def draw_box(point1, point2,
00085              color=colors.lightgreen, border=None, colour=None,
00086              **kwargs):
00087     """ draw_box(self, (x1, y1), (x2, y2), (x3, y3), (x4, y4),
00088               color=colors.lightgreen)
00089 
00090         o point1, point2 Co-ordinates for opposite corners of the box
00091                          (x,y tuples)
00092         
00093         o color /colour       The color for the box
00094                               (colour takes priority over color)
00095                               
00096         o border              Border color for the box
00097 
00098         Returns a closed path object, beginning at (x1,y1) going round
00099         the four points in order, and filling with the passed color.            
00100     """
00101     x1, y1 = point1
00102     x2, y2 = point2
00103 
00104     #Let the UK spelling (colour) override the USA spelling (color)
00105     if colour is not None:
00106         color = colour
00107         del colour
00108 
00109     if not isinstance(color, colors.Color):
00110         raise ValueError("Invalid color %s" % repr(color))
00111     
00112     if color == colors.white and border is None:   # Force black border on 
00113         strokecolor = colors.black                 # white boxes with
00114     elif border is None:                           # undefined border, else
00115         strokecolor = color                        # use fill color
00116     elif border:
00117         if not isinstance(border, colors.Color):
00118             raise ValueError("Invalid border color %s" % repr(border))
00119         strokecolor = border
00120     else:
00121         #e.g. False
00122         strokecolor = None
00123 
00124     x1, y1, x2, y2 = min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2)
00125     return Polygon([x1, y1, x2, y1, x2, y2, x1, y2],
00126                    strokeColor=strokecolor,
00127                    fillColor=color,
00128                    strokewidth=0,
00129                    **kwargs)
00130 
00131 
00132 def draw_polygon(list_of_points,
00133                  color=colors.lightgreen, border=None, colour=None,
00134                  **kwargs):
00135     """ draw_polygon(self, (x1, y1), (x2, y2), (x3, y3), (x4, y4)
00136               colour=colors.lightgreen)
00137 
00138         o list_of_point = list of (x,y) tuples for the corner coordinates
00139         
00140         o colour              The colour for the box
00141 
00142         Returns a closed path object, beginning at (x1,y1) going round
00143         the four points in order, and filling with the passed colour.          
00144     """
00145     #Let the UK spelling (colour) override the USA spelling (color)
00146     if colour is not None:
00147         color = colour
00148         del colour
00149 
00150     if color == colors.white and border is None:   # Force black border on 
00151         strokecolor = colors.black                 # white boxes with
00152     elif border is None:                           # undefined border, else
00153         strokecolor = color                        # use fill colour
00154     elif border:
00155         strokecolor = border
00156     else:
00157         #e.g. False
00158         strokecolor = None
00159 
00160     xy_list = []
00161     for (x,y) in list_of_points:
00162         xy_list.append(x)
00163         xy_list.append(y)
00164 
00165     return Polygon(xy_list,
00166                    strokeColor=strokecolor,
00167                    fillColor=color,
00168                    strokewidth=0,
00169                    **kwargs)
00170 
00171 
00172 def draw_arrow(point1, point2, color=colors.lightgreen, border=None,
00173                shaft_height_ratio=0.4, head_length_ratio=0.5, orientation='right',
00174                colour=None, **kwargs):
00175     """ Returns a closed path object representing an arrow enclosed by the
00176         box with corners at {point1=(x1,y1), point2=(x2,y2)}, a shaft height
00177         given by shaft_height_ratio (relative to box height), a head length
00178         given by head_length_ratio (also relative to box height), and
00179         an orientation that may be 'left' or 'right'.
00180     """
00181     x1, y1 = point1
00182     x2, y2 = point2
00183     
00184     if shaft_height_ratio < 0 or 1 < shaft_height_ratio:
00185         raise ValueError("Arrow shaft height ratio should be in range 0 to 1")
00186     if head_length_ratio < 0:
00187         raise ValueError("Arrow head length ratio should be positive")
00188 
00189     #Let the UK spelling (colour) override the USA spelling (color)
00190     if colour is not None:
00191         color = colour
00192         del colour
00193 
00194     if color == colors.white and border is None:   # Force black border on 
00195         strokecolor = colors.black                 # white boxes with
00196     elif border is None:                           # undefined border, else
00197         strokecolor = color                        # use fill colour
00198     elif border:
00199         strokecolor = border
00200     else:
00201         #e.g. False
00202         strokecolor = None
00203 
00204     # Depending on the orientation, we define the bottom left (x1, y1) and
00205     # top right (x2, y2) coordinates differently, but still draw the box
00206     # using the same relative co-ordinates:
00207     xmin, ymin = min(x1, x2), min(y1, y2)
00208     xmax, ymax = max(x1, x2), max(y1, y2)
00209     if orientation == 'right':
00210         x1, x2, y1, y2 = xmin, xmax, ymin, ymax
00211     elif orientation == 'left':
00212         x1, x2, y1, y2 = xmax, xmin, ymin, ymax
00213     else:
00214         raise ValueError("Invalid orientation %s, should be 'left' or 'right'" \
00215                          % repr(orientation))
00216 
00217     # We define boxheight and boxwidth accordingly, and calculate the shaft
00218     # height from these.  We also ensure that the maximum head length is
00219     # the width of the box enclosure
00220     boxheight = y2-y1
00221     boxwidth = x2-x1
00222     shaftheight = boxheight*shaft_height_ratio
00223     headlength = min(abs(boxheight)*head_length_ratio, abs(boxwidth))
00224     if boxwidth < 0:
00225         headlength *= -1 #reverse it
00226 
00227 
00228     shafttop = 0.5*(boxheight+shaftheight)
00229     shaftbase = boxheight-shafttop
00230     headbase = boxwidth-headlength
00231     midheight = 0.5*boxheight
00232     return Polygon([x1, y1+shafttop,
00233                     x1+headbase, y1+shafttop,
00234                     x1+headbase, y2,
00235                     x2, y1+midheight,
00236                     x1+headbase, y1,
00237                     x1+headbase, y1+shaftbase,
00238                     x1, y1+shaftbase],
00239                    strokeColor=strokecolor,
00240                    #strokeWidth=max(1, int(boxheight/40.)),
00241                    strokeWidth=1,
00242                    #default is mitre/miter which can stick out too much:
00243                    strokeLineJoin=1, #1=round
00244                    fillColor=color,
00245                    **kwargs)
00246 
00247 def angle2trig(theta):
00248     """ angle2trig(angle)
00249 
00250         o theta     Angle in degrees, counter clockwise from horizontal
00251 
00252         Returns a representation of the passed angle in a format suitable
00253         for ReportLab rotations (i.e. cos(theta), sin(theta), -sin(theta),
00254         cos(theta) tuple)
00255     """
00256     c = cos(theta * pi / 180)
00257     s = sin(theta * pi / 180)
00258     return(c, s, -s, c)         # Vector for rotating point around an origin
00259 
00260 
00261 def intermediate_points(start, end, graph_data):
00262     """ intermediate_points(start, end, graph_data)
00263 
00264         o graph_data
00265 
00266         o start
00267 
00268         o end
00269 
00270         Returns a list of (start, end, value) tuples describing the passed
00271         graph data as 'bins' between position midpoints.
00272     """
00273     #print start, end, len(graph_data)
00274     newdata = []    # data in form (X0, X1, val)
00275     # add first block
00276     newdata.append((start, graph_data[0][0]+(graph_data[1][0]-graph_data[0][0])/2.,
00277                     graph_data[0][1]))
00278     # add middle set
00279     for index in xrange(1, len(graph_data)-1):
00280         lastxval, lastyval = graph_data[index-1]
00281         xval, yval = graph_data[index]
00282         nextxval, nextyval = graph_data[index+1]
00283         newdata.append( (lastxval+(xval-lastxval)/2.,
00284                          xval+(nextxval-xval)/2., yval) )
00285     # add last block
00286     newdata.append( (xval+(nextxval-xval)/2.,
00287                          end, graph_data[-1][1]) )
00288     #print newdata[-1]
00289     #print newdata
00290     return newdata
00291 
00292 ################################################################################
00293 # CLASSES
00294 ################################################################################
00295 
00296 class AbstractDrawer(object):
00297     """ AbstractDrawer
00298 
00299         Provides:
00300 
00301         Methods:
00302 
00303         o __init__(self, parent, pagesize='A3', orientation='landscape',
00304                  x=0.05, y=0.05, xl=None, xr=None, yt=None, yb=None,
00305                  start=None, end=None, tracklines=0) Called on instantiation
00306 
00307         o set_page_size(self, pagesize, orientation)    Set the page size to the
00308                                                     passed size and orientation
00309 
00310         o set_margins(self, x, y, xl, xr, yt, yb)   Set the drawable area of the
00311                                                     page
00312 
00313         o set_bounds(self, start, end)  Set the bounds for the elements to be
00314                                         drawn
00315 
00316         o is_in_bounds(self, value)     Returns a boolean for whether the position
00317                                         is actually to be drawn
00318 
00319         o __len__(self)     Returns the length of sequence that will be drawn
00320 
00321         Attributes:
00322 
00323         o tracklines    Boolean for whether to draw lines dilineating tracks
00324 
00325         o pagesize      Tuple describing the size of the page in pixels
00326 
00327         o x0            Float X co-ord for leftmost point of drawable area
00328 
00329         o xlim          Float X co-ord for rightmost point of drawable area
00330 
00331         o y0            Float Y co-ord for lowest point of drawable area
00332 
00333         o ylim          Float Y co-ord for topmost point of drawable area
00334 
00335         o pagewidth     Float pixel width of drawable area
00336 
00337         o pageheight    Float pixel height of drawable area
00338 
00339         o xcenter       Float X co-ord of center of drawable area
00340 
00341         o ycenter       Float Y co-ord of center of drawable area
00342 
00343         o start         Int, base to start drawing from
00344 
00345         o end           Int, base to stop drawing at
00346 
00347         o length        Size of sequence to be drawn
00348         
00349         o cross_track_links List of tuples each with four entries (track A,
00350                             feature A, track B, feature B) to be linked.
00351     """
00352     def __init__(self, parent, pagesize='A3', orientation='landscape',
00353                  x=0.05, y=0.05, xl=None, xr=None, yt=None, yb=None,
00354                  start=None, end=None, tracklines=0, cross_track_links=None):
00355         """ __init__(self, parent, pagesize='A3', orientation='landscape',
00356                  x=0.05, y=0.05, xl=None, xr=None, yt=None, yb=None,
00357                  start=None, end=None, tracklines=0)
00358 
00359             o parent    Diagram object containing the data that the drawer
00360                         draws
00361 
00362             o pagesize  String describing the ISO size of the image, or a tuple
00363                         of pixels
00364 
00365             o orientation   String describing the required orientation of the
00366                             final drawing ('landscape' or 'portrait')
00367 
00368             o x         Float (0->1) describing the relative size of the X
00369                         margins to the page
00370 
00371             o y         Float (0->1) describing the relative size of the Y
00372                         margins to the page
00373 
00374             o xl        Float (0->1) describing the relative size of the left X
00375                         margin to the page (overrides x)
00376 
00377             o xl        Float (0->1) describing the relative size of the left X
00378                         margin to the page (overrides x)
00379 
00380             o xr        Float (0->1) describing the relative size of the right X
00381                         margin to the page (overrides x)
00382 
00383             o yt        Float (0->1) describing the relative size of the top Y
00384                         margin to the page (overrides y)
00385 
00386             o yb        Float (0->1) describing the relative size of the lower Y
00387                         margin to the page (overrides y)
00388 
00389             o start     Int, the position to begin drawing the diagram at
00390 
00391             o end       Int, the position to stop drawing the diagram at
00392 
00393             o tracklines    Boolean flag to show (or not) lines delineating tracks
00394                             on the diagram            
00395 
00396             o cross_track_links List of tuples each with four entries (track A,
00397                                 feature A, track B, feature B) to be linked.
00398         """
00399         self._parent = parent   # The calling Diagram object
00400 
00401         # Perform 'administrative' tasks of setting up the page
00402         self.set_page_size(pagesize, orientation)   # Set drawing size
00403         self.set_margins(x, y, xl, xr, yt, yb)      # Set page margins
00404         self.set_bounds(start, end) # Set limits on what will be drawn
00405         self.tracklines = tracklines    # Set flags
00406         if cross_track_links is None:
00407             cross_track_links = []
00408         else:
00409             self.cross_track_links = cross_track_links
00410         
00411     def _set_xcentre(self, value):
00412         import warnings
00413         import Bio
00414         warnings.warn("The _set_xcentre method and .xcentre attribute are deprecated; please use the .xcenter attribute instead", Bio.BiopythonDeprecationWarning)
00415         self.xcenter = value
00416     xcentre = property(fget = lambda self : self.xcenter,
00417                        fset = _set_xcentre,
00418                        doc="Backwards compatible alias for xcenter (DEPRECATED)")
00419 
00420     def _set_ycentre(self, value):
00421         import warnings
00422         import Bio
00423         warnings.warn("The _set_ycentre method and .xcentre attribute are deprecated; please use the .ycenter attribute instead", Bio.BiopythonDeprecationWarning)
00424         self.ycenter = value
00425     ycentre = property(fget = lambda self : self.ycenter,
00426                        fset = _set_ycentre,
00427                        doc="Backwards compatible alias for ycenter (DEPRECATED)")
00428 
00429     def set_page_size(self, pagesize, orientation):
00430         """ set_page_size(self, pagesize, orientation)
00431 
00432             o pagesize      Size of the output image, a tuple of pixels (width,
00433                             height, or a string in the reportlab.lib.pagesizes
00434                             set of ISO sizes.
00435 
00436             o orientation   String: 'landscape' or 'portrait'
00437 
00438             Set the size of the drawing
00439         """
00440         if type(pagesize) == type('a'):     # A string, so translate
00441             pagesize = page_sizes(pagesize)
00442         elif type(pagesize) == type((1,2)): # A tuple, so don't translate
00443             pagesize = pagesize
00444         else:
00445             raise ValueError, "Page size %s not recognised" % pagesize        
00446         shortside, longside = min(pagesize), max(pagesize)
00447 
00448         orientation = orientation.lower()
00449         if orientation not in ('landscape', 'portrait'):
00450             raise ValueError, "Orientation %s not recognised" % orientation
00451         if orientation == 'landscape':
00452             self.pagesize = (longside, shortside)
00453         else:
00454             self.pagesize = (shortside, longside)
00455 
00456 
00457     def set_margins(self, x, y, xl, xr, yt, yb):
00458         """ set_margins(self, x, y, xl, xr, yt, yb)
00459 
00460             o x         Float(0->1), Absolute X margin as % of page
00461 
00462             o y         Float(0->1), Absolute Y margin as % of page
00463 
00464             o xl        Float(0->1), Left X margin as % of page
00465 
00466             o xr        Float(0->1), Right X margin as % of page
00467 
00468             o yt        Float(0->1), Top Y margin as % of page
00469 
00470             o yb        Float(0->1), Bottom Y margin as % of page
00471 
00472             Set the page margins as proportions of the page 0->1, and also
00473             set the page limits x0, y0 and xlim, ylim, and page center
00474             xorigin, yorigin, as well as overall page width and height
00475         """
00476         # Set left, right, top and bottom margins
00477         xmargin_l = xl or x
00478         xmargin_r = xr or x
00479         ymargin_top = yt or y
00480         ymargin_btm = yb or y
00481         
00482         # Set page limits, center and height/width
00483         self.x0, self.y0 = self.pagesize[0]*xmargin_l, self.pagesize[1]*ymargin_btm
00484         self.xlim, self.ylim = self.pagesize[0]*(1-xmargin_r), self.pagesize[1]*(1-ymargin_top)
00485         self.pagewidth = self.xlim-self.x0
00486         self.pageheight = self.ylim-self.y0
00487         self.xcenter, self.ycenter = self.x0+self.pagewidth/2., self.y0+self.pageheight/2.
00488 
00489             
00490     def set_bounds(self, start, end):
00491         """ set_bounds(self, start, end)
00492 
00493             o start     The first base (or feature mark) to draw from
00494 
00495             o end       The last base (or feature mark) to draw to
00496 
00497             Sets start and end points for the drawing as a whole
00498         """
00499         low, high = self._parent.range()  # Extent of tracks
00500 
00501         if start is not None and end is not None and start > end:
00502             start, end = end, start
00503 
00504         if start is None or start < 0:  # Check validity of passed args and 
00505             start = 0   # default to 0
00506         if end is None or end < 0:
00507             end = high + 1  # default to track range top limit
00508         
00509         self.start, self.end = int(start), int(end)
00510         self.length = self.end - self.start + 1
00511 
00512 
00513     def is_in_bounds(self, value):
00514         """ is_in_bounds(self, value)
00515 
00516             o value   A base position
00517 
00518             Returns 1 if the value is within the region selected for drawing
00519         """
00520         if value >= self.start and value <= self.end:
00521             return 1
00522         return 0
00523 
00524 
00525     def __len__(self):
00526         """ __len__(self)
00527 
00528             Returns the length of the region to be drawn
00529         """
00530         return self.length
00531         
00532     def _current_track_start_end(self):        
00533         track = self._parent[self.current_track_level]
00534         if track.start is None:
00535             start = self.start
00536         else:
00537             start = max(self.start, track.start)
00538         if track.end is None:
00539             end = self.end
00540         else:
00541             end = min(self.end, track.end)
00542         return start, end