From 03bc8848382795bda548083f0bc3202dfe6a9b8b Mon Sep 17 00:00:00 2001 From: Ben Soares Date: Wed, 23 Aug 2023 01:07:29 +0100 Subject: [PATCH] JAL-4250 Simplification of polygons and adjustments to calculations in secondary structure for vector rendition. Corrected rounding error for line graph with round stroke to give joined up appearance in vector rendition. --- src/jalview/gui/AlignmentPanel.java | 9 +- src/jalview/gui/ImageExporter.java | 23 +-- src/jalview/renderer/AnnotationRenderer.java | 238 ++++++++++++++++++-------- src/jalview/util/StringUtils.java | 9 + 4 files changed, 193 insertions(+), 86 deletions(-) diff --git a/src/jalview/gui/AlignmentPanel.java b/src/jalview/gui/AlignmentPanel.java index 3dfb510..aa28a8c 100644 --- a/src/jalview/gui/AlignmentPanel.java +++ b/src/jalview/gui/AlignmentPanel.java @@ -1175,7 +1175,8 @@ public class AlignmentPanel extends GAlignmentPanel implements return (w > 0 ? w : calculateIdWidth().width); } - void makeAlignmentImage(ImageMaker.TYPE type, File file, String renderer) throws ImageOutputException + void makeAlignmentImage(ImageMaker.TYPE type, File file, String renderer) + throws ImageOutputException { makeAlignmentImage(type, file, renderer, BitmapImageSizing.nullBitmapImageSizing()); @@ -1266,7 +1267,8 @@ public class AlignmentPanel extends GAlignmentPanel implements } - public void makePNGImageMap(File imgMapFile, String imageName) throws ImageOutputException + public void makePNGImageMap(File imgMapFile, String imageName) + throws ImageOutputException { // /////ONLY WORKS WITH NON WRAPPED ALIGNMENTS // //////////////////////////////////////////// @@ -1391,7 +1393,8 @@ public class AlignmentPanel extends GAlignmentPanel implements } catch (Exception ex) { - throw new ImageOutputException("couldn't write ImageMap due to unexpected error",ex); + throw new ImageOutputException( + "couldn't write ImageMap due to unexpected error", ex); } } // /////////END OF IMAGE MAP diff --git a/src/jalview/gui/ImageExporter.java b/src/jalview/gui/ImageExporter.java index 4ea30d9..8d28b1b 100644 --- a/src/jalview/gui/ImageExporter.java +++ b/src/jalview/gui/ImageExporter.java @@ -34,6 +34,7 @@ import jalview.util.ImageMaker; import jalview.util.ImageMaker.TYPE; import jalview.util.MessageManager; import jalview.util.Platform; +import jalview.util.StringUtils; import jalview.util.imagemaker.BitmapImageSizing; /** @@ -111,7 +112,8 @@ public class ImageExporter } public void doExport(File file, Component parent, int width, int height, - String imageSource, String renderer, BitmapImageSizing userBis) throws ImageOutputException + String imageSource, String renderer, BitmapImageSizing userBis) + throws ImageOutputException { final long messageId = System.currentTimeMillis(); setStatus( @@ -126,8 +128,9 @@ public class ImageExporter { if (Desktop.instance.isInBatchMode()) { - // defensive error report - we could wait for user input.. I guess ? - throw(new ImageOutputException("Need an output file to render to when exporting images in batch mode!")); + // defensive error report - we could wait for user input.. I guess ? + throw (new ImageOutputException( + "Need an output file to render to when exporting images in batch mode!")); } JalviewFileChooser chooser = imageType.getFileChooser(); chooser.setFileView(new JalviewFileView()); @@ -164,9 +167,9 @@ public class ImageExporter renderStyle = "Text"; } AtomicBoolean textSelected = new AtomicBoolean( - !"Lineart".equals(renderStyle)); - if ((imageType == TYPE.EPS || imageType == TYPE.SVG) - && LineartOptions.PROMPT_EACH_TIME.equals(renderStyle) + !StringUtils.equalsIgnoreCase("lineart", renderStyle)); + if ((imageType == TYPE.EPS || imageType == TYPE.SVG) && StringUtils + .equalsIgnoreCase(LineartOptions.PROMPT_EACH_TIME, renderStyle) && !Jalview.isHeadlessMode()) { final File chosenFile = file; @@ -188,8 +191,8 @@ public class ImageExporter else { /* - * character rendering not required, or preference already set - * - just do the export + * character rendering not required, or preference already set + * or we're in headless mode - just do the export */ exportImage(file, !textSelected.get(), width, height, messageId, userBis); @@ -226,8 +229,8 @@ public class ImageExporter messageId); } catch (Exception e) { - jalview.bin.Console.error(String.format("Error creating %s file: %s", type, - e.toString()),e); + jalview.bin.Console.error(String.format("Error creating %s file: %s", + type, e.toString()), e); setStatus(MessageManager.formatMessage("info.error_creating_file", type), messageId); } diff --git a/src/jalview/renderer/AnnotationRenderer.java b/src/jalview/renderer/AnnotationRenderer.java index 3faddb7..ea54393 100644 --- a/src/jalview/renderer/AnnotationRenderer.java +++ b/src/jalview/renderer/AnnotationRenderer.java @@ -28,11 +28,15 @@ import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.RenderingHints; +import java.awt.Stroke; import java.awt.geom.AffineTransform; import java.awt.image.ImageObserver; import java.util.BitSet; import java.util.Hashtable; +import org.jfree.graphics2d.svg.SVGGraphics2D; +import org.jibble.epsgraphics.EpsGraphics2D; + import jalview.analysis.AAFrequency; import jalview.analysis.CodingUtils; import jalview.analysis.Rna; @@ -91,6 +95,10 @@ public class AnnotationRenderer private boolean av_ignoreGapsConsensus; + private boolean vectorRendition = false; + + private boolean glyphLineDrawn = false; + /** * attributes set from AwtRenderPanelI */ @@ -254,8 +262,7 @@ public class AnnotationRenderer if (diffupstream || diffdownstream) { // draw glyphline under arrow - this.drawGlyphLine(g, row_annotations, lastSSX, x, y, iconOffset, - startRes, column, validRes, validEnd); + drawGlyphLine(g, lastSSX, x, y, iconOffset); } g.setColor(nonCanColor); if (column > 0 && Rna.isClosingParenthesis(dc)) @@ -460,16 +467,11 @@ public class AnnotationRenderer AlignViewportI av, Graphics g, int activeRow, int startRes, int endRes) { - Graphics2D g2d = (Graphics2D) g; - /* - if (Cache.getDefault("ANTI_ALIAS", true)) + if (g instanceof EpsGraphics2D || g instanceof SVGGraphics2D) { - g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, - RenderingHints.VALUE_ANTIALIAS_ON); - g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, - RenderingHints.VALUE_STROKE_PURE); + this.setVectorRendition(true); } - */ + Graphics2D g2d = (Graphics2D) g; long stime = System.currentTimeMillis(); boolean usedFaded = false; @@ -616,6 +618,7 @@ public class AnnotationRenderer * * continue; } */ + // first pass sets up state for drawing continuation from left-hand // column // of startRes @@ -905,13 +908,17 @@ public class AnnotationRenderer // temp = x; break; default: - unsetAntialias(g); - g.setColor(GLYPHLINE_COLOR); - g.fillRect(lastSSX, y + 6 + iconOffset, - (x * charWidth) - lastSSX, 2); - g.drawRect(lastSSX, y + 6 + iconOffset, - (x * charWidth) - lastSSX - 1, 2); - // temp = x; + if (isVectorRendition()) + { + // draw single full width glyphline + drawGlyphLine(g, lastSSX, endRes - x, y, iconOffset); + // disable more glyph lines + this.glyphLineDrawn = true; + } + else + { + drawGlyphLine(g, lastSSX, x, y, iconOffset); + } break; } } @@ -1042,8 +1049,17 @@ public class AnnotationRenderer x, y, iconOffset, startRes, column, validRes, validEnd); break; default: - drawGlyphLine(g, row_annotations, lastSSX, x, y, iconOffset, - startRes, column, validRes, validEnd); + if (isVectorRendition()) + { + // draw single full width glyphline + drawGlyphLine(g, lastSSX, endRes - x, y, iconOffset); + // disable more glyph lines + this.glyphLineDrawn = true; + } + else + { + drawGlyphLine(g, lastSSX, x, y, iconOffset); + } break; } } @@ -1178,15 +1194,23 @@ public class AnnotationRenderer // private Color sdNOTCANONICAL_COLOUR; - void drawGlyphLine(Graphics g, Annotation[] row, int lastSSX, int x, - int y, int iconOffset, int startRes, int column, boolean validRes, - boolean validEnd) + void drawGlyphLine(Graphics g, int lastSSX, int x, int y, int iconOffset) { + if (glyphLineDrawn) + { + // if we've drawn a single long glyphline for an export, don't draw the + // bits + return; + } unsetAntialias(g); g.setColor(GLYPHLINE_COLOR); - g.fillRect(lastSSX, y + 6 + iconOffset, (x * charWidth) - lastSSX, 2); - g.drawRect(lastSSX, y + 6 + iconOffset, (x * charWidth) - lastSSX - 1, + g.fillRect(lastSSX, y + 6 + iconOffset, (x * charWidth) - lastSSX - 1, 2); + if (!isVectorRendition()) + { + g.drawRect(lastSSX, y + 6 + iconOffset, (x * charWidth) - lastSSX - 1, + 2); + } } void drawSheetAnnot(Graphics g, Annotation[] row, @@ -1198,11 +1222,11 @@ public class AnnotationRenderer || row[column].secondaryStructure != 'E') { // draw the glyphline underneath - drawGlyphLine(g, row, lastSSX, x, y, iconOffset, startRes, column, - validRes, validEnd); + drawGlyphLine(g, lastSSX, x, y, iconOffset); + g.setColor(SHEET_COLOUR); fillRect(g, lastSSX, y + 4 + iconOffset, - (x * charWidth) - lastSSX - 4, 6); + (x * charWidth) - lastSSX - 4, isVectorRendition() ? 6 : 7); fillPolygon(g, new int[] { (x * charWidth) - 6, (x * charWidth) - 6, @@ -1215,7 +1239,8 @@ public class AnnotationRenderer else { g.setColor(SHEET_COLOUR); - fillRect(g, lastSSX, y + 4 + iconOffset, x * charWidth - lastSSX, 6); + fillRect(g, lastSSX, y + 4 + iconOffset, x * charWidth - lastSSX, + isVectorRendition() ? 6 : 7); } } @@ -1223,21 +1248,22 @@ public class AnnotationRenderer int y, int iconOffset, int startRes, int column, boolean validRes, boolean validEnd) { - g.setColor(HELIX_COLOUR); - int sCol = (lastSSX / charWidth) + hiddenColumns.visibleToAbsoluteColumn(startRes); int x1 = lastSSX; int x2 = (x * charWidth); - y--; - - if (USE_FILL_ROUND_RECT) + if (USE_FILL_ROUND_RECT || isVectorRendition()) { + // draw glyph line behind helix (visible in EPS or SVG output) + drawGlyphLine(g, lastSSX, x, y, iconOffset); + + g.setColor(HELIX_COLOUR); + setAntialias(g); int ofs = charWidth / 2; // Off by 1 offset when drawing rects and ovals // to offscreen image on the MAC - fillRoundRect(g, lastSSX, y + 4 + iconOffset, x2 - x1 - 1, 8, 8, 8); + fillRoundRect(g, lastSSX, y + 3 + iconOffset, x2 - x1 - 1, 8, 8, 8); if (sCol == 0 || row[sCol - 1] == null || row[sCol - 1].secondaryStructure != 'H') { @@ -1245,7 +1271,7 @@ public class AnnotationRenderer else { // g.setColor(Color.orange); - fillRoundRect(g, lastSSX, y + 4 + iconOffset, x2 - x1 - ofs, 8, 0, + fillRoundRect(g, lastSSX, y + 3 + iconOffset, x2 - x1 - ofs, 8, 0, 0); } if (!validRes || row[column] == null @@ -1256,30 +1282,38 @@ public class AnnotationRenderer else { // g.setColor(Color.magenta); - fillRoundRect(g, lastSSX + ofs, y + 4 + iconOffset, x2 - x1 - ofs, + fillRoundRect(g, lastSSX + ofs, y + 3 + iconOffset, x2 - x1 - ofs, 8, 0, 0); - } return; } - if (sCol == 0 || row[sCol - 1] == null - || row[sCol - 1].secondaryStructure != 'H') + boolean leftEnd = sCol == 0 || row[sCol - 1] == null + || row[sCol - 1].secondaryStructure != 'H'; + boolean rightEnd = !validRes || row[column] == null + || row[column].secondaryStructure != 'H'; + + if (leftEnd || rightEnd) + { + drawGlyphLine(g, lastSSX, x, y, iconOffset); + } + g.setColor(HELIX_COLOUR); + + if (leftEnd) { - fillArc(g, lastSSX, y + 4 + iconOffset, charWidth, 8, 90, 180); + fillArc(g, lastSSX, y + 3 + iconOffset, charWidth, 8, 90, 180); x1 += charWidth / 2; } - if (!validRes || row[column] == null - || row[column].secondaryStructure != 'H') + if (rightEnd) { - fillArc(g, (x * charWidth) - charWidth - 1, y + 4 + iconOffset, + fillArc(g, (x * charWidth) - charWidth - 1, y + 3 + iconOffset, charWidth, 8, 270, 180); x2 -= charWidth / 2; } - fillRect(g, x1, y + 4 + iconOffset, x2 - x1, 8); + fillRect(g, x1, y + 3 + iconOffset, x2 - x1, 8); } void drawLineGraph(Graphics g, AlignmentAnnotation _aa, @@ -1290,6 +1324,13 @@ public class AnnotationRenderer { return; } + Stroke roundStroke = new BasicStroke(1, BasicStroke.CAP_ROUND, + BasicStroke.JOIN_ROUND); + Stroke squareStroke = new BasicStroke(1, BasicStroke.CAP_SQUARE, + BasicStroke.JOIN_MITER); + Graphics2D g2d = (Graphics2D) g; + Stroke prevStroke = g2d.getStroke(); + g2d.setStroke(roundStroke); int x = 0; @@ -1315,9 +1356,9 @@ public class AnnotationRenderer y2 = y - (int) ((0 - min / range) * graphHeight); } - setAntialias(g); g.setColor(Color.gray); - g.drawLine(x - charWidth, y2, (eRes - sRes + 1) * charWidth, y2); + drawLine(g, squareStroke, x * charWidth - charWidth, y2, + (eRes - sRes) * charWidth, y2); eRes = Math.min(eRes, aa_annotations.length); @@ -1359,7 +1400,7 @@ public class AnnotationRenderer // standalone value y1 = y - (int) (((aa_annotations[column].value - min) / range) * graphHeight); - g.drawLine(x * charWidth + charWidth / 4, y1, + drawLine(g, x * charWidth + charWidth / 4, y1, x * charWidth + 3 * charWidth / 4, y1); x++; continue; @@ -1376,7 +1417,7 @@ public class AnnotationRenderer y2 = y - (int) (((aa_annotations[column].value - min) / range) * graphHeight); - g.drawLine(x * charWidth - charWidth / 2, y1, + drawLine(g, (x - 1) * charWidth + charWidth / 2, y1, x * charWidth + charWidth / 2, y2); x++; } @@ -1385,14 +1426,14 @@ public class AnnotationRenderer { g.setColor(_aa.threshold.colour); Graphics2D g2 = (Graphics2D) g; - g2.setStroke(new BasicStroke(1, BasicStroke.CAP_SQUARE, + Stroke s = new BasicStroke(1, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND, 3f, new float[] - { 5f, 3f }, 0f)); + { 5f, 3f }, 0f); y2 = (int) (y - ((_aa.threshold.value - min) / range) * graphHeight); - g.drawLine(0, y2, (eRes - sRes) * charWidth, y2); - g2.setStroke(new BasicStroke()); + drawLine(g, s, 0, y2, (eRes - sRes) * charWidth, y2); } + g2d.setStroke(prevStroke); } @SuppressWarnings("unused") @@ -1419,7 +1460,7 @@ public class AnnotationRenderer g.setColor(Color.gray); - g.drawLine(x, y2, (eRes - sRes) * charWidth, y2); + drawLine(g, x, y2, (eRes - sRes) * charWidth, y2); int column; int aaMax = aa_annotations.length - 1; @@ -1618,15 +1659,13 @@ public class AnnotationRenderer if (_aa.threshold != null) { g.setColor(_aa.threshold.colour); - Graphics2D g2 = (Graphics2D) g; - g2.setStroke(new BasicStroke(1, BasicStroke.CAP_SQUARE, + Stroke s = new BasicStroke(1, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND, 3f, new float[] - { 5f, 3f }, 0f)); + { 5f, 3f }, 0f); y2 = (int) (y - ((_aa.threshold.value - min) / range) * _aa.graphHeight); - g.drawLine(0, y2, (eRes - sRes) * charWidth, y2); - g2.setStroke(new BasicStroke()); + drawLine(g, s, 0, y2, (eRes - sRes) * charWidth, y2); } } @@ -1793,40 +1832,78 @@ public class AnnotationRenderer } } - private static void fillPolygon(Graphics g, int[] xpoints, int[] ypoints, - int n) + private void fillPolygon(Graphics g, int[] xpoints, int[] ypoints, int n) { unsetAntialias(g); g.fillPolygon(xpoints, ypoints, n); - setAntialias(g); - g.fillPolygon(xpoints, ypoints, n); - g.drawPolygon(xpoints, ypoints, n); + if (!isVectorRendition()) + { + setAntialias(g); + g.fillPolygon(xpoints, ypoints, n); + g.drawPolygon(xpoints, ypoints, n); + } } - private static void fillRect(Graphics g, int a, int b, int c, int d) + /* + private void fillRect(Graphics g, int a, int b, int c, int d) + { + fillRect(g, false, a, b, c, d); + }*/ + + private void fillRect(Graphics g, int a, int b, int c, int d) { g.fillRect(a, b, c, d); - g.drawRect(a, b, c, d); + /* + if (false && !isVectorRendition() && drawRect) + { + g.drawRect(a, b, c, d); + } + */ } - private static void fillRoundRect(Graphics g, int a, int b, int c, int d, - int e, int f) + private void fillRoundRect(Graphics g, int a, int b, int c, int d, int e, + int f) { setAntialias(g); g.fillRoundRect(a, b, c, d, e, f); - g.drawRoundRect(a, b, c, d, e, f); + if (!isVectorRendition()) + { + g.drawRoundRect(a, b, c, d, e, f); + } } - private static void fillArc(Graphics g, int a, int b, int c, int d, int e, - int f) + private void fillArc(Graphics g, int a, int b, int c, int d, int e, int f) { setAntialias(g); g.fillArc(a, b, c, d, e, f); - g.drawArc(a, b, c, d, e, f); + if (!isVectorRendition()) + { + g.drawArc(a, b, c, d, e, f); + } } - private static void setAntialias(Graphics g) + private void drawLine(Graphics g, Stroke s, int a, int b, int c, int d) { + Graphics2D g2d = (Graphics2D) g; + Stroke p = g2d.getStroke(); + g2d.setStroke(s); + drawLine(g, a, b, c, d); + g2d.setStroke(p); + } + + private void drawLine(Graphics g, int a, int b, int c, int d) + { + setAntialias(g); + g.drawLine(a, b, c, d); + } + + private void setAntialias(Graphics g) + { + if (isVectorRendition()) + { + // no need to antialias vector drawings + return; + } if (Cache.getDefault("ANTI_ALIAS", true)) { Graphics2D g2d = (Graphics2D) g; @@ -1835,10 +1912,25 @@ public class AnnotationRenderer } } - private static void unsetAntialias(Graphics g) + private void unsetAntialias(Graphics g) { + if (isVectorRendition()) + { + // no need to antialias vector drawings + return; + } Graphics2D g2d = (Graphics2D) g; g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); } + + public void setVectorRendition(boolean b) + { + vectorRendition = b; + } + + public boolean isVectorRendition() + { + return vectorRendition; + } } diff --git a/src/jalview/util/StringUtils.java b/src/jalview/util/StringUtils.java index 1c67c92..7cbbe1c 100644 --- a/src/jalview/util/StringUtils.java +++ b/src/jalview/util/StringUtils.java @@ -586,6 +586,15 @@ public class StringUtils return min < text.length() + 1 ? min : -1; } + public static boolean equalsIgnoreCase(String s1, String s2) + { + if (s1 == null || s2 == null) + { + return s1 == s2; + } + return s1.toLowerCase(Locale.ROOT).equals(s2.toLowerCase(Locale.ROOT)); + } + public static int indexOfFirstWhitespace(String text) { int index = -1; -- 1.7.10.2