Merge branch 'develop' into bug/JAL-2346annotationChoice
[jalview.git] / src / jalview / renderer / seqfeatures / FeatureRenderer.java
index 9e0089f..72ac2c8 100644 (file)
@@ -23,6 +23,7 @@ package jalview.renderer.seqfeatures;
 import jalview.api.AlignViewportI;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.util.Comparison;
 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 
 import java.awt.AlphaComposite;
@@ -30,28 +31,11 @@ import java.awt.Color;
 import java.awt.FontMetrics;
 import java.awt.Graphics;
 import java.awt.Graphics2D;
-import java.awt.image.BufferedImage;
 
 public class FeatureRenderer extends FeatureRendererModel
 {
-
-  FontMetrics fm;
-
-  int charOffset;
-
-  boolean offscreenRender = false;
-
-  protected SequenceI lastSeq;
-
-  char s;
-
-  int i;
-
-  int av_charHeight, av_charWidth;
-
-  boolean av_validCharWidth, av_isShowSeqFeatureHeight;
-
-  private Integer currentColour;
+  private static final AlphaComposite NO_TRANSPARENCY = AlphaComposite
+          .getInstance(AlphaComposite.SRC_OVER, 1.0f);
 
   /**
    * Constructor given a viewport
@@ -63,273 +47,252 @@ public class FeatureRenderer extends FeatureRendererModel
     this.av = viewport;
   }
 
-  protected void updateAvConfig()
+  /**
+   * Renders the sequence using the given feature colour between the given start
+   * and end columns. Returns true if at least one column is drawn, else false
+   * (the feature range does not overlap the start and end positions).
+   * 
+   * @param g
+   * @param seq
+   * @param featureStart
+   * @param featureEnd
+   * @param featureColour
+   * @param start
+   * @param end
+   * @param y1
+   * @param colourOnly
+   * @return
+   */
+  boolean renderFeature(Graphics g, SequenceI seq, int featureStart,
+          int featureEnd, Color featureColour, int start, int end, int y1,
+          boolean colourOnly)
   {
-    av_charHeight = av.getCharHeight();
-    av_charWidth = av.getCharWidth();
-    av_validCharWidth = av.isValidCharWidth();
-    av_isShowSeqFeatureHeight = av.isShowSequenceFeaturesHeight();
-  }
+    int charHeight = av.getCharHeight();
+    int charWidth = av.getCharWidth();
+    boolean validCharWidth = av.isValidCharWidth();
 
-  void renderFeature(Graphics g, SequenceI seq, int fstart, int fend,
-          Color featureColour, int start, int end, int y1)
-  {
-    updateAvConfig();
-    if (((fstart <= end) && (fend >= start)))
+    if (featureStart > end || featureEnd < start)
     {
-      if (fstart < start)
-      { // fix for if the feature we have starts before the sequence start,
-        fstart = start; // but the feature end is still valid!!
-      }
-
-      if (fend >= end)
-      {
-        fend = end;
-      }
-      int pady = (y1 + av_charHeight) - av_charHeight / 5;
-      for (i = fstart; i <= fend; i++)
-      {
-        s = seq.getCharAt(i);
-
-        if (jalview.util.Comparison.isGap(s))
-        {
-          continue;
-        }
-
-        g.setColor(featureColour);
-
-        g.fillRect((i - start) * av_charWidth, y1, av_charWidth,
-                av_charHeight);
-
-        if (offscreenRender || !av_validCharWidth)
-        {
-          continue;
-        }
-
-        g.setColor(Color.white);
-        charOffset = (av_charWidth - fm.charWidth(s)) / 2;
-        g.drawString(String.valueOf(s), charOffset
-                + (av_charWidth * (i - start)), pady);
+      return false;
+    }
 
-      }
+    if (featureStart < start)
+    {
+      featureStart = start;
     }
-  }
+    if (featureEnd >= end)
+    {
+      featureEnd = end;
+    }
+    int pady = (y1 + charHeight) - charHeight / 5;
 
-  void renderScoreFeature(Graphics g, SequenceI seq, int fstart, int fend,
-          Color featureColour, int start, int end, int y1, byte[] bs)
-  {
-    updateAvConfig();
-    if (((fstart <= end) && (fend >= start)))
+    FontMetrics fm = g.getFontMetrics();
+    for (int i = featureStart; i <= featureEnd; i++)
     {
-      if (fstart < start)
-      { // fix for if the feature we have starts before the sequence start,
-        fstart = start; // but the feature end is still valid!!
-      }
+      char s = seq.getCharAt(i);
 
-      if (fend >= end)
-      {
-        fend = end;
-      }
-      int pady = (y1 + av_charHeight) - av_charHeight / 5;
-      int ystrt = 0, yend = av_charHeight;
-      if (bs[0] != 0)
-      {
-        // signed - zero is always middle of residue line.
-        if (bs[1] < 128)
-        {
-          yend = av_charHeight * (128 - bs[1]) / 512;
-          ystrt = av_charHeight - yend / 2;
-        }
-        else
-        {
-          ystrt = av_charHeight / 2;
-          yend = av_charHeight * (bs[1] - 128) / 512;
-        }
-      }
-      else
+      if (Comparison.isGap(s))
       {
-        yend = av_charHeight * bs[1] / 255;
-        ystrt = av_charHeight - yend;
-
+        continue;
       }
-      for (i = fstart; i <= fend; i++)
-      {
-        s = seq.getCharAt(i);
-
-        if (jalview.util.Comparison.isGap(s))
-        {
-          continue;
-        }
 
-        g.setColor(featureColour);
-        int x = (i - start) * av_charWidth;
-        g.drawRect(x, y1, av_charWidth, av_charHeight);
-        g.fillRect(x, y1 + ystrt, av_charWidth, yend);
+      g.setColor(featureColour);
 
-        if (offscreenRender || !av_validCharWidth)
-        {
-          continue;
-        }
+      g.fillRect((i - start) * charWidth, y1, charWidth,
+              charHeight);
 
-        g.setColor(Color.black);
-        charOffset = (av_charWidth - fm.charWidth(s)) / 2;
-        g.drawString(String.valueOf(s), charOffset
-                + (av_charWidth * (i - start)), pady);
+      if (colourOnly || !validCharWidth)
+      {
+        continue;
       }
-    }
-  }
-
-  BufferedImage offscreenImage;
 
-  @Override
-  public Color findFeatureColour(Color initialCol, SequenceI seq, int res)
-  {
-    return new Color(findFeatureColour(initialCol.getRGB(), seq, res));
+      g.setColor(Color.white);
+      int charOffset = (charWidth - fm.charWidth(s)) / 2;
+      g.drawString(String.valueOf(s), charOffset
+              + (charWidth * (i - start)), pady);
+    }
+    return true;
   }
 
   /**
-   * This is used by Structure Viewers and the Overview Window to get the
-   * feature colour of the rendered sequence, returned as an RGB value
+   * Renders the sequence using the given SCORE feature colour between the given
+   * start and end columns. Returns true if at least one column is drawn, else
+   * false (the feature range does not overlap the start and end positions).
    * 
-   * @param defaultColour
+   * @param g
    * @param seq
-   * @param column
+   * @param fstart
+   * @param fend
+   * @param featureColour
+   * @param start
+   * @param end
+   * @param y1
+   * @param bs
+   * @param colourOnly
    * @return
    */
-  public synchronized int findFeatureColour(int defaultColour,
-          final SequenceI seq, int column)
+  boolean renderScoreFeature(Graphics g, SequenceI seq, int fstart,
+          int fend, Color featureColour, int start, int end, int y1,
+          byte[] bs, boolean colourOnly)
   {
-    if (!av.isShowSequenceFeatures())
+    if (fstart > end || fend < start)
     {
-      return defaultColour;
+      return false;
     }
 
-    SequenceFeature[] sequenceFeatures = seq.getSequenceFeatures();
-    if (seq != lastSeq)
+    if (fstart < start)
+    { // fix for if the feature we have starts before the sequence start,
+      fstart = start; // but the feature end is still valid!!
+    }
+
+    if (fend >= end)
+    {
+      fend = end;
+    }
+    int charHeight = av.getCharHeight();
+    int pady = (y1 + charHeight) - charHeight / 5;
+    int ystrt = 0, yend = charHeight;
+    if (bs[0] != 0)
     {
-      lastSeq = seq;
-      lastSequenceFeatures = sequenceFeatures;
-      if (lastSequenceFeatures != null)
+      // signed - zero is always middle of residue line.
+      if (bs[1] < 128)
       {
-        sfSize = lastSequenceFeatures.length;
+        yend = charHeight * (128 - bs[1]) / 512;
+        ystrt = charHeight - yend / 2;
+      }
+      else
+      {
+        ystrt = charHeight / 2;
+        yend = charHeight * (bs[1] - 128) / 512;
       }
     }
     else
     {
-      if (lastSequenceFeatures != sequenceFeatures)
+      yend = charHeight * bs[1] / 255;
+      ystrt = charHeight - yend;
+
+    }
+
+    FontMetrics fm = g.getFontMetrics();
+    int charWidth = av.getCharWidth();
+
+    for (int i = fstart; i <= fend; i++)
+    {
+      char s = seq.getCharAt(i);
+
+      if (Comparison.isGap(s))
       {
-        lastSequenceFeatures = sequenceFeatures;
-        if (lastSequenceFeatures != null)
-        {
-          sfSize = lastSequenceFeatures.length;
-        }
+        continue;
       }
+
+      g.setColor(featureColour);
+      int x = (i - start) * charWidth;
+      g.drawRect(x, y1, charWidth, charHeight);
+      g.fillRect(x, y1 + ystrt, charWidth, yend);
+
+      if (colourOnly || !av.isValidCharWidth())
+      {
+        continue;
+      }
+
+      g.setColor(Color.black);
+      int charOffset = (charWidth - fm.charWidth(s)) / 2;
+      g.drawString(String.valueOf(s), charOffset
+              + (charWidth * (i - start)), pady);
     }
+    return true;
+  }
 
-    if (lastSequenceFeatures == null || sfSize == 0)
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public Color findFeatureColour(SequenceI seq, int column, Graphics g)
+  {
+    if (!av.isShowSequenceFeatures())
     {
-      return defaultColour;
+      return null;
     }
 
-    if (jalview.util.Comparison.isGap(lastSeq.getCharAt(column)))
+    SequenceFeature[] sequenceFeatures = seq.getSequenceFeatures();
+
+    if (sequenceFeatures == null || sequenceFeatures.length == 0)
     {
-      return Color.white.getRGB();
+      return null;
     }
 
-    // Only bother making an offscreen image if transparency is applied
-    if (transparency != 1.0f && offscreenImage == null)
+    if (Comparison.isGap(seq.getCharAt(column)))
     {
-      offscreenImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
+      return Color.white;
     }
 
-    currentColour = null;
-    // TODO: non-threadsafe - each rendering thread needs its own instance of
-    // the feature renderer - or this should be synchronized.
-    offscreenRender = true;
-
-    if (offscreenImage != null)
+    Color renderedColour = null;
+    if (transparency == 1.0f)
     {
-      offscreenImage.setRGB(0, 0, defaultColour);
-      drawSequence(offscreenImage.getGraphics(), lastSeq, column, column, 0);
-
-      return offscreenImage.getRGB(0, 0);
+      /*
+       * simple case - just find the topmost rendered visible feature colour
+       */
+      renderedColour = findFeatureColour(seq, seq.findPosition(column));
     }
     else
     {
-      drawSequence(null, lastSeq, lastSeq.findPosition(column), -1, -1);
-
-      if (currentColour == null)
-      {
-        return defaultColour;
-      }
-      else
-      {
-        return currentColour.intValue();
-      }
+      /*
+       * transparency case - draw all visible features in render order to
+       * build up a composite colour on the graphics context
+       */
+      renderedColour = drawSequence(g, seq, column, column, 0, true);
     }
-
+    return renderedColour;
   }
 
-  private volatile SequenceFeature[] lastSequenceFeatures;
-
-  int sfSize;
-
-  int sfindex;
-
-  int spos;
-
-  int epos;
-
   /**
-   * Draws the sequence on the graphics context, or just determines the colour
-   * that would be drawn (if flag offscreenrender is true).
+   * Draws the sequence features on the graphics context, or just determines the
+   * colour that would be drawn (if flag colourOnly is true). Returns the last
+   * colour drawn (which may not be the effective colour if transparency
+   * applies), or null if no feature is drawn in the range given.
    * 
    * @param g
+   *          the graphics context to draw on (may be null if colourOnly==true)
    * @param seq
    * @param start
-   *          start column (or sequence position in offscreenrender mode)
+   *          start column
    * @param end
-   *          end column (not used in offscreenrender mode)
+   *          end column
    * @param y1
    *          vertical offset at which to draw on the graphics
+   * @param colourOnly
+   *          if true, only do enough to determine the colour for the position,
+   *          do not draw the character
+   * @return
    */
-  public synchronized void drawSequence(Graphics g, final SequenceI seq,
-          int start, int end, int y1)
+  public synchronized Color drawSequence(final Graphics g,
+          final SequenceI seq, int start, int end, int y1,
+          boolean colourOnly)
   {
     SequenceFeature[] sequenceFeatures = seq.getSequenceFeatures();
     if (sequenceFeatures == null || sequenceFeatures.length == 0)
     {
-      return;
-    }
-
-    if (g != null)
-    {
-      fm = g.getFontMetrics();
+      return null;
     }
 
     updateFeatures();
 
-    if (lastSeq == null || seq != lastSeq
-            || sequenceFeatures != lastSequenceFeatures)
-    {
-      lastSeq = seq;
-      lastSequenceFeatures = sequenceFeatures;
-    }
-
-    if (transparency != 1 && g != null)
+    if (transparency != 1f && g != null)
     {
       Graphics2D g2 = (Graphics2D) g;
       g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
               transparency));
     }
 
-    if (!offscreenRender)
-    {
-      spos = lastSeq.findPosition(start);
-      epos = lastSeq.findPosition(end);
-    }
+    int startPos = seq.findPosition(start);
+    int endPos = seq.findPosition(end);
+
+    int sfSize = sequenceFeatures.length;
+    Color drawnColour = null;
 
-    sfSize = lastSequenceFeatures.length;
+    /*
+     * iterate over features in ordering of their rendering (last is on top)
+     */
     for (int renderIndex = 0; renderIndex < renderOrder.length; renderIndex++)
     {
       String type = renderOrder[renderIndex];
@@ -340,27 +303,29 @@ public class FeatureRenderer extends FeatureRendererModel
 
       // loop through all features in sequence to find
       // current feature to render
-      for (sfindex = 0; sfindex < sfSize; sfindex++)
+      for (int sfindex = 0; sfindex < sfSize; sfindex++)
       {
-        final SequenceFeature sequenceFeature = lastSequenceFeatures[sfindex];
+        final SequenceFeature sequenceFeature = sequenceFeatures[sfindex];
         if (!sequenceFeature.type.equals(type))
         {
           continue;
         }
 
+        /*
+         * a feature type may be flagged as shown but the group 
+         * an instance of it belongs to may be hidden
+         */
         if (featureGroupNotShown(sequenceFeature))
         {
           continue;
         }
 
         /*
-         * check feature overlaps the visible part of the alignment, 
-         * unless doing offscreenRender (to the Overview window or a 
-         * structure viewer) which is not limited 
+         * check feature overlaps the target range
+         * TODO: efficient retrieval of features overlapping a range
          */
-        if (!offscreenRender
-                && (sequenceFeature.getBegin() > epos || sequenceFeature
-                        .getEnd() < spos))
+        if (sequenceFeature.getBegin() > endPos
+                || sequenceFeature.getEnd() < startPos)
         {
           continue;
         }
@@ -368,58 +333,46 @@ public class FeatureRenderer extends FeatureRendererModel
         Color featureColour = getColour(sequenceFeature);
         boolean isContactFeature = sequenceFeature.isContactFeature();
 
-        if (offscreenRender && offscreenImage == null)
-        {
-          /*
-           * offscreen mode with no image (image is only needed if transparency 
-           * is applied to feature colours) - just check feature is rendered at 
-           * the requested position (start == sequence position in this mode)
-           */
-          boolean featureIsAtPosition = sequenceFeature.begin <= start
-                  && sequenceFeature.end >= start;
-          if (isContactFeature)
-          {
-            featureIsAtPosition = sequenceFeature.begin == start
-                    || sequenceFeature.end == start;
-          }
-          if (featureIsAtPosition)
-          {
-            // this is passed out to the overview and other sequence renderers
-            // (e.g. molecule viewer) to get displayed colour for rendered
-            // sequence
-            currentColour = new Integer(featureColour.getRGB());
-            // used to be retreived from av.featuresDisplayed
-            // currentColour = av.featuresDisplayed
-            // .get(sequenceFeatures[sfindex].type);
-
-          }
-        }
-        else if (isContactFeature)
+        if (isContactFeature)
         {
-          renderFeature(g, seq, seq.findIndex(sequenceFeature.begin) - 1,
+          boolean drawn = renderFeature(g, seq,
+                  seq.findIndex(sequenceFeature.begin) - 1,
                   seq.findIndex(sequenceFeature.begin) - 1, featureColour,
-                  start, end, y1);
-          renderFeature(g, seq, seq.findIndex(sequenceFeature.end) - 1,
+                  start, end, y1, colourOnly);
+          drawn |= renderFeature(g, seq,
+                  seq.findIndex(sequenceFeature.end) - 1,
                   seq.findIndex(sequenceFeature.end) - 1, featureColour,
-                  start, end, y1);
-
+                  start, end, y1, colourOnly);
+          if (drawn)
+          {
+            drawnColour = featureColour;
+          }
         }
         else if (showFeature(sequenceFeature))
         {
-          if (av_isShowSeqFeatureHeight
+          if (av.isShowSequenceFeaturesHeight()
                   && !Float.isNaN(sequenceFeature.score))
           {
-            renderScoreFeature(g, seq,
+            boolean drawn = renderScoreFeature(g, seq,
                     seq.findIndex(sequenceFeature.begin) - 1,
-                    seq.findIndex(sequenceFeature.end) - 1,
-                    featureColour, start, end, y1,
-                    normaliseScore(sequenceFeature));
+                    seq.findIndex(sequenceFeature.end) - 1, featureColour,
+                    start, end, y1, normaliseScore(sequenceFeature),
+                    colourOnly);
+            if (drawn)
+            {
+              drawnColour = featureColour;
+            }
           }
           else
           {
-            renderFeature(g, seq, seq.findIndex(sequenceFeature.begin) - 1,
-                    seq.findIndex(sequenceFeature.end) - 1,
-                    featureColour, start, end, y1);
+            boolean drawn = renderFeature(g, seq,
+                    seq.findIndex(sequenceFeature.begin) - 1,
+                    seq.findIndex(sequenceFeature.end) - 1, featureColour,
+                    start, end, y1, colourOnly);
+            if (drawn)
+            {
+              drawnColour = featureColour;
+            }
           }
         }
       }
@@ -427,10 +380,14 @@ public class FeatureRenderer extends FeatureRendererModel
 
     if (transparency != 1.0f && g != null)
     {
+      /*
+       * reset transparency
+       */
       Graphics2D g2 = (Graphics2D) g;
-      g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
-              1.0f));
+      g2.setComposite(NO_TRANSPARENCY);
     }
+
+    return drawnColour;
   }
 
   /**
@@ -459,7 +416,78 @@ public class FeatureRenderer extends FeatureRendererModel
   @Override
   public void featuresAdded()
   {
-    lastSeq = null;
     findAllFeatures();
   }
+
+  /**
+   * Returns the sequence feature colour rendered at the given sequence
+   * position, or null if none found. The feature of highest render order (i.e.
+   * on top) is found, subject to both feature type and feature group being
+   * visible, and its colour returned.
+   * 
+   * @param seq
+   * @param pos
+   * @return
+   */
+  Color findFeatureColour(SequenceI seq, int pos)
+  {
+    SequenceFeature[] sequenceFeatures = seq.getSequenceFeatures();
+    if (sequenceFeatures == null || sequenceFeatures.length == 0)
+    {
+      return null;
+    }
+  
+    /*
+     * check for new feature added while processing
+     */
+    updateFeatures();
+
+    /*
+     * inspect features in reverse renderOrder (the last in the array is 
+     * displayed on top) until we find one that is rendered at the position
+     */
+    for (int renderIndex = renderOrder.length - 1; renderIndex >= 0; renderIndex--)
+    {
+      String type = renderOrder[renderIndex];
+      if (!showFeatureOfType(type))
+      {
+        continue;
+      }
+
+      for (int sfindex = 0; sfindex < sequenceFeatures.length; sfindex++)
+      {
+        SequenceFeature sequenceFeature = sequenceFeatures[sfindex];
+        if (!sequenceFeature.type.equals(type))
+        {
+          continue;
+        }
+
+        if (featureGroupNotShown(sequenceFeature))
+        {
+          continue;
+        }
+
+        /*
+         * check the column position is within the feature range
+         * (or is one of the two contact positions for a contact feature)
+         */
+        boolean featureIsAtPosition = sequenceFeature.begin <= pos
+                && sequenceFeature.end >= pos;
+        if (sequenceFeature.isContactFeature())
+        {
+          featureIsAtPosition = sequenceFeature.begin == pos
+                  || sequenceFeature.end == pos;
+        }
+        if (featureIsAtPosition)
+        {
+          return getColour(sequenceFeature);
+        }
+      }
+    }
+  
+    /*
+     * no displayed feature found at position
+     */
+    return null;
+  }
 }