JAL-4381 For lineGraph line segments that should be one colour at one end and another...
[jalview.git] / src / jalview / renderer / AnnotationRenderer.java
index d0ee0e9..a4c339e 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;
@@ -1407,18 +1409,19 @@ public class AnnotationRenderer
 
     eRes = Math.min(eRes, aa_annotations.length);
 
-    int y1 = y, y2 = y;
+    // (x1,y1), (x2,y2) pixel end points of line to be drawn
+    int y0 = y, y1 = y;
     float range = max - min;
 
     // //Draw origin
     if (min < 0)
     {
-      y2 = y - (int) ((0 - min / range) * graphHeight);
+      y1 = y - (int) ((0 - min / range) * graphHeight);
     }
 
     g.setColor(Color.gray);
-    drawLine(g, squareStroke, x * charWidth, y2, (eRes - sRes) * charWidth,
-            y2);
+    drawLine(g, squareStroke, x * charWidth, y1, (eRes - sRes) * charWidth,
+            y1);
 
     if (sRes == 0)
     {
@@ -1449,66 +1452,84 @@ public class AnnotationRenderer
         continue;
       }
 
-      if (aa_annotations[column].colour == null)
-      {
-        g.setColor(Color.black);
-      }
-      else
-      {
-        g.setColor(aa_annotations[column].colour);
-      }
+      Color color1 = aa_annotations[column].colour == null ? Color.black
+              : aa_annotations[column].colour;
+      Color color0 = null;
 
-      boolean value1Exists = column > 0
+      /* value0 is the previous value. value2 is the next value. */
+      boolean value0Exists = column > 0
               && aa_annotations[column - 1] != null;
-      float value1 = 0f;
-      if (value1Exists)
+      float value0 = 0f;
+      if (value0Exists)
       {
-        value1 = aa_annotations[column - 1].value;
+        Annotation lastAnnotation = aa_annotations[column - 1];
+        value0 = lastAnnotation.value;
+        color0 = lastAnnotation.colour == null ? Color.black
+                : lastAnnotation.colour;
       }
-      float value2 = aa_annotations[column].value;
-      boolean nextValueExists = aa_annotations.length > column + 1
+      float value1 = aa_annotations[column].value;
+      boolean value2Exists = aa_annotations.length > column + 1
               && aa_annotations[column + 1] != null;
 
       // check for standalone value
-      if (!value1Exists && !nextValueExists)
+      if (!value0Exists && !value2Exists)
       {
-        y2 = y - yValueToPixelHeight(value2, min, range, graphHeight);
-        drawLine(g, x * charWidth + charWidth / 4, y2,
-                x * charWidth + 3 * charWidth / 4, y2);
+        g.setColor(color1);
+        y1 = y - yValueToPixelHeight(value1, min, range, graphHeight);
+        drawLine(g, x * charWidth + charWidth / 4, y1,
+                x * charWidth + 3 * charWidth / 4, y1);
         x++;
         continue;
       }
 
-      if (!value1Exists)
+      if (!value0Exists)
       {
         x++;
+        // this annotation column value will be drawn with the line segment
+        // attached to the next annotation column value
         continue;
       }
 
+      y0 = y - yValueToPixelHeight(value0, min, range, graphHeight);
       y1 = y - yValueToPixelHeight(value1, min, range, graphHeight);
-      y2 = y - yValueToPixelHeight(value2, min, range, graphHeight);
 
-      int a1 = (x - 1) * charWidth + charWidth / 2;
+      // (a1, b1), (a2, b2) line segment pixel endpoints
+      int a0 = (x - 1) * charWidth + charWidth / 2;
+      int b0 = y0;
+      int a1 = x * charWidth + charWidth / 2;
       int b1 = y1;
-      int a2 = x * charWidth + charWidth / 2;
-      int b2 = y2;
+
+      Color col = null;
       if (x == 0)
       {
-        // only draw an initial half-line
-        a1 = x * charWidth;
-        b1 = y1 + (y2 - y1) / 2;
+        // only draw an initial half-line (the second half)
+        col = color1;
+        a0 = x * charWidth;
+        b0 = y0 + (y1 - y0) / 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;
+        // this is one past the end of visible alignment to draw -- only draw
+        // the first half of the line and use last point's colour
+        col = color0;
+        a1 = x * charWidth - 1;
+        b1 = y0 + (y1 - y0) / 2;
+      }
+      if (color1.equals(color0))
+      {
+        // no change in colour, just draw the whole line
+        col = color1;
+      }
+      if (col != null)
+      {
+        g.setColor(col);
+        drawLine(g, a0, b0, a1, b1);
       }
       else
       {
+        drawHalfSegmentedLine(g, color0, color1, a0, b0, a1, b1);
       }
-      drawLine(g, a1, b1, a2, b2);
+
       x++;
     }
 
@@ -1516,9 +1537,9 @@ public class AnnotationRenderer
     {
       g.setColor(_aa.threshold.colour);
       Graphics2D g2 = (Graphics2D) g;
-      y2 = (int) (y - ((_aa.threshold.value - min) / range) * graphHeight);
-      drawLine(g, dashedLine(charWidth), 0, y2, (eRes - sRes) * charWidth,
-              y2);
+      y1 = (int) (y - ((_aa.threshold.value - min) / range) * graphHeight);
+      drawLine(g, dashedLine(charWidth), 0, y1, (eRes - sRes) * charWidth,
+              y1);
     }
     g2d.setStroke(prevStroke);
   }
@@ -1560,6 +1581,176 @@ public class AnnotationRenderer
     return (int) (((value - min) / range) * graphHeight);
   }
 
+  private void drawHalfSegmentedLine(Graphics g, Color col0, Color col2,
+          int x0, int y0, int x2, int y2)
+  {
+    // y1 is vertical midpoint between the line ends (x0,y0) and (x2,y2)
+    // restricted to integer (pixel) value for better appearance
+    int y1 = (y0 + y2) / 2;
+    if (y0 == y1 || y1 == y2)
+    {
+      // not enough vertical difference for one of the halves, split
+      // horizontally
+      int x1 = (x0 + x2) / 2;
+      drawHorizontallyClippedLineSegment(g, col0, x0, y0, x1, x2, y2, true);
+      drawHorizontallyClippedLineSegment(g, col2, x0, y0, x1, x2, y2,
+              false);
+      return;
+    }
+
+    drawVerticallyClippedLineSegment(g, col0, x0, y0, y1, x2, y2, true);
+    drawVerticallyClippedLineSegment(g, col2, x0, y0, y1, x2, y2, false);
+  }
+
+  private void drawVerticallyClippedLineSegment(Graphics g, Color col,
+          int x0, int y0, int y1, int x2, int y2, boolean firstPart)
+  {
+    int margin = 1;
+    boolean y0top = y0 <= y2;
+    int clipX = Math.min(x0, x2) - margin;
+    int clipW = Math.abs(x2 - x0) + 2 * margin;
+    int clipY = 0;
+    int clipH = 0;
+    int yA = y0;
+    int yB = y1;
+    boolean marginAtTop = y0top;
+    if (!firstPart)
+    {
+      yA = y1;
+      yB = y2;
+      marginAtTop = !y0top;
+    }
+    clipY = Math.min(yA, yB);
+    clipH = Math.abs(yB - yA) + margin;
+    if (marginAtTop)
+    {
+      clipY -= margin;
+    }
+    // work on a copy (avoid later clipping problems)
+    Graphics gCopy = g.create();
+    gCopy.setClip(clipX, clipY, clipW, clipH);
+    gCopy.setColor(col);
+    drawLine(gCopy, x0, y0, x2, y2);
+  }
+
+  private void drawHorizontallyClippedLineSegment(Graphics g, Color col,
+          int x0, int y0, int x1, int x2, int y2, boolean firstPart)
+  {
+    int margin = 1;
+    boolean x0left = x0 <= x2;
+    int clipX = 0;
+    int clipW = 0;
+    int clipY = Math.min(y0, y2) - margin;
+    int clipH = Math.abs(y2 - y0) + 2 * margin;
+    int xA = x0;
+    int xB = x1;
+    boolean marginAtLeft = x0left;
+    if (!firstPart)
+    {
+      xA = x1;
+      xB = x2;
+      marginAtLeft = !x0left;
+    }
+    clipX = Math.min(xA, xB);
+    clipW = Math.abs(xB - xA) + margin;
+    if (marginAtLeft)
+    {
+      clipX -= margin;
+    }
+    // work on a copy (avoid later clipping problems)
+    Graphics gCopy = g.create();
+    gCopy.setClip(clipX, clipY, clipW, clipH);
+    gCopy.setColor(col);
+    drawLine(gCopy, x0, y0, x2, y2);
+  }
+
+  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();
+  }
+
   @SuppressWarnings("unused")
   void drawBarGraph(Graphics g, AlignmentAnnotation _aa,
           Annotation[] aa_annotations, int sRes, int eRes, float min,