From efd301320c3bd89d126980f661e4a7dbfca8bbfe Mon Sep 17 00:00:00 2001 From: Ben Soares Date: Tue, 20 Feb 2024 15:34:43 +0000 Subject: [PATCH] JAL-4375 Add an AnnotationColouringI interface, and generic AnnotationColouringRanges to calculate changing colours between two values and specific AnnotationColouringPLDDT. Linegraph annotations can use this to drawSegmentedLine -- a line drawn multiple times clipped to different ranges in different colours. Calculations are cached. --- src/jalview/datamodel/Annotation.java | 19 ++- .../annotations/AlphaFoldAnnotationRowBuilder.java | 24 +-- .../datamodel/annotations/AnnotationColouring.java | 20 +++ .../annotations/AnnotationColouringI.java | 27 ++++ .../annotations/AnnotationColouringPLDDT.java | 23 +++ .../annotations/AnnotationColouringRanges.java | 155 +++++++++++++++++++ .../annotations/AnnotationRowBuilder.java | 17 ++ src/jalview/renderer/AnnotationRenderer.java | 163 +++++++++++++++++--- 8 files changed, 402 insertions(+), 46 deletions(-) create mode 100644 src/jalview/datamodel/annotations/AnnotationColouring.java create mode 100644 src/jalview/datamodel/annotations/AnnotationColouringI.java create mode 100644 src/jalview/datamodel/annotations/AnnotationColouringPLDDT.java create mode 100644 src/jalview/datamodel/annotations/AnnotationColouringRanges.java diff --git a/src/jalview/datamodel/Annotation.java b/src/jalview/datamodel/Annotation.java index f6919cd..996015c 100755 --- a/src/jalview/datamodel/Annotation.java +++ b/src/jalview/datamodel/Annotation.java @@ -22,6 +22,8 @@ package jalview.datamodel; import java.awt.Color; +import jalview.datamodel.annotations.AnnotationColouringI; + /** * Holds all annotation values for a position in an AlignmentAnnotation row * @@ -60,6 +62,11 @@ public class Annotation public Color colour; /** + * link back to the AnnotationRowBuilder + */ + private AnnotationColouringI annotationColouring = null; + + /** * Creates a new Annotation object. * * @param displayChar @@ -125,7 +132,7 @@ public class Annotation secondaryStructure = that.secondaryStructure; value = that.value; colour = that.colour; - + annotationColouring = that.getAnnotationColouring(); } /** @@ -217,4 +224,14 @@ public class Annotation && (secondaryStructure == '\0' || (secondaryStructure == ' ')) && colour == null); } + + public void setAnnotationColouring(AnnotationColouringI a) + { + this.annotationColouring = a; + } + + public AnnotationColouringI getAnnotationColouring() + { + return annotationColouring; + } } diff --git a/src/jalview/datamodel/annotations/AlphaFoldAnnotationRowBuilder.java b/src/jalview/datamodel/annotations/AlphaFoldAnnotationRowBuilder.java index 4e9553e..d805a21 100644 --- a/src/jalview/datamodel/annotations/AlphaFoldAnnotationRowBuilder.java +++ b/src/jalview/datamodel/annotations/AlphaFoldAnnotationRowBuilder.java @@ -2,7 +2,6 @@ package jalview.datamodel.annotations; import jalview.datamodel.Annotation; import jalview.structure.StructureImportSettings; -import jalview.structure.StructureImportSettings.TFType; public class AlphaFoldAnnotationRowBuilder extends AnnotationRowBuilder { @@ -20,24 +19,9 @@ public class AlphaFoldAnnotationRowBuilder extends AnnotationRowBuilder @Override public void processAnnotation(Annotation annotation) { - if (annotation.value > 90) - { - // Very High - annotation.colour = new java.awt.Color(0, 83, 214); - } - if (annotation.value <= 90) - { - // High - annotation.colour = new java.awt.Color(101, 203, 243); - } - if (annotation.value <= 70) - { - // Confident - annotation.colour = new java.awt.Color(255, 219, 19); - } - if (annotation.value < 50) - { - annotation.colour = new java.awt.Color(255, 125, 69); - } + AnnotationColouringI ac = new AnnotationColouringPLDDT(); + annotation.setAnnotationColouring(ac); + annotation.colour = ac.valueToColour(annotation.value); } + } \ No newline at end of file diff --git a/src/jalview/datamodel/annotations/AnnotationColouring.java b/src/jalview/datamodel/annotations/AnnotationColouring.java new file mode 100644 index 0000000..e8597e4 --- /dev/null +++ b/src/jalview/datamodel/annotations/AnnotationColouring.java @@ -0,0 +1,20 @@ +package jalview.datamodel.annotations; + +import java.awt.Color; +import java.util.List; +import java.util.Map; + +public abstract class AnnotationColouring implements AnnotationColouringI +{ + @Override + public Color valueToColour(float val) + { + return null; + } + + @Override + public List> rangeColours(float val1, float val2) + { + return null; + } +} diff --git a/src/jalview/datamodel/annotations/AnnotationColouringI.java b/src/jalview/datamodel/annotations/AnnotationColouringI.java new file mode 100644 index 0000000..83ea2eb --- /dev/null +++ b/src/jalview/datamodel/annotations/AnnotationColouringI.java @@ -0,0 +1,27 @@ +package jalview.datamodel.annotations; + +import java.awt.Color; +import java.util.List; +import java.util.Map; + +public interface AnnotationColouringI +{ + /** + * Return the colour associated with this value + * + * @param val + * @return + */ + public Color valueToColour(float val); + + /** + * Given two values, val1 and val2, returns a list of (float,Color) pairs (as + * Map.Entry objects). The float is a proportional distance (should start with + * 0 and end with 1) and the Color is the color change from that point. + * + * @param val1 + * @param val2 + * @return + */ + public List> rangeColours(float val1, float val2); +} diff --git a/src/jalview/datamodel/annotations/AnnotationColouringPLDDT.java b/src/jalview/datamodel/annotations/AnnotationColouringPLDDT.java new file mode 100644 index 0000000..ca51120 --- /dev/null +++ b/src/jalview/datamodel/annotations/AnnotationColouringPLDDT.java @@ -0,0 +1,23 @@ +package jalview.datamodel.annotations; + +import java.awt.Color; + +public class AnnotationColouringPLDDT extends AnnotationColouringRanges + implements AnnotationColouringI +{ + private static final Color BLUE = new Color(0, 83, 214); + + private static final Color CYAN = new Color(101, 203, 243); + + private static final Color YELLOW = new Color(255, 219, 19); + + private static final Color ORANGE = new Color(255, 125, 69); + + static + { + addFirstColour(ORANGE); + addValColour(50, YELLOW); + addValColour(70, CYAN); + addValColour(90, BLUE); + } +} diff --git a/src/jalview/datamodel/annotations/AnnotationColouringRanges.java b/src/jalview/datamodel/annotations/AnnotationColouringRanges.java new file mode 100644 index 0000000..bcb9e5c --- /dev/null +++ b/src/jalview/datamodel/annotations/AnnotationColouringRanges.java @@ -0,0 +1,155 @@ +package jalview.datamodel.annotations; + +import java.awt.Color; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class AnnotationColouringRanges extends AnnotationColouring +{ + private static List colours = new ArrayList(); + + private static List values = new ArrayList(); + + protected static void addValColour(float v, Color c) + { + values.add(v); + colours.add(c); + } + + protected static void addFirstColour(Color c) + { + colours.add(0, c); + } + + static + { + // e.g. + // addFirstColour(Color.black); -infty..25 = black + // addValColour(25,Color.darkGray); 25..50 = darkGray + // addValColour(50,Color.gray); 50..75 = gray + // addValColour(75,Color.lightGray); 75..100 = lightGray + // addValColour(100,Color.white); 100..infty = white + } + + @Override + public Color valueToColour(float val) + { + Color col = null; + boolean set = false; + for (int i = 0; i < values.size(); i++) + { + float compareVal = values.get(i); + if (colours.size() > i) + { + col = colours.get(i); + } + if (val < compareVal) + { + set = true; + break; + } + } + if (!set && colours.size() > values.size()) + { + col = colours.get(values.size()); + } + return col; + } + + @Override + public List> rangeColours(float val1, float val2) + { + String cacheKey = cacheKey(val1, val2); + if (!valColorsCache.containsKey(cacheKey)) + { + List> valCols = new ArrayList<>(); + float v1 = val1 <= val2 ? val1 : val2; + float v2 = val1 <= val2 ? val2 : val1; + boolean reversed = val1 > val2; + Color col = null; + boolean set1 = false; + boolean set2 = false; + int i = 0; + while (i < values.size() && (!set1 || !set2)) + { + float compareVal = values.get(i); + if (colours.size() > i) + { + col = colours.get(i); + } + + if (!set1 && v1 < compareVal) + { + // add the initial checkpoint + valCols.add(valCol(reversed ? 1f : 0f, col)); + set1 = true; + } + + if (!set2 && v2 < compareVal) + { + // add the final checkpoint + valCols.add(valCol(reversed ? 0f : 1f, col)); + set2 = true; + break; + } + + if (set1) // && !set2 + { + // add an intermediate checkpoint + float v = (compareVal - v1) / (v2 - v1); + valCols.add(valCol(reversed ? 1f - v : v, col)); + } + + i++; + } + if (colours.size() > i) + { + col = colours.get(i); + } + // add above the final checkpoint colour(s) if not set + if (!set1) + { + valCols.add(valCol(reversed ? 1f : 0f, col)); + set1 = true; + } + if (!set2) + { + // add the final checkpoint + valCols.add(valCol(reversed ? 0f : 1f, col)); + set2 = true; + } + if (reversed) + { + Collections.reverse(valCols); + } + // put in the cache + valColorsCache.put(cacheKey, valCols); + } + + return getFromCache(cacheKey); + } + + private Map.Entry valCol(Float v, Color c) + { + return new AbstractMap.SimpleEntry(v, c); + } + + private Map>> valColorsCache = new HashMap>>(); + + private List> getFromCache(String key) + { + // return a copy of the list in case of element order manipulation (e.g. + // valCols.remove(0)) + return new ArrayList>(valColorsCache.get(key)); + } + + private static String cacheKey(float f1, float f2) + { + return new StringBuilder().append(Float.hashCode(f1)).append(' ') + .append(Float.hashCode(f2)).toString(); + } +} diff --git a/src/jalview/datamodel/annotations/AnnotationRowBuilder.java b/src/jalview/datamodel/annotations/AnnotationRowBuilder.java index 2dec59c..648b173 100644 --- a/src/jalview/datamodel/annotations/AnnotationRowBuilder.java +++ b/src/jalview/datamodel/annotations/AnnotationRowBuilder.java @@ -50,6 +50,23 @@ public class AnnotationRowBuilder return tfType; } + /** + * Colouring model for the annotation + * + * @param ac + */ + private AnnotationColouringI annotationColouring = null; + + public void setAnnotationColouring(AnnotationColouringI ac) + { + annotationColouring = ac; + } + + public AnnotationColouringI getAnnotationColouring() + { + return annotationColouring; + } + public String getName() { return name; diff --git a/src/jalview/renderer/AnnotationRenderer.java b/src/jalview/renderer/AnnotationRenderer.java index 2856868..524e291 100644 --- a/src/jalview/renderer/AnnotationRenderer.java +++ b/src/jalview/renderer/AnnotationRenderer.java @@ -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; @@ -788,14 +791,16 @@ public class AnnotationRenderer // 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 - charHeight, clipWidth, - charHeight); + g2dCopy.setClip(0, (int) yPos - 2 * charHeight, clipWidth, + charHeight * 3); /* * translate to drawing position _before_ applying any scaling */ @@ -1447,6 +1452,7 @@ public class AnnotationRenderer continue; } + boolean individualColour = false; if (aa_annotations[column].colour == null) { g.setColor(Color.black); @@ -1454,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> 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++; } @@ -1550,6 +1576,93 @@ public class AnnotationRenderer return dashedLineLookup.get(charWidth); } + private void drawSegmentedLine(Graphics g, + List> 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 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 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) { @@ -1986,10 +2099,10 @@ 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) -- 1.7.10.2