JAL-4375 Add an AnnotationColouringI interface, and generic AnnotationColouringRanges...
[jalview.git] / src / jalview / renderer / AnnotationRenderer.java
index cba36d2..524e291 100644 (file)
@@ -32,7 +32,11 @@ import java.awt.Stroke;
 import java.awt.geom.AffineTransform;
 import java.awt.image.ImageObserver;
 import java.util.BitSet;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
 
 import org.jfree.graphics2d.svg.SVGGraphics2D;
 import org.jibble.epsgraphics.EpsGraphics2D;
@@ -49,6 +53,7 @@ import jalview.datamodel.Annotation;
 import jalview.datamodel.ColumnSelection;
 import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.ProfilesI;
+import jalview.datamodel.annotations.AnnotationColouringI;
 import jalview.renderer.api.AnnotationRendererFactoryI;
 import jalview.renderer.api.AnnotationRowRendererI;
 import jalview.schemes.ColourSchemeI;
@@ -140,6 +145,51 @@ public class AnnotationRenderer
    */
   private boolean canClip = false;
 
+  /**
+   * Property to set text antialiasing method
+   */
+  private static final String TEXT_ANTIALIAS_METHOD = "TEXT_ANTIALIAS_METHOD";
+
+  private static final Object textAntialiasMethod;
+
+  static
+  {
+    final String textAntialiasMethodPref = Cache
+            .getDefault(TEXT_ANTIALIAS_METHOD, "GASP");
+    switch (textAntialiasMethodPref)
+    {
+    case "ON":
+      textAntialiasMethod = RenderingHints.VALUE_TEXT_ANTIALIAS_ON;
+      break;
+    case "GASP":
+      textAntialiasMethod = RenderingHints.VALUE_TEXT_ANTIALIAS_GASP;
+      break;
+    case "LCD_HBGR":
+      textAntialiasMethod = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HBGR;
+      break;
+    case "LCD_HRGB":
+      textAntialiasMethod = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB;
+      break;
+    case "LCD_VBGR":
+      textAntialiasMethod = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VBGR;
+      break;
+    case "LCD_VRGB":
+      textAntialiasMethod = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VRGB;
+      break;
+    case "OFF":
+      textAntialiasMethod = RenderingHints.VALUE_TEXT_ANTIALIAS_OFF;
+      break;
+    case "DEFAULT":
+      textAntialiasMethod = RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT;
+      break;
+    default:
+      jalview.bin.Console.warn(TEXT_ANTIALIAS_METHOD + " value '"
+              + textAntialiasMethodPref
+              + "' not recognised, defaulting to 'GASP'. See https://docs.oracle.com/javase/8/docs/api/java/awt/RenderingHints.html#KEY_TEXT_ANTIALIASING");
+      textAntialiasMethod = RenderingHints.VALUE_TEXT_ANTIALIAS_GASP;
+    }
+  }
+
   public AnnotationRenderer()
   {
     this(false);
@@ -175,7 +225,6 @@ public class AnnotationRenderer
           int x, int y, int iconOffset, int startRes, int column,
           boolean validRes, boolean validEnd)
   {
-    g.setColor(STEM_COLOUR);
     int sCol = (lastSSX / charWidth)
             + hiddenColumns.visibleToAbsoluteColumn(startRes);
     int x1 = lastSSX;
@@ -185,11 +234,19 @@ public class AnnotationRenderer
             : row_annotations[column - 1].secondaryStructure;
 
     boolean diffupstream = sCol == 0 || row_annotations[sCol - 1] == null
-            || dc != row_annotations[sCol - 1].secondaryStructure || !validEnd;
+            || dc != row_annotations[sCol - 1].secondaryStructure
+            || !validEnd;
     boolean diffdownstream = !validRes || !validEnd
             || row_annotations[column] == null
             || dc != row_annotations[column].secondaryStructure;
 
+    if (diffupstream || diffdownstream)
+    {
+      // draw glyphline under arrow
+      drawGlyphLine(g, lastSSX, x, y, iconOffset);
+    }
+    g.setColor(STEM_COLOUR);
+
     if (column > 0 && Rna.isClosingParenthesis(dc))
     {
       if (diffupstream)
@@ -202,7 +259,8 @@ public class AnnotationRenderer
          */
         fillPolygon(g, new int[] { lastSSX + 5, lastSSX + 5, lastSSX },
                 new int[]
-                { y + iconOffset, y + 13 + iconOffset, y + 7 + iconOffset },
+                { y + iconOffset + 1, y + 13 + iconOffset,
+                    y + 7 + iconOffset },
                 3);
         x1 += 5;
       }
@@ -220,9 +278,10 @@ public class AnnotationRenderer
          * if annotation ending with an opeing base pair half of the stem, 
          * display a forward arrow
          */
-        fillPolygon(g, new int[] { x2 - 5, x2 - 5, x2 },
+        fillPolygon(g, new int[] { x2 - 6, x2 - 6, x2 - 1 },
                 new int[]
-                { y + iconOffset, y + 13 + iconOffset, y + 7 + iconOffset },
+                { y + iconOffset + 1, y + 13 + iconOffset,
+                    y + 7 + iconOffset },
                 3);
         x2 -= 5;
       }
@@ -232,6 +291,7 @@ public class AnnotationRenderer
       }
     }
     // draw arrow body
+    unsetAntialias(g);
     fillRect(g, x1, y + 4 + iconOffset, x2 - x1, 6);
   }
 
@@ -251,7 +311,8 @@ public class AnnotationRenderer
             : row_annotations[column - 1].displayCharacter;
 
     boolean diffupstream = sCol == 0 || row_annotations[sCol - 1] == null
-            || !dc.equals(row_annotations[sCol - 1].displayCharacter) || !validEnd;
+            || !dc.equals(row_annotations[sCol - 1].displayCharacter)
+            || !validEnd;
     boolean diffdownstream = !validRes || !validEnd
             || row_annotations[column] == null
             || !dc.equals(row_annotations[column].displayCharacter);
@@ -467,6 +528,14 @@ public class AnnotationRenderer
           AlignViewportI av, Graphics g, int activeRow, int startRes,
           int endRes)
   {
+    return drawComponent(annotPanel, av, g, activeRow, startRes, endRes,
+            false);
+  }
+
+  public boolean drawComponent(AwtRenderPanelI annotPanel,
+          AlignViewportI av, Graphics g, int activeRow, int startRes,
+          int endRes, boolean forExport)
+  {
     if (g instanceof EpsGraphics2D || g instanceof SVGGraphics2D)
     {
       this.setVectorRendering(true);
@@ -505,7 +574,6 @@ public class AnnotationRenderer
             .getComplementConsensusAnnotation();
 
     BitSet graphGroupDrawn = new BitSet();
-    int charOffset = 0; // offset for a label
     // \u03B2 \u03B1
     // debug ints
     int yfrom = 0, f_i = 0, yto = 0, f_to = 0;
@@ -626,6 +694,7 @@ public class AnnotationRenderer
         // flag used for vector rendition
         this.glyphLineDrawn = false;
         x = (startRes == 0) ? 0 : -1;
+
         while (x < endRes - startRes)
         {
           if (hasHiddenColumns)
@@ -682,9 +751,7 @@ public class AnnotationRenderer
             if (validCharWidth && validRes && displayChar != null
                     && (displayChar.length() > 0))
             {
-              // Graphics2D gg = (g);
-              float fmWidth = fm.charsWidth(displayChar.toCharArray(), 0,
-                      displayChar.length());
+              float fmWidth = fm.stringWidth(displayChar);
 
               /*
                * shrink label width to fit in column, if that is
@@ -695,13 +762,12 @@ public class AnnotationRenderer
               if (scaleColLabel && fmWidth > charWidth)
               {
                 scaledToFit = true;
-                fmScaling = charWidth;
-                fmScaling /= fmWidth;
+                fmScaling = (float) charWidth / fmWidth;
                 // and update the label's width to reflect the scaling.
                 fmWidth = charWidth;
               }
 
-              charOffset = (int) ((charWidth - fmWidth) / 2f);
+              float charOffset = (charWidth - fmWidth) / 2f;
 
               if (row_annotations[column].colour == null)
               {
@@ -716,26 +782,42 @@ public class AnnotationRenderer
                * draw the label, unless it is the same secondary structure
                * symbol (excluding RNA Helix) as the previous column
                */
-              final int xPos = (x * charWidth) + charOffset;
-              final int yPos = y + iconOffset;
-
+              final float xPos = (x * charWidth) + charOffset;
+              final float yPos = y + iconOffset;
+
+              // Act on a copy of the Graphics2d object
+              Graphics2D g2dCopy = (Graphics2D) g2d.create();
+              // Clip to this annotation line (particularly width).
+              // This removes artifacts from text when side scrolling
+              // (particularly in wrap format), but can result in clipped
+              // characters until a full paint is drawn.
+              // Add charHeight allowance above and below annotation for
+              // character overhang.
+              // If we're in an image export, set the clip width to be the
+              // entire width of the annotation.
+
+              int clipWidth = forExport
+                      ? row_annotations.length * charWidth - 1
+                      : imgWidth - 1;
+              g2dCopy.setClip(0, (int) yPos - 2 * charHeight, clipWidth,
+                      charHeight * 3);
               /*
                * translate to drawing position _before_ applying any scaling
                */
-              g2d.translate(xPos, yPos);
+              g2dCopy.translate(xPos, yPos);
               if (scaledToFit)
               {
                 /*
                  * use a scaling transform to make the label narrower
                  * (JalviewJS doesn't have Font.deriveFont(AffineTransform))
                  */
-                g2d.transform(
+                g2dCopy.transform(
                         AffineTransform.getScaleInstance(fmScaling, 1.0));
               }
-              setAntialias(g);
+              setAntialias(g2dCopy, true);
               if (column == 0 || row.graph > 0)
               {
-                g2d.drawString(displayChar, 0, 0);
+                g2dCopy.drawString(displayChar, 0, 0);
               }
               else if (row_annotations[column - 1] == null || (labelAllCols
                       || !displayChar.equals(
@@ -743,18 +825,9 @@ public class AnnotationRenderer
                       || (displayChar.length() < 2
                               && row_annotations[column].secondaryStructure == ' ')))
               {
-                g2d.drawString(displayChar, 0, 0);
-              }
-              if (scaledToFit)
-              {
-                /*
-                 * undo scaling before translating back 
-                 * (restoring saved transform does NOT work in JS PDFGraphics!)
-                 */
-                g2d.transform(AffineTransform
-                        .getScaleInstance(1D / fmScaling, 1.0));
+                g2dCopy.drawString(displayChar, 0, 0);
               }
-              g2d.translate(-xPos, -yPos);
+              g2dCopy.dispose();
             }
           }
           if (row.hasIcons)
@@ -812,7 +885,7 @@ public class AnnotationRenderer
             if (!validRes || (ss != lastSS))
             {
 
-              if (x > -1)
+              if (x > 0)
               {
 
                 // int nb_annot = x - temp;
@@ -1129,7 +1202,7 @@ public class AnnotationRenderer
               renderer.renderRow(g, charWidth, charHeight, hasHiddenColumns,
                       av, hiddenColumns, columnSelection, row,
                       row_annotations, startRes, endRes, row.graphMin,
-                      row.graphMax, y);
+                      row.graphMax, y, isVectorRendering());
             }
             if (debugRedraw)
             {
@@ -1267,7 +1340,6 @@ public class AnnotationRenderer
       }
       else
       {
-        // g.setColor(Color.orange);
         fillRoundRect(g, lastSSX, y + 3 + iconOffset, x2 - x1 - ofs, 8, 0,
                 0);
       }
@@ -1278,7 +1350,6 @@ public class AnnotationRenderer
       }
       else
       {
-        // g.setColor(Color.magenta);
         fillRoundRect(g, lastSSX + ofs, y + 3 + iconOffset, x2 - x1 - ofs,
                 8, 0, 0);
       }
@@ -1339,11 +1410,6 @@ public class AnnotationRenderer
 
     eRes = Math.min(eRes, aa_annotations.length);
 
-    if (sRes == 0)
-    {
-      x++;
-    }
-
     int y1 = y, y2 = y;
     float range = max - min;
 
@@ -1354,15 +1420,20 @@ public class AnnotationRenderer
     }
 
     g.setColor(Color.gray);
-    drawLine(g, squareStroke, x * charWidth - charWidth, y2,
-            (eRes - sRes) * charWidth, y2);
+    drawLine(g, squareStroke, x * charWidth, y2, (eRes - sRes) * charWidth,
+            y2);
+
+    if (sRes == 0)
+    {
+      x++;
+    }
 
     eRes = Math.min(eRes, aa_annotations.length);
 
     int column;
     int aaMax = aa_annotations.length - 1;
 
-    while (x < eRes - sRes)
+    while (x <= eRes - sRes)
     {
       column = sRes + x;
       if (hasHiddenColumns)
@@ -1381,6 +1452,7 @@ public class AnnotationRenderer
         continue;
       }
 
+      boolean individualColour = false;
       if (aa_annotations[column].colour == null)
       {
         g.setColor(Color.black);
@@ -1388,34 +1460,77 @@ public class AnnotationRenderer
       else
       {
         g.setColor(aa_annotations[column].colour);
+        individualColour = true;
+      }
+
+      boolean value1Exists = column > 0
+              && aa_annotations[column - 1] != null;
+      float value1 = 0f;
+      Color color1 = null;
+      if (value1Exists)
+      {
+        value1 = aa_annotations[column - 1].value;
+        color1 = aa_annotations[column - 1].colour;
       }
+      float value2 = aa_annotations[column].value;
+      boolean nextValueExists = aa_annotations.length > column + 1
+              && aa_annotations[column + 1] != null;
 
-      if (aa_annotations[column - 1] == null
-              && aa_annotations.length > column + 1
-              && aa_annotations[column + 1] == null)
+      // check for standalone value
+      if (!value1Exists && !nextValueExists)
       {
-        // standalone value
-        y1 = y - (int) (((aa_annotations[column].value - min) / range)
-                * graphHeight);
-        drawLine(g, x * charWidth + charWidth / 4, y1,
-                x * charWidth + 3 * charWidth / 4, y1);
+        y2 = y - yValueToPixelHeight(value2, min, range, graphHeight);
+        drawLine(g, x * charWidth + charWidth / 4, y2,
+                x * charWidth + 3 * charWidth / 4, y2);
         x++;
         continue;
       }
 
-      if (aa_annotations[column - 1] == null)
+      if (!value1Exists)
       {
         x++;
         continue;
       }
 
-      y1 = y - (int) (((aa_annotations[column - 1].value - min) / range)
-              * graphHeight);
-      y2 = y - (int) (((aa_annotations[column].value - min) / range)
-              * graphHeight);
+      y1 = y - yValueToPixelHeight(value1, min, range, graphHeight);
+      y2 = y - yValueToPixelHeight(value2, min, range, graphHeight);
 
-      drawLine(g, (x - 1) * charWidth + charWidth / 2, y1,
-              x * charWidth + charWidth / 2, y2);
+      float v1 = value1;
+      float v2 = value2;
+      int a1 = (x - 1) * charWidth + charWidth / 2;
+      int b1 = y1;
+      int a2 = x * charWidth + charWidth / 2;
+      int b2 = y2;
+      if (x == 0)
+      {
+        // only draw an initial half-line
+        a1 = x * charWidth;
+        b1 = y1 + (y2 - y1) / 2;
+        v1 = value1 + (value2 - value1) / 2;
+      }
+      else if (x == eRes - sRes)
+      {
+        // this is one past the end to draw -- only draw the first half of the
+        // line
+        a2 = x * charWidth - 1;
+        b2 = y1 + (y2 - y1) / 2;
+        v2 = value1 + (value2 - value1) / 2;
+      }
+      else
+      {
+      }
+      AnnotationColouringI ac = aa_annotations[column]
+              .getAnnotationColouring();
+      List<Map.Entry<Float, Color>> valCols = ac == null ? null
+              : ac.rangeColours(v1, v2);
+      if (valCols != null)
+      {
+        drawSegmentedLine(g, valCols, a1, b1, a2, b2);
+      }
+      else
+      {
+        drawLine(g, a1, b1, a2, b2);
+      }
       x++;
     }
 
@@ -1423,16 +1538,137 @@ public class AnnotationRenderer
     {
       g.setColor(_aa.threshold.colour);
       Graphics2D g2 = (Graphics2D) g;
-      Stroke s = new BasicStroke(1, BasicStroke.CAP_SQUARE,
-              BasicStroke.JOIN_ROUND, 3f, new float[]
-              { 5f, 3f }, 0f);
-
       y2 = (int) (y - ((_aa.threshold.value - min) / range) * graphHeight);
-      drawLine(g, s, 0, y2, (eRes - sRes) * charWidth, y2);
+      drawLine(g, dashedLine(charWidth), 0, y2, (eRes - sRes) * charWidth,
+              y2);
     }
     g2d.setStroke(prevStroke);
   }
 
+  private static double log2 = Math.log(2);
+
+  // Cached dashed line Strokes
+  private static Map<Integer, Stroke> dashedLineLookup = new HashMap<>();
+
+  /**
+   * Returns a dashed line stroke as close to 6-4 pixels as fits within the
+   * charWidth. This allows translations of multiples of charWidth without
+   * disrupting the dashed line. The exact values are 0.6-0.4 proportions of
+   * charWidth for charWidth under 16. For charWidth 16 or over, the number of
+   * dashes doubles as charWidth doubles.
+   * 
+   * @param charWidth
+   * @return Stroke with appropriate dashed line fitting exactly within the
+   *         charWidth
+   */
+  private static Stroke dashedLine(int charWidth)
+  {
+    if (!dashedLineLookup.containsKey(charWidth))
+    {
+      int power2 = charWidth >= 8 ? (int) (Math.log(charWidth) / log2) : 2;
+      float width = ((float) charWidth) / ((float) Math.pow(2, power2 - 3));
+      float segment1 = width * 0.6f;
+      float segment2 = width - segment1;
+      dashedLineLookup.put(charWidth, new BasicStroke(1,
+              BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND, 3f, new float[]
+              { segment1, segment2 }, 0f));
+    }
+    return dashedLineLookup.get(charWidth);
+  }
+
+  private void drawSegmentedLine(Graphics g,
+          List<Map.Entry<Float, Color>> valCols, int x1, int y1, int x2,
+          int y2)
+  {
+    if (valCols == null || valCols.size() == 0)
+    {
+      return;
+    }
+    // let's only go forwards+up|down -- try and avoid providing right to left
+    // x values
+    if (x2 < x1)
+    {
+      int tmp = y2;
+      y2 = y1;
+      y1 = tmp;
+      tmp = x2;
+      x2 = x1;
+      x1 = tmp;
+      Collections.reverse(valCols);
+    }
+    Graphics2D g2d = (Graphics2D) g.create();
+    float yd = y2 - y1;
+    boolean reverse = yd > 0; // reverse => line going DOWN (y increasing)
+    Map.Entry<Float, Color> firstValCol = valCols.remove(0);
+    float firstVal = firstValCol.getKey();
+    Color firstCol = firstValCol.getValue();
+    int yy1 = 0;
+    yy1 = reverse ? (int) Math.ceil(y1 + firstVal * yd)
+            : (int) Math.floor(y1 + firstVal * yd);
+    Color thisCol = firstCol;
+    for (int i = 0; i < valCols.size(); i++)
+    {
+      Map.Entry<Float, Color> valCol = valCols.get(i);
+      float val = valCol.getKey();
+      Color col = valCol.getValue();
+      int clipX = x1 - 1;
+      int clipW = x2 - x1 + 2;
+      int clipY = 0;
+      int clipH = 0;
+      int yy2 = 0;
+      if (reverse) // line going down
+      {
+        yy2 = (int) Math.ceil(y1 + val * yd);
+        g2d.setColor(thisCol);
+        clipY = yy1 - 1;
+        clipH = yy2 - yy1;
+        if (i == 0)
+        {
+          // highest segment, don't clip at the top
+          clipY -= 2;
+          clipH += 2;
+        }
+        if (i == valCols.size() - 1)
+        {
+          // lowest segment, don't clip at the bottom
+          clipH += 2;
+        }
+      }
+      else // line going up (or level)
+      {
+        yy2 = (int) Math.floor(y1 + val * yd);
+        // g2d.setColor(Color.cyan); g2d.drawRect(x1 - 1, yy1, x2 - x1 + 1, yy2
+        // -
+        // yy1 + 1);
+        g2d.setColor(col);
+        clipY = yy2;
+        clipH = yy1 - yy2;
+        if (i == 0)
+        {
+          // lowest segment, don't clip at the bottom
+          clipH += 2;
+        }
+        if (i == valCols.size() - 1)
+        {
+          // highest segment, don't clip at the top
+          clipY -= 2;
+          clipH += 2;
+        }
+      }
+      g2d.setClip(clipX, clipY, clipW, clipH);
+      drawLine(g2d, x1, y1, x2, y2);
+      yy1 = yy2;
+      thisCol = col;
+    }
+    g2d.dispose();
+  }
+
+  private static int yValueToPixelHeight(float value, float min,
+          float range, int graphHeight)
+  {
+    return (int) (((value - min) / range) * graphHeight);
+  }
+
   @SuppressWarnings("unused")
   void drawBarGraph(Graphics g, AlignmentAnnotation _aa,
           Annotation[] aa_annotations, int sRes, int eRes, float min,
@@ -1597,12 +1833,6 @@ public class AnnotationRenderer
             }
             g.setColor(colour == Color.white ? Color.lightGray : colour);
 
-            // Debug - render boxes around characters
-            // g.setColor(Color.red);
-            // g.drawRect(x*av.charWidth, (int)ht, av.charWidth,
-            // (int)(scl));
-            // g.setColor(profcolour.findColour(dc[0]).darker());
-
             double sx = 1f * charWidth / fm.charsWidth(dc, 0, dc.length);
             double sy = newHeight / asc;
             double newAsc = asc * sy;
@@ -1869,14 +2099,19 @@ public class AnnotationRenderer
     g2d.setStroke(p);
   }
 
-  private void drawLine(Graphics g, int a, int b, int c, int d)
+  private void drawLine(Graphics g, int x1, int y1, int x2, int y2)
   {
     setAntialias(g);
-    g.drawLine(a, b, c, d);
+    g.drawLine(x1, y1, x2, y2);
   }
 
   private void setAntialias(Graphics g)
   {
+    setAntialias(g, false);
+  }
+
+  private void setAntialias(Graphics g, boolean text)
+  {
     if (isVectorRendering())
     {
       // no need to antialias vector drawings
@@ -1885,8 +2120,16 @@ public class AnnotationRenderer
     if (Cache.getDefault("ANTI_ALIAS", true))
     {
       Graphics2D g2d = (Graphics2D) g;
-      g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
-              RenderingHints.VALUE_ANTIALIAS_ON);
+      if (text)
+      {
+        g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
+                this.textAntialiasMethod);
+      }
+      else
+      {
+        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+                RenderingHints.VALUE_ANTIALIAS_ON);
+      }
     }
   }