JAL-2754 Sequence.findFeatures(fromCol, toCol)
[jalview.git] / src / jalview / renderer / seqfeatures / FeatureRenderer.java
index 26bc895..5e071f9 100644 (file)
  */
 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;
 import java.awt.Color;
 import java.awt.FontMetrics;
 import java.awt.Graphics;
 import java.awt.Graphics2D;
-import java.awt.image.BufferedImage;
+import java.util.List;
 
-public class FeatureRenderer extends
-        jalview.viewmodel.seqfeatures.FeatureRendererModel
+public class FeatureRenderer extends FeatureRendererModel
 {
+  private static final AlphaComposite NO_TRANSPARENCY = AlphaComposite
+          .getInstance(AlphaComposite.SRC_OVER, 1.0f);
 
-  FontMetrics fm;
-
-  int charOffset;
-
-  boolean offscreenRender = false;
-
-  protected SequenceI lastSeq;
-
-  char s;
-
-  int i;
-
-  int av_charHeight, av_charWidth;
-
-  boolean av_validCharWidth, av_isShowSeqFeatureHeight;
-
-  protected void updateAvConfig()
+  /**
+   * Constructor given a viewport
+   * 
+   * @param viewport
+   */
+  public FeatureRenderer(AlignViewportI viewport)
   {
-    av_charHeight = av.getCharHeight();
-    av_charWidth = av.getCharWidth();
-    av_validCharWidth = av.isValidCharWidth();
-    av_isShowSeqFeatureHeight = av.isShowSequenceFeaturesHeight();
+    this.av = viewport;
   }
 
-  void renderFeature(Graphics g, SequenceI seq, int fstart, int fend,
-          Color featureColour, int start, int end, int y1)
+  /**
+   * 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)
   {
-    updateAvConfig();
-    if (((fstart <= end) && (fend >= 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!!
-      }
+    int charHeight = av.getCharHeight();
+    int charWidth = av.getCharWidth();
+    boolean validCharWidth = av.isValidCharWidth();
 
-      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);
+    if (featureStart > end || featureEnd < start)
+    {
+      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)
+      if (Comparison.isGap(s))
       {
-        // 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
-      {
-        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);
 
-        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);
-
-        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;
 
-  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 the Molecule Viewer and Overview to get the accurate
-   * colourof the rendered sequence
+   * 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 g
+   * @param seq
+   * @param fstart
+   * @param fend
+   * @param featureColour
+   * @param start
+   * @param end
+   * @param y1
+   * @param bs
+   * @param colourOnly
+   * @return
    */
-  public synchronized int findFeatureColour(int initialCol,
-          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 initialCol;
+      return false;
+    }
+
+    if (fstart < start)
+    { // fix for if the feature we have starts before the sequence start,
+      fstart = start; // but the feature end is still valid!!
     }
 
-    SequenceFeature[] sequenceFeatures = seq.getSequenceFeatures();
-    if (seq != lastSeq)
+    if (fend >= end)
     {
-      lastSeq = seq;
-      lastSequenceFeatures = sequenceFeatures;
-      if (lastSequenceFeatures != null)
-      {
-        sfSize = lastSequenceFeatures.length;
-      }
+      fend = end;
     }
-    else
+    int charHeight = av.getCharHeight();
+    int pady = (y1 + charHeight) - charHeight / 5;
+    int ystrt = 0, yend = charHeight;
+    if (bs[0] != 0)
     {
-      if (lastSequenceFeatures != sequenceFeatures)
+      // signed - zero is always middle of residue line.
+      if (bs[1] < 128)
       {
-        lastSequenceFeatures = sequenceFeatures;
-        if (lastSequenceFeatures != null)
-        {
-          sfSize = lastSequenceFeatures.length;
-        }
+        yend = charHeight * (128 - bs[1]) / 512;
+        ystrt = charHeight - yend / 2;
+      }
+      else
+      {
+        ystrt = charHeight / 2;
+        yend = charHeight * (bs[1] - 128) / 512;
       }
     }
-
-    if (lastSequenceFeatures == null || sfSize == 0)
-    {
-      return initialCol;
-    }
-
-    if (jalview.util.Comparison.isGap(lastSeq.getCharAt(column)))
+    else
     {
-      return Color.white.getRGB();
-    }
+      yend = charHeight * bs[1] / 255;
+      ystrt = charHeight - yend;
 
-    // Only bother making an offscreen image if transparency is applied
-    if (transparency != 1.0f && offscreenImage == null)
-    {
-      offscreenImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
     }
 
-    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)
-    {
-      offscreenImage.setRGB(0, 0, initialCol);
-      drawSequence(offscreenImage.getGraphics(), lastSeq, column, column, 0);
+    FontMetrics fm = g.getFontMetrics();
+    int charWidth = av.getCharWidth();
 
-      return offscreenImage.getRGB(0, 0);
-    }
-    else
+    for (int i = fstart; i <= fend; i++)
     {
-      drawSequence(null, lastSeq, lastSeq.findPosition(column), -1, -1);
+      char s = seq.getCharAt(i);
 
-      if (currentColour == null)
+      if (Comparison.isGap(s))
       {
-        return initialCol;
+        continue;
       }
-      else
+
+      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())
       {
-        return ((Integer) currentColour).intValue();
+        continue;
       }
-    }
 
+      g.setColor(Color.black);
+      int charOffset = (charWidth - fm.charWidth(s)) / 2;
+      g.drawString(String.valueOf(s), charOffset
+              + (charWidth * (i - start)), pady);
+    }
+    return true;
   }
 
-  private volatile SequenceFeature[] lastSequenceFeatures;
-
-  int sfSize;
-
-  int sfindex;
-
-  int spos;
-
-  int epos;
-
-  public synchronized void drawSequence(Graphics g, final SequenceI seq,
-          int start, int end, int y1)
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public Color findFeatureColour(SequenceI seq, int column, Graphics g)
   {
-    SequenceFeature[] sequenceFeatures = seq.getSequenceFeatures();
-    if (sequenceFeatures == null || sequenceFeatures.length == 0)
+    if (!av.isShowSequenceFeatures())
     {
-      return;
+      return null;
     }
 
-    if (g != null)
+    if (Comparison.isGap(seq.getCharAt(column)))
     {
-      fm = g.getFontMetrics();
+      return Color.white;
     }
 
-    updateFeatures();
+    Color renderedColour = null;
+    if (transparency == 1.0f)
+    {
+      /*
+       * simple case - just find the topmost rendered visible feature colour
+       */
+      renderedColour = findFeatureColour(seq, column);
+    }
+    else
+    {
+      /*
+       * 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;
+  }
 
-    if (lastSeq == null || seq != lastSeq
-            || sequenceFeatures != lastSequenceFeatures)
+  /**
+   * 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
+   * @param end
+   *          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 Color drawSequence(final Graphics g,
+          final SequenceI seq, int start, int end, int y1,
+          boolean colourOnly)
+  {
+    if (!seq.getFeatures().hasFeatures())
     {
-      lastSeq = seq;
-      lastSequenceFeatures = sequenceFeatures;
+      return null;
     }
 
-    if (transparency != 1 && g != null)
+    updateFeatures();
+
+    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);
-    }
+    Color drawnColour = null;
 
-    sfSize = lastSequenceFeatures.length;
-    String type;
+    /*
+     * iterate over features in ordering of their rendering (last is on top)
+     */
     for (int renderIndex = 0; renderIndex < renderOrder.length; renderIndex++)
     {
-      type = renderOrder[renderIndex];
-
-      if (type == null || !showFeatureOfType(type))
+      String type = renderOrder[renderIndex];
+      if (!showFeatureOfType(type))
       {
         continue;
       }
 
-      // loop through all features in sequence to find
-      // current feature to render
-      for (sfindex = 0; sfindex < sfSize; sfindex++)
+      List<SequenceFeature> overlaps = seq.findFeatures(start + 1, end + 1,
+              type);
+      for (SequenceFeature sf : overlaps)
       {
-        final SequenceFeature sequenceFeature = lastSequenceFeatures[sfindex];
-        if (!sequenceFeature.type.equals(type))
-        {
-          continue;
-        }
-
-        if (featureGroups != null
-                && sequenceFeature.featureGroup != null
-                && sequenceFeature.featureGroup.length() != 0
-                && featureGroups.containsKey(sequenceFeature.featureGroup)
-                && !featureGroups.get(sequenceFeature.featureGroup)
-                        .booleanValue())
+        /*
+         * a feature type may be flagged as shown but the group 
+         * an instance of it belongs to may be hidden
+         */
+        if (featureGroupNotShown(sf))
         {
           continue;
         }
 
-        if (!offscreenRender
-                && (sequenceFeature.getBegin() > epos || sequenceFeature
-                        .getEnd() < spos))
-        {
-          continue;
-        }
+        Color featureColour = getColour(sf);
+        boolean isContactFeature = sf.isContactFeature();
 
-        if (offscreenRender && offscreenImage == null)
+        int featureStartCol = seq.findIndex(sf.begin);
+        int featureEndCol = sf.begin == sf.end ? featureStartCol : seq
+                .findIndex(sf.end);
+        if (isContactFeature)
         {
-          if (sequenceFeature.begin <= start
-                  && sequenceFeature.end >= start)
+          boolean drawn = renderFeature(g, seq, featureStartCol - 1,
+                  featureStartCol - 1, featureColour, start, end, y1,
+                  colourOnly);
+          drawn |= renderFeature(g, seq, featureEndCol - 1,
+                  featureEndCol - 1, featureColour, start, end, y1,
+                  colourOnly);
+          if (drawn)
           {
-            // 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(getColour(sequenceFeature).getRGB());
-            // used to be retreived from av.featuresDisplayed
-            // currentColour = av.featuresDisplayed
-            // .get(sequenceFeatures[sfindex].type);
-
+            drawnColour = featureColour;
           }
         }
-        else if (sequenceFeature.type.equals("disulfide bond"))
-        {
-          renderFeature(g, seq, seq.findIndex(sequenceFeature.begin) - 1,
-                  seq.findIndex(sequenceFeature.begin) - 1,
-                  getColour(sequenceFeature)
-                  // new Color(((Integer) av.featuresDisplayed
-                  // .get(sequenceFeatures[sfindex].type)).intValue())
-                  , start, end, y1);
-          renderFeature(g, seq, seq.findIndex(sequenceFeature.end) - 1,
-                  seq.findIndex(sequenceFeature.end) - 1,
-                  getColour(sequenceFeature)
-                  // new Color(((Integer) av.featuresDisplayed
-                  // .get(sequenceFeatures[sfindex].type)).intValue())
-                  , start, end, y1);
-
-        }
-        else if (showFeature(sequenceFeature))
+        else if (showFeature(sf))
         {
-          if (av_isShowSeqFeatureHeight
+          /*
+           * showing feature score by height of colour
+           * is not implemented as a selectable option 
+           *
+          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,
-                    getColour(sequenceFeature), 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,
-                    getColour(sequenceFeature), start, end, y1);
-          }
+          */
+            boolean drawn = renderFeature(g, seq,
+                    featureStartCol - 1,
+                    featureEndCol - 1, featureColour,
+                    start, end, y1, colourOnly);
+            if (drawn)
+            {
+              drawnColour = featureColour;
+            }
+          /*}*/
         }
-
       }
-
     }
 
-    if (transparency != 1.0f && g != null && transparencyAvailable)
+    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);
     }
-  }
 
-  boolean transparencyAvailable = true;
-
-  protected void setTransparencyAvailable(boolean isTransparencyAvailable)
-  {
-    transparencyAvailable = isTransparencyAvailable;
-  }
-
-  @Override
-  public boolean isTransparencyAvailable()
-  {
-    return transparencyAvailable;
+    return drawnColour;
   }
 
   /**
@@ -415,9 +374,60 @@ public class FeatureRenderer extends
    * discover and display.
    * 
    */
+  @Override
   public void featuresAdded()
   {
-    lastSeq = null;
     findAllFeatures();
   }
+
+  /**
+   * Returns the sequence feature colour rendered at the given column 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.
+   * <p>
+   * Note this method does not check for a gap in the column so would return the
+   * colour for features enclosing a gapped column. Check for gap before calling
+   * if different behaviour is wanted.
+   * 
+   * @param seq
+   * @param column
+   *          (1..)
+   * @return
+   */
+  Color findFeatureColour(SequenceI seq, int column)
+  {
+    /*
+     * 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;
+      }
+
+      List<SequenceFeature> overlaps = seq.findFeatures(column, column,
+              type);
+      for (SequenceFeature sequenceFeature : overlaps)
+      {
+        if (!featureGroupNotShown(sequenceFeature))
+        {
+          return getColour(sequenceFeature);
+        }
+      }
+    }
+  
+    /*
+     * no displayed feature found at position
+     */
+    return null;
+  }
 }