text: # Copyright 2003-2008 by Leighton Pritchard.  All rights reserved.
  # This code is part of the Biopython distribution and governed by its
  # license.  Please see the LICENSE file that should have been included
  # as part of this package.
  #
  # Contact:       Leighton Pritchard, Scottish Crop Research Institute,
  #                Invergowrie, Dundee, Scotland, DD2 5DA, UK
  #                L.Pritchard@scri.ac.uk
  ################################################################################
  
  """ Diagram module
  
      Provides:
  
      o Diagram -   Container for information concerning the tracks to be
                      drawn in a diagram, and the interface for defining the
                      diagram (possibly split these functions in later version?)
  
      For drawing capabilities, this module uses reportlab to draw and write
      the diagram:
  
      http://www.reportlab.com
  
      For dealing with biological information, the package expects BioPython
      objects - namely SeqRecord ojbects containing SeqFeature objects.
  """
  
  #------------------------------------------------------------------------------
  # IMPORTS
  
  # ReportLab
  from reportlab.graphics import renderPS, renderPDF, renderSVG
  try:
      from reportlab.graphics import renderPM
  except ImportError:
      #This is an optional part of ReportLab, so may not be installed.
      renderPM=None
  from reportlab.lib import pagesizes
  
  # GenomeDiagram
  from _LinearDrawer import LinearDrawer
  from _CircularDrawer import CircularDrawer
  from _Track import Track
  
  # Builtins
  import sys
  
  from Bio.Graphics import _write
  
  #------------------------------------------------------------------------------
  # CLASSES
  
  #------------------------------------------------------------
  # Diagram
  
  class Diagram(object):
      """ Diagram
  
          Provides:
  
          Attributes:
  
          o name         String, identifier for the diagram
  
          o tracks       List of Track objects comprising the diagram 
  
          o format       String, format of the diagram (circular/linear)
  
          o pagesize     String, the pagesize of output
  
          o orientation  String, the page orientation (landscape/portrait)
  
          o x            Float, the proportion of the page to take up with even 
                                X margins
  
          o y            Float, the proportion of the page to take up with even 
                                Y margins
  
          o xl           Float, the proportion of the page to take up with the 
                                left X margin
  
          o xr           Float, the proportion of the page to take up with the 
                                right X margin
  
          o yt           Float, the proportion of the page to take up with the 
                                top Y margin
  
          o yb           Float, the proportion of the page to take up with the 
                                bottom Y margin
  
          o start        Int, the base/aa position to start the diagram at
  
          o end          Int, the base/aa position to end the diagram at
  
          o tracklines   Boolean, True if track guidelines are to be drawn
  
          o fragments    Int, for a linear diagram, the number of equal divisions
                                  into which the sequence is divided
  
          o fragment_size Float, the proportion of the space available to each 
                                     fragment that should be used in drawing
  
          o track_size   Float, the proportion of the space available to each 
                                    track that should be used in drawing
  
          o circular     Boolean, True if the genome/sequence to be drawn is, in 
                                  reality, circular.  
  
          Methods:
  
          o __init__(self, name=None) Called on instantiation
  
          o draw(self, format='circular', pagesize='A3', orientation='landscape',
               x=0.05, y=0.05, xl=None, xr=None, yt=None, yb=None,
               start=None, end=None, tracklines=0, fragments=10,
               fragment_size=0.9, track_size=0.75) Instructs the package to draw
               the diagram
  
          o write(self, filename='test1.ps', output='PS') Writes the drawn
              diagram to a specified file, in a specified format.
  
          o add_track(self, track, track_level) Adds a Track object to the
              diagram, with instructions to place it at a particular level on
              the diagram
  
          o del_track(self, track_level) Removes the track that is to be drawn
              at a particular level on the diagram
  
          o get_tracks(self) Returns the list of Track objects to be drawn
              contained in the diagram
  
          o renumber_tracks(self, low=1) Renumbers all tracks consecutively,
              optionally from a passed lowest number
  
          o get_levels(self) Returns a list of levels currently occupied by
              Track objects
  
          o get_drawn_levels(self) Returns a list of levels currently occupied
              by Track objects that will be shown in the drawn diagram (i.e.
              are not hidden)
  
          o range(self) Returns the lowest- and highest-numbered positions
              contained within features in all tracks on the diagram as a tuple.
  
          o __getitem__(self, key) Returns the track contained at the level of
              the passed key
  
          o __str__(self) Returns a formatted string describing the diagram
  
      """
      def __init__(self, name=None, format='circular', pagesize='A3', 
           orientation='landscape', x=0.05, y=0.05, xl=None, 
           xr=None, yt=None, yb=None, start=None, end=None, 
           tracklines=False, fragments=10, fragment_size=0.9, 
           track_size=0.75, circular=True):
          """ __init__(self, name=None)
  
              o name  String describing the diagram
  
              o format    String: 'circular' or 'linear', depending on the sort of
                          diagram required
  
              o pagesize  String describing the ISO size of the image, or a tuple
                          of pixels
  
              o orientation   String describing the required orientation of the
                              final drawing ('landscape' or 'portrait')
  
              o x         Float (0->1) describing the relative size of the X
                          margins to the page
  
              o y         Float (0->1) describing the relative size of the Y
                          margins to the page
  
              o xl        Float (0->1) describing the relative size of the left X
                          margin to the page (overrides x)
  
              o xl        Float (0->1) describing the relative size of the left X
                          margin to the page (overrides x)
  
              o xr        Float (0->1) describing the relative size of the right X
                          margin to the page (overrides x)
  
              o yt        Float (0->1) describing the relative size of the top Y
                          margin to the page (overrides y)
  
              o yb        Float (0->1) describing the relative size of the lower Y
                          margin to the page (overrides y)
  
              o start     Int, the position to begin drawing the diagram at
  
  
              o end       Int, the position to stop drawing the diagram at
  
              o tracklines    Boolean flag to show (or not) lines delineating 
                          tracks on the diagram
  
              o fragments Int, for linear diagrams, the number of sections into
                          which to break the sequence being drawn
  
              o fragment_size     Float (0->1), for linear diagrams, describing 
                                  the proportion of space in a fragment to take
                                  up with tracks
  
              o track_size        Float (0->1) describing the proportion of space
                                  in a track to take up with sigils
  
              o circular  Boolean flag to indicate whether the sequence being
                          drawn is circular
                          
  
          """
          self.tracks = {}   # Holds all Track objects, keyed by level
          self.name = name    # Description of the diagram
          # Diagram page setup attributes
          self.format = format
          self.pagesize = pagesize
          self.orientation = orientation
          self.x = x
          self.y = y
          self.xl = xl
          self.xr = xr
          self.yt = yt
          self.yb = yb
          self.start = start
          self.end = end
          self.tracklines = tracklines
          self.fragments = fragments
          self.fragment_size = fragment_size
          self.track_size = track_size
          self.circular = circular
  
      def set_all_tracks(self, attr, value):
          """ set_all_tracks(self, attr, value)
  
              o attr      An attribute of the Track class
  
              o value     The value to set that attribute
  
              Set the passed attribute of all tracks in the set to the
              passed value
          """
          for track in self.tracks.values():
              if hasattr(track, attr):          # If the feature has the attribute
                  if getattr(track, attr) != value:
                      setattr(track, attr, value)   # set it to the passed value     
  
      def draw(self, format=None, pagesize=None, orientation=None,
               x=None, y=None, xl=None, xr=None, yt=None, yb=None,
               start=None, end=None, tracklines=None, fragments=None,
               fragment_size=None, track_size=None, circular=None):
          """ draw(self, format=None, pagesize=None, orientation=None,
              x=None, y=None, xl=None, xr=None, yt=None, yb=None,
              start=None, end=None, tracklines=None, fragments=None,
              fragment_size=None, track_size=None)
  
              Draws the diagram using the passed parameters, if any, to override
          previous settings for the diagram object.
          """
          # Pass the parameters to the drawer objects that will build the 
          # diagrams.  At the moment, we detect overrides with an or in the 
          # Instantiation arguments, but I suspect there's a neater way to do 
          # this.
          if format == 'linear':
              drawer = LinearDrawer(self, pagesize or self.pagesize, 
                                    orientation or self.orientation, 
                                    x or self.x, y or self.y, xl or self.xl, 
                                    xr or self.xr, yt or self.yt, 
                                    yb or self.yb, start or self.start, 
                                    end or self.end, 
                                    tracklines or self.tracklines,
                                    fragments or self.fragments, 
                                    fragment_size or self.fragment_size, 
                                    track_size or self.track_size)
          else:
              drawer = CircularDrawer(self, pagesize or self.pagesize, 
                                      orientation or self.orientation, 
                                      x or self.x, y or self.y, xl or self.xl, 
                                      xr or self.xr, yt or self.yt, 
                                      yb or self.yb, start or self.start, 
                                      end or self.end, 
                                      tracklines or self.tracklines,
                                      track_size or self.track_size,
                                      circular or self.circular)
          drawer.draw()   # Tell the drawer to complete the drawing
          self.drawing = drawer.drawing  # Get the completed drawing
          
      def write(self, filename='test1.ps', output='PS', dpi=72):
          """ write(self, filename='test1.ps', output='PS', dpi=72)
  
              o filename      String indicating the name of the output file,
                              or a handle to write to.
  
              o output        String indicating output format, one of PS, PDF,
                              SVG, or provided the ReportLab renderPM module is
                              installed, one of the bitmap formats JPG, BMP,
                              GIF, PNG, TIFF or TIFF.  The format can be given
                              in upper or lower case.
  
              o dpi           Resolution (dots per inch) for bitmap formats.
  
              Write the completed drawing out to a file in a prescribed format
  
              No return value.
          """
          return _write(self.drawing, filename, output, dpi=dpi)
          
      def write_to_string(self, output='PS', dpi=72):
          """ write(self, output='PS')
  
              o output        String indicating output format, one of PS, PDF,
                              SVG, JPG, BMP, GIF, PNG, TIFF or TIFF (as
                              specified for the write method).
  
              o dpi           Resolution (dots per inch) for bitmap formats.
  
              Return the completed drawing as a string in a prescribed format
          """
          #The ReportLab drawToString method, which this function used to call,
          #just uses a cStringIO or StringIO handle with the drawToFile method.
          #In order to put all our complicated file format specific code in one
          #place we'll just use a StringIO handle here:
          from StringIO import StringIO
          handle = StringIO()
          self.write(handle, output, dpi)
          return handle.getvalue()
  
      def add_track(self, track, track_level):
          """ add_track(self, track, track_level)
  
              o track         Track object to draw
  
              o track_level   Int, the level at which the track will be drawn
                              (above an arbitrary baseline)
  
              Add a pre-existing Track to the diagram at a given level
          """
          if track is None:
              raise ValueError("Must specify track")
          if track_level not in self.tracks:     # No track at that level
              self.tracks[track_level] = track   # so just add it
          else:       # Already a track there, so shunt all higher tracks up one
              occupied_levels = self.get_levels() # Get list of occupied levels...
              occupied_levels.sort()              # ...sort it...
              occupied_levels.reverse()           # ...reverse it (highest first)
              for val in occupied_levels:
                  # If track value >= that to be added
                  if val >= track.track_level:
                      self.tracks[val+1] = self.tracks[val] # ...increment by 1
              self.tracks[track_level] = track   # And put the new track in
          self.tracks[track_level].track_level = track_level
                  
  
      def new_track(self, track_level, **args):
          """ new_track(self, track_level) -> Track
  
              o track_level   Int, the level at which the track will be drawn
                              (above an arbitrary baseline)
  
              Add a new Track to the diagram at a given level and returns it for
              further user manipulation.
          """
          newtrack = Track()
          for key in args:
              setattr(newtrack, key, args[key])
          if track_level not in self.tracks:        # No track at that level
              self.tracks[track_level] = newtrack   # so just add it
          else:       # Already a track there, so shunt all higher tracks up one
              occupied_levels = self.get_levels() # Get list of occupied levels...
              occupied_levels.sort()              # ...sort it...
              occupied_levels.reverse()           # ...reverse (highest first)...
              for val in occupied_levels:     
                  if val >= track_level:        # Track value >= that to be added
                      self.tracks[val+1] = self.tracks[val] # ..increment by 1
              self.tracks[track_level] = newtrack   # And put the new track in
          self.tracks[track_level].track_level = track_level
          return newtrack
  
              
      def del_track(self, track_level):
          """ del_track(self, track_level)
  
              o track_level   Int, the level of the track on the diagram to delete
  
              Remove the track at the passed level from the diagram
          """
          del self.tracks[track_level]
  
  
      def get_tracks(self):
          """ get_tracks(self) -> list
  
              Returns a list of the tracks contained in the diagram
          """
          return self.tracks.values()
  
  
      def move_track(self, from_level, to_level):
          """ move_track(self, from_level, to_level)
  
              o from_level    Int, the level at which the track to be moved is
                              found
  
              o to_level      Int, the level to move the track to
  
              Moves a track from one level on the diagram to another
          """
          aux = self.tracks[from_level]
          del self.tracks[from_level]
          self.add_track(aux, to_level)
  
  
      def renumber_tracks(self, low=1, step=1):
          """ renumber_tracks(self, low=1, step=1)
  
              o low       Int, the track number to start from
  
              o step      Int, the track interval for separation of tracks
  
              Reassigns all the tracks to run consecutively from the lowest
              value (low)
          """
          track = low                 # Start numbering from here
          levels = self.get_levels()  # 
  
          conversion = {}             # Holds new set of levels
          for level in levels:        # Starting at low...
              conversion[track] = self.tracks[level] # Add old tracks to new set
              conversion[track].track_level = track
              track += step                           # step interval
          self.tracks = conversion   # Replace old set of levels with new set
  
      def get_levels(self):
          """ get_levels(self) -> [int, int, ...]
  
              Return a sorted list of levels occupied by tracks in the diagram
          """
          levels = self.tracks.keys()
          levels.sort()
          return levels
  
  
      def get_drawn_levels(self):
          """ get_drawn_levels(self) -> [int, int, ...]
  
              Return a sorted list of levels occupied by tracks that are not
              explicitly hidden
          """
          drawn_levels = [key for key in self.tracks.keys() if \
                          not self.tracks[key].hide] # get list of shown levels
          drawn_levels.sort()
          return drawn_levels
  
  
      def range(self):
          """ range(self) -> (int, int)
  
              Returns the lowest and highest base (or mark) numbers containd in
              track features as a tuple
          """
          lows, highs = [], []
          for track in self.tracks.values(): # Get ranges for each track
              low, high = track.range()
              lows.append(low)
              highs.append(high)
          return (min(lows), max(highs))      # Return extremes from all tracks
  
      def __getitem__(self, key):
          """ __getitem__(self, key) -> Track
  
              o key       The id of a track in the diagram
  
              Return the Track object with the passed id
          """
          return self.tracks[key]
  
      def __str__(self):
          """ __str__(self) -> ""
  
              Returns a formatted string with information about the diagram
          """
          outstr = ["\n<%s: %s>" % (self.__class__, self.name)]
          outstr.append("%d tracks" % len(self.tracks))
          for level in self.get_levels():
              outstr.append("Track %d: %s\n" % (level, self.tracks[level]))
          outstr = '\n'.join(outstr)
          return outstr       
  
  