JAL-4375 Add an AnnotationColouringI interface, and generic AnnotationColouringRanges...
[jalview.git] / src / jalview / renderer / AnnotationRenderer.java
index 0fbfb02..524e291 100644 (file)
@@ -32,8 +32,10 @@ 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;
@@ -51,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;
@@ -142,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);
@@ -480,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);
@@ -518,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;
@@ -639,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)
@@ -695,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
@@ -708,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)
               {
@@ -729,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(
@@ -756,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)
@@ -1392,6 +1452,7 @@ public class AnnotationRenderer
         continue;
       }
 
+      boolean individualColour = false;
       if (aa_annotations[column].colour == null)
       {
         g.setColor(Color.black);
@@ -1399,56 +1460,76 @@ public class AnnotationRenderer
       else
       {
         g.setColor(aa_annotations[column].colour);
+        individualColour = true;
       }
 
-      boolean previousValueExists = column > 0
+      boolean value1Exists = column > 0
               && aa_annotations[column - 1] != null;
-      float previousValue = previousValueExists
-              ? aa_annotations[column - 1].value
-              : 0;
-      float thisValue = aa_annotations[column].value;
+      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;
-      float nextValue = nextValueExists ? aa_annotations[column + 1].value
-              : 0;
 
       // check for standalone value
-      if (!previousValueExists && !nextValueExists)
+      if (!value1Exists && !nextValueExists)
       {
-        y2 = y - yValueToPixelHeight(thisValue, min, range, graphHeight);
+        y2 = y - yValueToPixelHeight(value2, min, range, graphHeight);
         drawLine(g, x * charWidth + charWidth / 4, y2,
                 x * charWidth + 3 * charWidth / 4, y2);
         x++;
         continue;
       }
 
-      if (!previousValueExists)
+      if (!value1Exists)
       {
         x++;
         continue;
       }
 
-      y1 = y - yValueToPixelHeight(previousValue, min, range, graphHeight);
-      y2 = y - yValueToPixelHeight(thisValue, min, range, graphHeight);
+      y1 = y - yValueToPixelHeight(value1, min, range, graphHeight);
+      y2 = y - yValueToPixelHeight(value2, min, range, graphHeight);
 
+      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
-        drawLine(g, x * charWidth, y1 + (y2 - y1) / 2,
-                x * charWidth + charWidth / 2, y2);
-
+        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 a half line
-        drawLine(g, (x - 1) * charWidth + charWidth / 2, y1,
-                x * charWidth - 1, y1 + (y2 - y1) / 2);
-
+        // 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, (x - 1) * charWidth + charWidth / 2, y1,
-                x * charWidth + charWidth / 2, y2);
+        drawLine(g, a1, b1, a2, b2);
       }
       x++;
     }
@@ -1495,6 +1576,93 @@ public class AnnotationRenderer
     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)
   {
@@ -1931,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
@@ -1947,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);
+      }
     }
   }