524e29142e272464fe2c71c4f61c48904520d720
[jalview.git] / src / jalview / renderer / AnnotationRenderer.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.renderer;
22
23 import java.awt.BasicStroke;
24 import java.awt.Color;
25 import java.awt.Font;
26 import java.awt.FontMetrics;
27 import java.awt.Graphics;
28 import java.awt.Graphics2D;
29 import java.awt.Image;
30 import java.awt.RenderingHints;
31 import java.awt.Stroke;
32 import java.awt.geom.AffineTransform;
33 import java.awt.image.ImageObserver;
34 import java.util.BitSet;
35 import java.util.Collections;
36 import java.util.HashMap;
37 import java.util.Hashtable;
38 import java.util.List;
39 import java.util.Map;
40
41 import org.jfree.graphics2d.svg.SVGGraphics2D;
42 import org.jibble.epsgraphics.EpsGraphics2D;
43
44 import jalview.analysis.AAFrequency;
45 import jalview.analysis.CodingUtils;
46 import jalview.analysis.Rna;
47 import jalview.analysis.StructureFrequency;
48 import jalview.api.AlignViewportI;
49 import jalview.bin.Cache;
50 import jalview.bin.Console;
51 import jalview.datamodel.AlignmentAnnotation;
52 import jalview.datamodel.Annotation;
53 import jalview.datamodel.ColumnSelection;
54 import jalview.datamodel.HiddenColumns;
55 import jalview.datamodel.ProfilesI;
56 import jalview.datamodel.annotations.AnnotationColouringI;
57 import jalview.renderer.api.AnnotationRendererFactoryI;
58 import jalview.renderer.api.AnnotationRowRendererI;
59 import jalview.schemes.ColourSchemeI;
60 import jalview.schemes.NucleotideColourScheme;
61 import jalview.schemes.ResidueProperties;
62 import jalview.schemes.ZappoColourScheme;
63 import jalview.util.Platform;
64
65 public class AnnotationRenderer
66 {
67   private static final int UPPER_TO_LOWER = 'a' - 'A'; // 32
68
69   private static final int CHAR_A = 'A'; // 65
70
71   private static final int CHAR_Z = 'Z'; // 90
72
73   /**
74    * flag indicating if timing and redraw parameter info should be output
75    */
76   private final boolean debugRedraw;
77
78   private int charWidth, endRes, charHeight;
79
80   private boolean validCharWidth, hasHiddenColumns;
81
82   private FontMetrics fm;
83
84   private final boolean USE_FILL_ROUND_RECT = Platform.isAMacAndNotJS();
85
86   boolean av_renderHistogram = true, av_renderProfile = true,
87           av_normaliseProfile = false;
88
89   ResidueShaderI profcolour = null;
90
91   private ColumnSelection columnSelection;
92
93   private HiddenColumns hiddenColumns;
94
95   private ProfilesI hconsensus;
96
97   private Hashtable<String, Object>[] complementConsensus;
98
99   private Hashtable<String, Object>[] hStrucConsensus;
100
101   private boolean av_ignoreGapsConsensus;
102
103   private boolean renderingVectors = false;
104
105   private boolean glyphLineDrawn = false;
106
107   /**
108    * attributes set from AwtRenderPanelI
109    */
110   /**
111    * old image used when data is currently being calculated and cannot be
112    * rendered
113    */
114   private Image fadedImage;
115
116   /**
117    * panel being rendered into
118    */
119   private ImageObserver annotationPanel;
120
121   /**
122    * width of image to render in panel
123    */
124   private int imgWidth;
125
126   /**
127    * offset to beginning of visible area
128    */
129   private int sOffset;
130
131   /**
132    * offset to end of visible area
133    */
134   private int visHeight;
135
136   /**
137    * indicate if the renderer should only render the visible portion of the
138    * annotation given the current view settings
139    */
140   private boolean useClip = true;
141
142   /**
143    * master flag indicating if renderer should ever try to clip. not enabled for
144    * jalview 2.8.1
145    */
146   private boolean canClip = false;
147
148   /**
149    * Property to set text antialiasing method
150    */
151   private static final String TEXT_ANTIALIAS_METHOD = "TEXT_ANTIALIAS_METHOD";
152
153   private static final Object textAntialiasMethod;
154
155   static
156   {
157     final String textAntialiasMethodPref = Cache
158             .getDefault(TEXT_ANTIALIAS_METHOD, "GASP");
159     switch (textAntialiasMethodPref)
160     {
161     case "ON":
162       textAntialiasMethod = RenderingHints.VALUE_TEXT_ANTIALIAS_ON;
163       break;
164     case "GASP":
165       textAntialiasMethod = RenderingHints.VALUE_TEXT_ANTIALIAS_GASP;
166       break;
167     case "LCD_HBGR":
168       textAntialiasMethod = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HBGR;
169       break;
170     case "LCD_HRGB":
171       textAntialiasMethod = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB;
172       break;
173     case "LCD_VBGR":
174       textAntialiasMethod = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VBGR;
175       break;
176     case "LCD_VRGB":
177       textAntialiasMethod = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VRGB;
178       break;
179     case "OFF":
180       textAntialiasMethod = RenderingHints.VALUE_TEXT_ANTIALIAS_OFF;
181       break;
182     case "DEFAULT":
183       textAntialiasMethod = RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT;
184       break;
185     default:
186       jalview.bin.Console.warn(TEXT_ANTIALIAS_METHOD + " value '"
187               + textAntialiasMethodPref
188               + "' not recognised, defaulting to 'GASP'. See https://docs.oracle.com/javase/8/docs/api/java/awt/RenderingHints.html#KEY_TEXT_ANTIALIASING");
189       textAntialiasMethod = RenderingHints.VALUE_TEXT_ANTIALIAS_GASP;
190     }
191   }
192
193   public AnnotationRenderer()
194   {
195     this(false);
196   }
197
198   /**
199    * Create a new annotation Renderer
200    * 
201    * @param debugRedraw
202    *          flag indicating if timing and redraw parameter info should be
203    *          output
204    */
205   public AnnotationRenderer(boolean debugRedraw)
206   {
207     this.debugRedraw = debugRedraw;
208   }
209
210   /**
211    * Remove any references and resources when this object is no longer required
212    */
213   public void dispose()
214   {
215     hiddenColumns = null;
216     hconsensus = null;
217     complementConsensus = null;
218     hStrucConsensus = null;
219     fadedImage = null;
220     annotationPanel = null;
221     rendererFactoryI = null;
222   }
223
224   void drawStemAnnot(Graphics g, Annotation[] row_annotations, int lastSSX,
225           int x, int y, int iconOffset, int startRes, int column,
226           boolean validRes, boolean validEnd)
227   {
228     int sCol = (lastSSX / charWidth)
229             + hiddenColumns.visibleToAbsoluteColumn(startRes);
230     int x1 = lastSSX;
231     int x2 = (x * charWidth);
232
233     char dc = (column == 0 || row_annotations[column - 1] == null) ? ' '
234             : row_annotations[column - 1].secondaryStructure;
235
236     boolean diffupstream = sCol == 0 || row_annotations[sCol - 1] == null
237             || dc != row_annotations[sCol - 1].secondaryStructure
238             || !validEnd;
239     boolean diffdownstream = !validRes || !validEnd
240             || row_annotations[column] == null
241             || dc != row_annotations[column].secondaryStructure;
242
243     if (diffupstream || diffdownstream)
244     {
245       // draw glyphline under arrow
246       drawGlyphLine(g, lastSSX, x, y, iconOffset);
247     }
248     g.setColor(STEM_COLOUR);
249
250     if (column > 0 && Rna.isClosingParenthesis(dc))
251     {
252       if (diffupstream)
253       // if (validRes && column>1 && row_annotations[column-2]!=null &&
254       // dc.equals(row_annotations[column-2].displayCharacter))
255       {
256         /*
257          * if new annotation with a closing base pair half of the stem, 
258          * display a backward arrow
259          */
260         fillPolygon(g, new int[] { lastSSX + 5, lastSSX + 5, lastSSX },
261                 new int[]
262                 { y + iconOffset + 1, y + 13 + iconOffset,
263                     y + 7 + iconOffset },
264                 3);
265         x1 += 5;
266       }
267       if (diffdownstream)
268       {
269         x2 -= 1;
270       }
271     }
272     else
273     {
274       // display a forward arrow
275       if (diffdownstream)
276       {
277         /*
278          * if annotation ending with an opeing base pair half of the stem, 
279          * display a forward arrow
280          */
281         fillPolygon(g, new int[] { x2 - 6, x2 - 6, x2 - 1 },
282                 new int[]
283                 { y + iconOffset + 1, y + 13 + iconOffset,
284                     y + 7 + iconOffset },
285                 3);
286         x2 -= 5;
287       }
288       if (diffupstream)
289       {
290         x1 += 1;
291       }
292     }
293     // draw arrow body
294     unsetAntialias(g);
295     fillRect(g, x1, y + 4 + iconOffset, x2 - x1, 6);
296   }
297
298   void drawNotCanonicalAnnot(Graphics g, Color nonCanColor,
299           Annotation[] row_annotations, int lastSSX, int x, int y,
300           int iconOffset, int startRes, int column, boolean validRes,
301           boolean validEnd)
302   {
303     // Console.info(nonCanColor);
304
305     int sCol = (lastSSX / charWidth)
306             + hiddenColumns.visibleToAbsoluteColumn(startRes);
307     int x1 = lastSSX;
308     int x2 = (x * charWidth);
309
310     String dc = (column == 0 || row_annotations[column - 1] == null) ? ""
311             : row_annotations[column - 1].displayCharacter;
312
313     boolean diffupstream = sCol == 0 || row_annotations[sCol - 1] == null
314             || !dc.equals(row_annotations[sCol - 1].displayCharacter)
315             || !validEnd;
316     boolean diffdownstream = !validRes || !validEnd
317             || row_annotations[column] == null
318             || !dc.equals(row_annotations[column].displayCharacter);
319     // Console.info("Column "+column+" diff up:
320     // "+diffupstream+"
321     // down:"+diffdownstream);
322     // If a closing base pair half of the stem, display a backward arrow
323     if (diffupstream || diffdownstream)
324     {
325       // draw glyphline under arrow
326       drawGlyphLine(g, lastSSX, x, y, iconOffset);
327     }
328     g.setColor(nonCanColor);
329     if (column > 0 && Rna.isClosingParenthesis(dc))
330     {
331
332       if (diffupstream)
333       // if (validRes && column>1 && row_annotations[column-2]!=null &&
334       // dc.equals(row_annotations[column-2].displayCharacter))
335       {
336         fillPolygon(g, new int[] { lastSSX + 5, lastSSX + 5, lastSSX },
337                 new int[]
338                 { y + iconOffset + 1, y + 13 + iconOffset,
339                     y + 7 + iconOffset },
340                 3);
341         x1 += 5;
342       }
343       if (diffdownstream)
344       {
345         x2 -= 1;
346       }
347     }
348     else
349     {
350
351       // display a forward arrow
352       if (diffdownstream)
353       {
354         fillPolygon(g, new int[] { x2 - 6, x2 - 6, x2 - 1 },
355                 new int[]
356                 { y + iconOffset + 1, y + 13 + iconOffset,
357                     y + 7 + iconOffset },
358                 3);
359         x2 -= 5;
360       }
361       if (diffupstream)
362       {
363         x1 += 1;
364       }
365     }
366     // draw arrow body
367     unsetAntialias(g);
368     fillRect(g, x1, y + 4 + iconOffset, x2 - x1, 6);
369   }
370
371   // public void updateFromAnnotationPanel(FontMetrics annotFM, AlignViewportI
372   // av)
373   public void updateFromAwtRenderPanel(AwtRenderPanelI annotPanel,
374           AlignViewportI av)
375   {
376     fm = annotPanel.getFontMetrics();
377     annotationPanel = annotPanel;
378     fadedImage = annotPanel.getFadedImage();
379     imgWidth = annotPanel.getFadedImageWidth();
380     // visible area for rendering
381     int[] bounds = annotPanel.getVisibleVRange();
382     if (bounds != null)
383     {
384       sOffset = bounds[0];
385       visHeight = bounds[1];
386       if (visHeight == 0)
387       {
388         useClip = false;
389       }
390       else
391       {
392         useClip = canClip;
393       }
394     }
395     else
396     {
397       useClip = false;
398     }
399
400     rendererFactoryI = AnnotationRendererFactory.getRendererFactory();
401     updateFromAlignViewport(av);
402   }
403
404   public void updateFromAlignViewport(AlignViewportI av)
405   {
406     charWidth = av.getCharWidth();
407     endRes = av.getRanges().getEndRes();
408     charHeight = av.getCharHeight();
409     hasHiddenColumns = av.hasHiddenColumns();
410     validCharWidth = av.isValidCharWidth();
411     av_renderHistogram = av.isShowConsensusHistogram();
412     av_renderProfile = av.isShowSequenceLogo();
413     av_normaliseProfile = av.isNormaliseSequenceLogo();
414     profcolour = av.getResidueShading();
415     if (profcolour == null || profcolour.getColourScheme() == null)
416     {
417       /*
418        * Use default colour for sequence logo if 
419        * the alignment has no colourscheme set
420        * (would like to use user preference but n/a for applet)
421        */
422       ColourSchemeI col = av.getAlignment().isNucleotide()
423               ? new NucleotideColourScheme()
424               : new ZappoColourScheme();
425       profcolour = new ResidueShader(col);
426     }
427     columnSelection = av.getColumnSelection();
428     hiddenColumns = av.getAlignment().getHiddenColumns();
429     hconsensus = av.getSequenceConsensusHash();
430     complementConsensus = av.getComplementConsensusHash();
431     hStrucConsensus = av.getRnaStructureConsensusHash();
432     av_ignoreGapsConsensus = av.isIgnoreGapsConsensus();
433   }
434
435   /**
436    * Returns profile data; the first element is the profile type, the second is
437    * the number of distinct values, the third the total count, and the remainder
438    * depend on the profile type.
439    * 
440    * @param aa
441    * @param column
442    * @return
443    */
444   int[] getProfileFor(AlignmentAnnotation aa, int column)
445   {
446     // TODO : consider refactoring the global alignment calculation
447     // properties/rendering attributes as a global 'alignment group' which holds
448     // all vis settings for the alignment as a whole rather than a subset
449     //
450     if (aa.autoCalculated && (aa.label.startsWith("Consensus")
451             || aa.label.startsWith("cDNA Consensus")))
452     {
453       boolean forComplement = aa.label.startsWith("cDNA Consensus");
454       if (aa.groupRef != null && aa.groupRef.consensusData != null
455               && aa.groupRef.isShowSequenceLogo())
456       {
457         // TODO? group consensus for cDNA complement
458         return AAFrequency.extractProfile(
459                 aa.groupRef.consensusData.get(column),
460                 aa.groupRef.getIgnoreGapsConsensus());
461       }
462       // TODO extend annotation row to enable dynamic and static profile data to
463       // be stored
464       if (aa.groupRef == null && aa.sequenceRef == null)
465       {
466         if (forComplement)
467         {
468           return AAFrequency.extractCdnaProfile(complementConsensus[column],
469                   av_ignoreGapsConsensus);
470         }
471         else
472         {
473           return AAFrequency.extractProfile(hconsensus.get(column),
474                   av_ignoreGapsConsensus);
475         }
476       }
477     }
478     else
479     {
480       if (aa.autoCalculated && aa.label.startsWith("StrucConsensus"))
481       {
482         // TODO implement group structure consensus
483         /*
484          * if (aa.groupRef != null && aa.groupRef.consensusData != null &&
485          * aa.groupRef.isShowSequenceLogo()) { //TODO check what happens for
486          * group selections return StructureFrequency.extractProfile(
487          * aa.groupRef.consensusData[column], aa.groupRef
488          * .getIgnoreGapsConsensus()); }
489          */
490         // TODO extend annotation row to enable dynamic and static profile data
491         // to
492         // be stored
493         if (aa.groupRef == null && aa.sequenceRef == null
494                 && hStrucConsensus != null
495                 && hStrucConsensus.length > column)
496         {
497           return StructureFrequency.extractProfile(hStrucConsensus[column],
498                   av_ignoreGapsConsensus);
499         }
500       }
501     }
502     return null;
503   }
504
505   boolean rna = false;
506
507   private AnnotationRendererFactoryI rendererFactoryI;
508
509   /**
510    * Render the annotation rows associated with an alignment.
511    * 
512    * @param annotPanel
513    *          container frame
514    * @param av
515    *          data and view settings to render
516    * @param g
517    *          destination for graphics
518    * @param activeRow
519    *          row where a mouse event occured (or -1)
520    * @param startRes
521    *          first column that will be drawn
522    * @param endRes
523    *          last column that will be drawn
524    * @return true if the fadedImage was used for any alignment annotation rows
525    *         currently being calculated
526    */
527   public boolean drawComponent(AwtRenderPanelI annotPanel,
528           AlignViewportI av, Graphics g, int activeRow, int startRes,
529           int endRes)
530   {
531     return drawComponent(annotPanel, av, g, activeRow, startRes, endRes,
532             false);
533   }
534
535   public boolean drawComponent(AwtRenderPanelI annotPanel,
536           AlignViewportI av, Graphics g, int activeRow, int startRes,
537           int endRes, boolean forExport)
538   {
539     if (g instanceof EpsGraphics2D || g instanceof SVGGraphics2D)
540     {
541       this.setVectorRendering(true);
542     }
543     Graphics2D g2d = (Graphics2D) g;
544
545     long stime = System.currentTimeMillis();
546     boolean usedFaded = false;
547     // NOTES:
548     // AnnotationPanel needs to implement: ImageObserver, access to
549     // AlignViewport
550     updateFromAwtRenderPanel(annotPanel, av);
551     fm = g.getFontMetrics();
552     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
553     // int temp = 0;
554     if (aa == null)
555     {
556       return false;
557     }
558     int x = 0, y = 0;
559     int column = 0;
560     char lastSS;
561     int lastSSX;
562     int iconOffset = 0;
563     boolean validRes = false;
564     boolean validEnd = false;
565     boolean labelAllCols = false;
566     // boolean centreColLabels;
567     // boolean centreColLabelsDef = av.isCentreColumnLabels();
568     boolean scaleColLabel = false;
569     final AlignmentAnnotation consensusAnnot = av
570             .getAlignmentConsensusAnnotation();
571     final AlignmentAnnotation structConsensusAnnot = av
572             .getAlignmentStrucConsensusAnnotation();
573     final AlignmentAnnotation complementConsensusAnnot = av
574             .getComplementConsensusAnnotation();
575
576     BitSet graphGroupDrawn = new BitSet();
577     // \u03B2 \u03B1
578     // debug ints
579     int yfrom = 0, f_i = 0, yto = 0, f_to = 0;
580     boolean clipst = false, clipend = false;
581     for (int i = 0; i < aa.length; i++)
582     {
583       AlignmentAnnotation row = aa[i];
584       boolean renderHistogram = true;
585       boolean renderProfile = false;
586       boolean normaliseProfile = false;
587       boolean isRNA = row.isRNA();
588
589       // check if this is a consensus annotation row and set the display
590       // settings appropriately
591       // TODO: generalise this to have render styles for consensus/profile
592       // data
593       if (row.groupRef != null && row == row.groupRef.getConsensus())
594       {
595         renderHistogram = row.groupRef.isShowConsensusHistogram();
596         renderProfile = row.groupRef.isShowSequenceLogo();
597         normaliseProfile = row.groupRef.isNormaliseSequenceLogo();
598       }
599       else if (row == consensusAnnot || row == structConsensusAnnot
600               || row == complementConsensusAnnot)
601       {
602         renderHistogram = av_renderHistogram;
603         renderProfile = av_renderProfile;
604         normaliseProfile = av_normaliseProfile;
605       }
606
607       Annotation[] row_annotations = row.annotations;
608       if (!row.visible)
609       {
610         continue;
611       }
612       // centreColLabels = row.centreColLabels || centreColLabelsDef;
613       labelAllCols = row.showAllColLabels;
614       scaleColLabel = row.scaleColLabel;
615       lastSS = ' ';
616       lastSSX = 0;
617
618       if (!useClip || ((y - charHeight) < visHeight
619               && (y + row.height + charHeight * 2) >= sOffset))
620       {// if_in_visible_region
621         if (!clipst)
622         {
623           clipst = true;
624           yfrom = y;
625           f_i = i;
626         }
627         yto = y;
628         f_to = i;
629         if (row.graph > 0)
630         {
631           if (row.graphGroup > -1 && graphGroupDrawn.get(row.graphGroup))
632           {
633             continue;
634           }
635
636           // this is so that we draw the characters below the graph
637           y += row.height;
638
639           if (row.hasText)
640           {
641             iconOffset = charHeight - fm.getDescent();
642             y -= charHeight;
643           }
644         }
645         else if (row.hasText)
646         {
647           iconOffset = charHeight - fm.getDescent();
648
649         }
650         else
651         {
652           iconOffset = 0;
653         }
654
655         if (row.autoCalculated && av.isCalculationInProgress(row))
656         {
657           y += charHeight;
658           usedFaded = true;
659           g.drawImage(fadedImage, 0, y - row.height, imgWidth, y, 0,
660                   y - row.height, imgWidth, y, annotationPanel);
661           g.setColor(Color.black);
662           // g.drawString("Calculating "+aa[i].label+"....",20, y-row.height/2);
663
664           continue;
665         }
666
667         /*
668          * else if (annotationPanel.av.updatingConservation &&
669          * aa[i].label.equals("Conservation")) {
670          * 
671          * y += charHeight; g.drawImage(annotationPanel.fadedImage, 0, y -
672          * row.height, annotationPanel.imgWidth, y, 0, y - row.height,
673          * annotationPanel.imgWidth, y, annotationPanel);
674          * 
675          * g.setColor(Color.black); //
676          * g.drawString("Calculating Conservation.....",20, y-row.height/2);
677          * 
678          * continue; } else if (annotationPanel.av.updatingConservation &&
679          * aa[i].label.equals("Quality")) {
680          * 
681          * y += charHeight; g.drawImage(annotationPanel.fadedImage, 0, y -
682          * row.height, annotationPanel.imgWidth, y, 0, y - row.height,
683          * annotationPanel.imgWidth, y, annotationPanel);
684          * g.setColor(Color.black); // /
685          * g.drawString("Calculating Quality....",20, y-row.height/2);
686          * 
687          * continue; }
688          */
689
690         // first pass sets up state for drawing continuation from left-hand
691         // column
692         // of startRes
693
694         // flag used for vector rendition
695         this.glyphLineDrawn = false;
696         x = (startRes == 0) ? 0 : -1;
697
698         while (x < endRes - startRes)
699         {
700           if (hasHiddenColumns)
701           {
702             column = hiddenColumns.visibleToAbsoluteColumn(startRes + x);
703             if (column > row_annotations.length - 1)
704             {
705               break;
706             }
707           }
708           else
709           {
710             column = startRes + x;
711           }
712
713           if ((row_annotations == null)
714                   || (row_annotations.length <= column)
715                   || (row_annotations[column] == null))
716           {
717             validRes = false;
718           }
719           else
720           {
721             validRes = true;
722           }
723           final String displayChar = validRes
724                   ? row_annotations[column].displayCharacter
725                   : null;
726           if (x > -1)
727           {
728             unsetAntialias(g);
729             if (activeRow == i)
730             {
731               g.setColor(Color.red);
732
733               if (columnSelection != null)
734               {
735                 if (columnSelection.contains(column))
736                 {
737                   fillRect(g, x * charWidth, y, charWidth, charHeight);
738                 }
739               }
740             }
741             if (row.getInvalidStrucPos() > x)
742             {
743               g.setColor(Color.orange);
744               fillRect(g, x * charWidth, y, charWidth, charHeight);
745             }
746             else if (row.getInvalidStrucPos() == x)
747             {
748               g.setColor(Color.orange.darker());
749               fillRect(g, x * charWidth, y, charWidth, charHeight);
750             }
751             if (validCharWidth && validRes && displayChar != null
752                     && (displayChar.length() > 0))
753             {
754               float fmWidth = fm.stringWidth(displayChar);
755
756               /*
757                * shrink label width to fit in column, if that is
758                * both configured and necessary
759                */
760               boolean scaledToFit = false;
761               float fmScaling = 1f;
762               if (scaleColLabel && fmWidth > charWidth)
763               {
764                 scaledToFit = true;
765                 fmScaling = (float) charWidth / fmWidth;
766                 // and update the label's width to reflect the scaling.
767                 fmWidth = charWidth;
768               }
769
770               float charOffset = (charWidth - fmWidth) / 2f;
771
772               if (row_annotations[column].colour == null)
773               {
774                 g2d.setColor(Color.black);
775               }
776               else
777               {
778                 g2d.setColor(row_annotations[column].colour);
779               }
780
781               /*
782                * draw the label, unless it is the same secondary structure
783                * symbol (excluding RNA Helix) as the previous column
784                */
785               final float xPos = (x * charWidth) + charOffset;
786               final float yPos = y + iconOffset;
787
788               // Act on a copy of the Graphics2d object
789               Graphics2D g2dCopy = (Graphics2D) g2d.create();
790               // Clip to this annotation line (particularly width).
791               // This removes artifacts from text when side scrolling
792               // (particularly in wrap format), but can result in clipped
793               // characters until a full paint is drawn.
794               // Add charHeight allowance above and below annotation for
795               // character overhang.
796               // If we're in an image export, set the clip width to be the
797               // entire width of the annotation.
798
799               int clipWidth = forExport
800                       ? row_annotations.length * charWidth - 1
801                       : imgWidth - 1;
802               g2dCopy.setClip(0, (int) yPos - 2 * charHeight, clipWidth,
803                       charHeight * 3);
804               /*
805                * translate to drawing position _before_ applying any scaling
806                */
807               g2dCopy.translate(xPos, yPos);
808               if (scaledToFit)
809               {
810                 /*
811                  * use a scaling transform to make the label narrower
812                  * (JalviewJS doesn't have Font.deriveFont(AffineTransform))
813                  */
814                 g2dCopy.transform(
815                         AffineTransform.getScaleInstance(fmScaling, 1.0));
816               }
817               setAntialias(g2dCopy, true);
818               if (column == 0 || row.graph > 0)
819               {
820                 g2dCopy.drawString(displayChar, 0, 0);
821               }
822               else if (row_annotations[column - 1] == null || (labelAllCols
823                       || !displayChar.equals(
824                               row_annotations[column - 1].displayCharacter)
825                       || (displayChar.length() < 2
826                               && row_annotations[column].secondaryStructure == ' ')))
827               {
828                 g2dCopy.drawString(displayChar, 0, 0);
829               }
830               g2dCopy.dispose();
831             }
832           }
833           if (row.hasIcons)
834           {
835             char ss = validRes ? row_annotations[column].secondaryStructure
836                     : '-';
837
838             if (ss == '(')
839             {
840               // distinguish between forward/backward base-pairing
841               if (displayChar.indexOf(')') > -1)
842               {
843
844                 ss = ')';
845
846               }
847             }
848             if (ss == '[')
849             {
850               if ((displayChar.indexOf(']') > -1))
851               {
852                 ss = ']';
853
854               }
855             }
856             if (ss == '{')
857             {
858               // distinguish between forward/backward base-pairing
859               if (displayChar.indexOf('}') > -1)
860               {
861                 ss = '}';
862
863               }
864             }
865             if (ss == '<')
866             {
867               // distinguish between forward/backward base-pairing
868               if (displayChar.indexOf('<') > -1)
869               {
870                 ss = '>';
871
872               }
873             }
874             if (isRNA && (ss >= CHAR_A) && (ss <= CHAR_Z))
875             {
876               // distinguish between forward/backward base-pairing
877               int ssLowerCase = ss + UPPER_TO_LOWER;
878               // TODO would .equals() be safer here? or charAt(0)?
879               if (displayChar.indexOf(ssLowerCase) > -1)
880               {
881                 ss = (char) ssLowerCase;
882               }
883             }
884
885             if (!validRes || (ss != lastSS))
886             {
887
888               if (x > 0)
889               {
890
891                 // int nb_annot = x - temp;
892                 // Console.info("\t type :"+lastSS+"\t x
893                 // :"+x+"\t nbre
894                 // annot :"+nb_annot);
895                 switch (lastSS)
896                 {
897                 case '(': // Stem case for RNA secondary structure
898                 case ')': // and opposite direction
899                   drawStemAnnot(g, row_annotations, lastSSX, x, y,
900                           iconOffset, startRes, column, validRes, validEnd);
901                   // temp = x;
902                   break;
903
904                 case 'H':
905                   if (!isRNA)
906                   {
907                     drawHelixAnnot(g, row_annotations, lastSSX, x, y,
908                             iconOffset, startRes, column, validRes,
909                             validEnd);
910                     break;
911                   }
912                   // no break if isRNA - falls through to drawNotCanonicalAnnot!
913                 case 'E':
914                   if (!isRNA)
915                   {
916                     drawSheetAnnot(g, row_annotations, lastSSX, x, y,
917                             iconOffset, startRes, column, validRes,
918                             validEnd);
919                     break;
920                   }
921                   // no break if isRNA - fall through to drawNotCanonicalAnnot!
922
923                 case '{':
924                 case '}':
925                 case '[':
926                 case ']':
927                 case '>':
928                 case '<':
929                 case 'A':
930                 case 'a':
931                 case 'B':
932                 case 'b':
933                 case 'C':
934                 case 'c':
935                 case 'D':
936                 case 'd':
937                 case 'e':
938                 case 'F':
939                 case 'f':
940                 case 'G':
941                 case 'g':
942                 case 'h':
943                 case 'I':
944                 case 'i':
945                 case 'J':
946                 case 'j':
947                 case 'K':
948                 case 'k':
949                 case 'L':
950                 case 'l':
951                 case 'M':
952                 case 'm':
953                 case 'N':
954                 case 'n':
955                 case 'O':
956                 case 'o':
957                 case 'P':
958                 case 'p':
959                 case 'Q':
960                 case 'q':
961                 case 'R':
962                 case 'r':
963                 case 'S':
964                 case 's':
965                 case 'T':
966                 case 't':
967                 case 'U':
968                 case 'u':
969                 case 'V':
970                 case 'v':
971                 case 'W':
972                 case 'w':
973                 case 'X':
974                 case 'x':
975                 case 'Y':
976                 case 'y':
977                 case 'Z':
978                 case 'z':
979
980                   Color nonCanColor = getNotCanonicalColor(lastSS);
981                   drawNotCanonicalAnnot(g, nonCanColor, row_annotations,
982                           lastSSX, x, y, iconOffset, startRes, column,
983                           validRes, validEnd);
984                   // temp = x;
985                   break;
986                 default:
987                   if (isVectorRendering())
988                   {
989                     // draw single full width glyphline
990                     drawGlyphLine(g, lastSSX, endRes - x, y, iconOffset);
991                     // disable more glyph lines
992                     this.glyphLineDrawn = true;
993                   }
994                   else
995                   {
996                     drawGlyphLine(g, lastSSX, x, y, iconOffset);
997                   }
998                   break;
999                 }
1000               }
1001               if (validRes)
1002               {
1003                 lastSS = ss;
1004               }
1005               else
1006               {
1007                 lastSS = ' ';
1008               }
1009               if (x > -1)
1010               {
1011                 lastSSX = (x * charWidth);
1012               }
1013             }
1014           }
1015           column++;
1016           x++;
1017         }
1018         if (column >= row_annotations.length)
1019         {
1020           column = row_annotations.length - 1;
1021           validEnd = false;
1022         }
1023         else
1024         {
1025           validEnd = true;
1026         }
1027         if ((row_annotations == null) || (row_annotations.length <= column)
1028                 || (row_annotations[column] == null))
1029         {
1030           validRes = false;
1031         }
1032         else
1033         {
1034           validRes = true;
1035         }
1036         // x ++;
1037
1038         if (row.hasIcons)
1039         {
1040           switch (lastSS)
1041           {
1042
1043           case 'H':
1044             if (!isRNA)
1045             {
1046               drawHelixAnnot(g, row_annotations, lastSSX, x, y, iconOffset,
1047                       startRes, column, validRes, validEnd);
1048               break;
1049             }
1050             // no break if isRNA - fall through to drawNotCanonicalAnnot!
1051
1052           case 'E':
1053             if (!isRNA)
1054             {
1055               drawSheetAnnot(g, row_annotations, lastSSX, x, y, iconOffset,
1056                       startRes, column, validRes, validEnd);
1057               break;
1058             }
1059             // no break if isRNA - fall through to drawNotCanonicalAnnot!
1060
1061           case '(':
1062           case ')': // Stem case for RNA secondary structure
1063
1064             drawStemAnnot(g, row_annotations, lastSSX, x, y, iconOffset,
1065                     startRes, column, validRes, validEnd);
1066
1067             break;
1068           case '{':
1069           case '}':
1070           case '[':
1071           case ']':
1072           case '>':
1073           case '<':
1074           case 'A':
1075           case 'a':
1076           case 'B':
1077           case 'b':
1078           case 'C':
1079           case 'c':
1080           case 'D':
1081           case 'd':
1082           case 'e':
1083           case 'F':
1084           case 'f':
1085           case 'G':
1086           case 'g':
1087           case 'h':
1088           case 'I':
1089           case 'i':
1090           case 'J':
1091           case 'j':
1092           case 'K':
1093           case 'k':
1094           case 'L':
1095           case 'l':
1096           case 'M':
1097           case 'm':
1098           case 'N':
1099           case 'n':
1100           case 'O':
1101           case 'o':
1102           case 'P':
1103           case 'p':
1104           case 'Q':
1105           case 'q':
1106           case 'R':
1107           case 'r':
1108           case 'T':
1109           case 't':
1110           case 'U':
1111           case 'u':
1112           case 'V':
1113           case 'v':
1114           case 'W':
1115           case 'w':
1116           case 'X':
1117           case 'x':
1118           case 'Y':
1119           case 'y':
1120           case 'Z':
1121           case 'z':
1122             // Console.info(lastSS);
1123             Color nonCanColor = getNotCanonicalColor(lastSS);
1124             drawNotCanonicalAnnot(g, nonCanColor, row_annotations, lastSSX,
1125                     x, y, iconOffset, startRes, column, validRes, validEnd);
1126             break;
1127           default:
1128             if (isVectorRendering())
1129             {
1130               // draw single full width glyphline
1131               drawGlyphLine(g, lastSSX, endRes - x, y, iconOffset);
1132               // disable more glyph lines
1133               this.glyphLineDrawn = true;
1134             }
1135             else
1136             {
1137               drawGlyphLine(g, lastSSX, x, y, iconOffset);
1138             }
1139             break;
1140           }
1141         }
1142
1143         if (row.graph > 0 && row.graphHeight > 0)
1144         {
1145           if (row.graph == AlignmentAnnotation.LINE_GRAPH)
1146           {
1147             if (row.graphGroup > -1 && !graphGroupDrawn.get(row.graphGroup))
1148             {
1149               // TODO: JAL-1291 revise rendering model so the graphGroup map is
1150               // computed efficiently for all visible labels
1151               float groupmax = -999999, groupmin = 9999999;
1152               for (int gg = 0; gg < aa.length; gg++)
1153               {
1154                 if (aa[gg].graphGroup != row.graphGroup)
1155                 {
1156                   continue;
1157                 }
1158
1159                 if (aa[gg] != row)
1160                 {
1161                   aa[gg].visible = false;
1162                 }
1163                 if (aa[gg].graphMax > groupmax)
1164                 {
1165                   groupmax = aa[gg].graphMax;
1166                 }
1167                 if (aa[gg].graphMin < groupmin)
1168                 {
1169                   groupmin = aa[gg].graphMin;
1170                 }
1171               }
1172
1173               for (int gg = 0; gg < aa.length; gg++)
1174               {
1175                 if (aa[gg].graphGroup == row.graphGroup)
1176                 {
1177                   drawLineGraph(g, aa[gg], aa[gg].annotations, startRes,
1178                           endRes, y, groupmin, groupmax, row.graphHeight);
1179                 }
1180               }
1181
1182               graphGroupDrawn.set(row.graphGroup);
1183             }
1184             else
1185             {
1186               drawLineGraph(g, row, row_annotations, startRes, endRes, y,
1187                       row.graphMin, row.graphMax, row.graphHeight);
1188             }
1189           }
1190           else if (row.graph == AlignmentAnnotation.BAR_GRAPH)
1191           {
1192             drawBarGraph(g, row, row_annotations, startRes, endRes,
1193                     row.graphMin, row.graphMax, y, renderHistogram,
1194                     renderProfile, normaliseProfile);
1195           }
1196           else
1197           {
1198             AnnotationRowRendererI renderer = rendererFactoryI
1199                     .getRendererFor(row);
1200             if (renderer != null)
1201             {
1202               renderer.renderRow(g, charWidth, charHeight, hasHiddenColumns,
1203                       av, hiddenColumns, columnSelection, row,
1204                       row_annotations, startRes, endRes, row.graphMin,
1205                       row.graphMax, y, isVectorRendering());
1206             }
1207             if (debugRedraw)
1208             {
1209               if (renderer == null)
1210               {
1211                 System.err
1212                         .println("No renderer found for " + row.toString());
1213               }
1214               else
1215               {
1216                 Console.warn(
1217                         "rendered with " + renderer.getClass().toString());
1218               }
1219             }
1220
1221           }
1222         }
1223       }
1224       else
1225       {
1226         if (clipst && !clipend)
1227         {
1228           clipend = true;
1229         }
1230       } // end if_in_visible_region
1231       if (row.graph > 0 && row.hasText)
1232       {
1233         y += charHeight;
1234       }
1235
1236       if (row.graph == 0)
1237       {
1238         y += aa[i].height;
1239       }
1240     }
1241     if (debugRedraw)
1242     {
1243       if (canClip)
1244       {
1245         if (clipst)
1246         {
1247           Console.warn("Start clip at : " + yfrom + " (index " + f_i + ")");
1248         }
1249         if (clipend)
1250         {
1251           Console.warn("End clip at : " + yto + " (index " + f_to + ")");
1252         }
1253       }
1254       ;
1255       Console.warn("Annotation Rendering time:"
1256               + (System.currentTimeMillis() - stime));
1257     }
1258     ;
1259
1260     return !usedFaded;
1261   }
1262
1263   public static final Color GLYPHLINE_COLOR = Color.gray;
1264
1265   public static final Color SHEET_COLOUR = Color.green;
1266
1267   public static final Color HELIX_COLOUR = Color.red;
1268
1269   public static final Color STEM_COLOUR = Color.blue;
1270
1271   // private Color sdNOTCANONICAL_COLOUR;
1272
1273   void drawGlyphLine(Graphics g, int lastSSX, int x, int y, int iconOffset)
1274   {
1275     if (glyphLineDrawn)
1276     {
1277       // if we've drawn a single long glyphline for an export, don't draw the
1278       // bits
1279       return;
1280     }
1281     unsetAntialias(g);
1282     g.setColor(GLYPHLINE_COLOR);
1283     g.fillRect(lastSSX, y + 6 + iconOffset, (x * charWidth) - lastSSX, 2);
1284   }
1285
1286   void drawSheetAnnot(Graphics g, Annotation[] row,
1287
1288           int lastSSX, int x, int y, int iconOffset, int startRes,
1289           int column, boolean validRes, boolean validEnd)
1290   {
1291     if (!validEnd || !validRes || row == null || row[column] == null
1292             || row[column].secondaryStructure != 'E')
1293     {
1294       // draw the glyphline underneath
1295       drawGlyphLine(g, lastSSX, x, y, iconOffset);
1296
1297       g.setColor(SHEET_COLOUR);
1298       fillRect(g, lastSSX, y + 4 + iconOffset,
1299               (x * charWidth) - lastSSX - 4, 6);
1300       fillPolygon(g,
1301               new int[]
1302               { (x * charWidth) - 6, (x * charWidth) - 6,
1303                   (x * charWidth - 1) },
1304               new int[]
1305               { y + iconOffset + 1, y + 13 + iconOffset,
1306                   y + 7 + iconOffset },
1307               3);
1308     }
1309     else
1310     {
1311       g.setColor(SHEET_COLOUR);
1312       fillRect(g, lastSSX, y + 4 + iconOffset, (x * charWidth) - lastSSX,
1313               6);
1314     }
1315   }
1316
1317   void drawHelixAnnot(Graphics g, Annotation[] row, int lastSSX, int x,
1318           int y, int iconOffset, int startRes, int column, boolean validRes,
1319           boolean validEnd)
1320   {
1321     int sCol = (lastSSX / charWidth)
1322             + hiddenColumns.visibleToAbsoluteColumn(startRes);
1323     int x1 = lastSSX;
1324     int x2 = (x * charWidth);
1325
1326     if (USE_FILL_ROUND_RECT || isVectorRendering())
1327     {
1328       // draw glyph line behind helix (visible in EPS or SVG output)
1329       drawGlyphLine(g, lastSSX, x, y, iconOffset);
1330
1331       g.setColor(HELIX_COLOUR);
1332       setAntialias(g);
1333       int ofs = charWidth / 2;
1334       // Off by 1 offset when drawing rects and ovals
1335       // to offscreen image on the MAC
1336       fillRoundRect(g, lastSSX, y + 3 + iconOffset, x2 - x1 - 1, 8, 8, 8);
1337       if (sCol == 0 || row[sCol - 1] == null
1338               || row[sCol - 1].secondaryStructure != 'H')
1339       {
1340       }
1341       else
1342       {
1343         fillRoundRect(g, lastSSX, y + 3 + iconOffset, x2 - x1 - ofs, 8, 0,
1344                 0);
1345       }
1346       if (!validRes || row[column] == null
1347               || row[column].secondaryStructure != 'H')
1348       {
1349
1350       }
1351       else
1352       {
1353         fillRoundRect(g, lastSSX + ofs, y + 3 + iconOffset, x2 - x1 - ofs,
1354                 8, 0, 0);
1355       }
1356
1357       return;
1358     }
1359
1360     boolean leftEnd = sCol == 0 || row[sCol - 1] == null
1361             || row[sCol - 1].secondaryStructure != 'H';
1362     boolean rightEnd = !validRes || row[column] == null
1363             || row[column].secondaryStructure != 'H';
1364
1365     if (leftEnd || rightEnd)
1366     {
1367       drawGlyphLine(g, lastSSX, x, y, iconOffset);
1368     }
1369     g.setColor(HELIX_COLOUR);
1370
1371     if (leftEnd)
1372     {
1373       fillArc(g, lastSSX, y + 3 + iconOffset, charWidth, 8, 90, 180);
1374       x1 += charWidth / 2;
1375     }
1376
1377     if (rightEnd)
1378     {
1379       fillArc(g, ((x - 1) * charWidth), y + 3 + iconOffset, charWidth, 8,
1380               270, 180);
1381       x2 -= charWidth / 2;
1382     }
1383
1384     fillRect(g, x1, y + 3 + iconOffset, x2 - x1, 8);
1385   }
1386
1387   void drawLineGraph(Graphics g, AlignmentAnnotation _aa,
1388           Annotation[] aa_annotations, int sRes, int eRes, int y, float min,
1389           float max, int graphHeight)
1390   {
1391     if (sRes > aa_annotations.length)
1392     {
1393       return;
1394     }
1395     Stroke roundStroke = new BasicStroke(1, BasicStroke.CAP_ROUND,
1396             BasicStroke.JOIN_ROUND);
1397     Stroke squareStroke = new BasicStroke(1, BasicStroke.CAP_SQUARE,
1398             BasicStroke.JOIN_MITER);
1399     Graphics2D g2d = (Graphics2D) g;
1400     Stroke prevStroke = g2d.getStroke();
1401     g2d.setStroke(roundStroke);
1402
1403     int x = 0;
1404
1405     // Adjustment for fastpaint to left
1406     if (eRes < endRes)
1407     {
1408       eRes++;
1409     }
1410
1411     eRes = Math.min(eRes, aa_annotations.length);
1412
1413     int y1 = y, y2 = y;
1414     float range = max - min;
1415
1416     // //Draw origin
1417     if (min < 0)
1418     {
1419       y2 = y - (int) ((0 - min / range) * graphHeight);
1420     }
1421
1422     g.setColor(Color.gray);
1423     drawLine(g, squareStroke, x * charWidth, y2, (eRes - sRes) * charWidth,
1424             y2);
1425
1426     if (sRes == 0)
1427     {
1428       x++;
1429     }
1430
1431     eRes = Math.min(eRes, aa_annotations.length);
1432
1433     int column;
1434     int aaMax = aa_annotations.length - 1;
1435
1436     while (x <= eRes - sRes)
1437     {
1438       column = sRes + x;
1439       if (hasHiddenColumns)
1440       {
1441         column = hiddenColumns.visibleToAbsoluteColumn(column);
1442       }
1443
1444       if (column > aaMax)
1445       {
1446         break;
1447       }
1448
1449       if (aa_annotations[column] == null)
1450       {
1451         x++;
1452         continue;
1453       }
1454
1455       boolean individualColour = false;
1456       if (aa_annotations[column].colour == null)
1457       {
1458         g.setColor(Color.black);
1459       }
1460       else
1461       {
1462         g.setColor(aa_annotations[column].colour);
1463         individualColour = true;
1464       }
1465
1466       boolean value1Exists = column > 0
1467               && aa_annotations[column - 1] != null;
1468       float value1 = 0f;
1469       Color color1 = null;
1470       if (value1Exists)
1471       {
1472         value1 = aa_annotations[column - 1].value;
1473         color1 = aa_annotations[column - 1].colour;
1474       }
1475       float value2 = aa_annotations[column].value;
1476       boolean nextValueExists = aa_annotations.length > column + 1
1477               && aa_annotations[column + 1] != null;
1478
1479       // check for standalone value
1480       if (!value1Exists && !nextValueExists)
1481       {
1482         y2 = y - yValueToPixelHeight(value2, min, range, graphHeight);
1483         drawLine(g, x * charWidth + charWidth / 4, y2,
1484                 x * charWidth + 3 * charWidth / 4, y2);
1485         x++;
1486         continue;
1487       }
1488
1489       if (!value1Exists)
1490       {
1491         x++;
1492         continue;
1493       }
1494
1495       y1 = y - yValueToPixelHeight(value1, min, range, graphHeight);
1496       y2 = y - yValueToPixelHeight(value2, min, range, graphHeight);
1497
1498       float v1 = value1;
1499       float v2 = value2;
1500       int a1 = (x - 1) * charWidth + charWidth / 2;
1501       int b1 = y1;
1502       int a2 = x * charWidth + charWidth / 2;
1503       int b2 = y2;
1504       if (x == 0)
1505       {
1506         // only draw an initial half-line
1507         a1 = x * charWidth;
1508         b1 = y1 + (y2 - y1) / 2;
1509         v1 = value1 + (value2 - value1) / 2;
1510       }
1511       else if (x == eRes - sRes)
1512       {
1513         // this is one past the end to draw -- only draw the first half of the
1514         // line
1515         a2 = x * charWidth - 1;
1516         b2 = y1 + (y2 - y1) / 2;
1517         v2 = value1 + (value2 - value1) / 2;
1518       }
1519       else
1520       {
1521       }
1522       AnnotationColouringI ac = aa_annotations[column]
1523               .getAnnotationColouring();
1524       List<Map.Entry<Float, Color>> valCols = ac == null ? null
1525               : ac.rangeColours(v1, v2);
1526       if (valCols != null)
1527       {
1528         drawSegmentedLine(g, valCols, a1, b1, a2, b2);
1529       }
1530       else
1531       {
1532         drawLine(g, a1, b1, a2, b2);
1533       }
1534       x++;
1535     }
1536
1537     if (_aa.threshold != null)
1538     {
1539       g.setColor(_aa.threshold.colour);
1540       Graphics2D g2 = (Graphics2D) g;
1541       y2 = (int) (y - ((_aa.threshold.value - min) / range) * graphHeight);
1542       drawLine(g, dashedLine(charWidth), 0, y2, (eRes - sRes) * charWidth,
1543               y2);
1544     }
1545     g2d.setStroke(prevStroke);
1546   }
1547
1548   private static double log2 = Math.log(2);
1549
1550   // Cached dashed line Strokes
1551   private static Map<Integer, Stroke> dashedLineLookup = new HashMap<>();
1552
1553   /**
1554    * Returns a dashed line stroke as close to 6-4 pixels as fits within the
1555    * charWidth. This allows translations of multiples of charWidth without
1556    * disrupting the dashed line. The exact values are 0.6-0.4 proportions of
1557    * charWidth for charWidth under 16. For charWidth 16 or over, the number of
1558    * dashes doubles as charWidth doubles.
1559    * 
1560    * @param charWidth
1561    * @return Stroke with appropriate dashed line fitting exactly within the
1562    *         charWidth
1563    */
1564   private static Stroke dashedLine(int charWidth)
1565   {
1566     if (!dashedLineLookup.containsKey(charWidth))
1567     {
1568       int power2 = charWidth >= 8 ? (int) (Math.log(charWidth) / log2) : 2;
1569       float width = ((float) charWidth) / ((float) Math.pow(2, power2 - 3));
1570       float segment1 = width * 0.6f;
1571       float segment2 = width - segment1;
1572       dashedLineLookup.put(charWidth, new BasicStroke(1,
1573               BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND, 3f, new float[]
1574               { segment1, segment2 }, 0f));
1575     }
1576     return dashedLineLookup.get(charWidth);
1577   }
1578
1579   private void drawSegmentedLine(Graphics g,
1580           List<Map.Entry<Float, Color>> valCols, int x1, int y1, int x2,
1581           int y2)
1582   {
1583     if (valCols == null || valCols.size() == 0)
1584     {
1585       return;
1586     }
1587     // let's only go forwards+up|down -- try and avoid providing right to left
1588     // x values
1589     if (x2 < x1)
1590     {
1591       int tmp = y2;
1592       y2 = y1;
1593       y1 = tmp;
1594       tmp = x2;
1595       x2 = x1;
1596       x1 = tmp;
1597       Collections.reverse(valCols);
1598     }
1599     Graphics2D g2d = (Graphics2D) g.create();
1600     float yd = y2 - y1;
1601     boolean reverse = yd > 0; // reverse => line going DOWN (y increasing)
1602     Map.Entry<Float, Color> firstValCol = valCols.remove(0);
1603     float firstVal = firstValCol.getKey();
1604     Color firstCol = firstValCol.getValue();
1605     int yy1 = 0;
1606     yy1 = reverse ? (int) Math.ceil(y1 + firstVal * yd)
1607             : (int) Math.floor(y1 + firstVal * yd);
1608     Color thisCol = firstCol;
1609     for (int i = 0; i < valCols.size(); i++)
1610     {
1611       Map.Entry<Float, Color> valCol = valCols.get(i);
1612       float val = valCol.getKey();
1613       Color col = valCol.getValue();
1614       int clipX = x1 - 1;
1615       int clipW = x2 - x1 + 2;
1616       int clipY = 0;
1617       int clipH = 0;
1618       int yy2 = 0;
1619       if (reverse) // line going down
1620       {
1621         yy2 = (int) Math.ceil(y1 + val * yd);
1622         g2d.setColor(thisCol);
1623         clipY = yy1 - 1;
1624         clipH = yy2 - yy1;
1625         if (i == 0)
1626         {
1627           // highest segment, don't clip at the top
1628           clipY -= 2;
1629           clipH += 2;
1630         }
1631         if (i == valCols.size() - 1)
1632         {
1633           // lowest segment, don't clip at the bottom
1634           clipH += 2;
1635         }
1636       }
1637       else // line going up (or level)
1638       {
1639         yy2 = (int) Math.floor(y1 + val * yd);
1640         // g2d.setColor(Color.cyan); g2d.drawRect(x1 - 1, yy1, x2 - x1 + 1, yy2
1641         // -
1642         // yy1 + 1);
1643         g2d.setColor(col);
1644         clipY = yy2;
1645         clipH = yy1 - yy2;
1646         if (i == 0)
1647         {
1648           // lowest segment, don't clip at the bottom
1649           clipH += 2;
1650         }
1651         if (i == valCols.size() - 1)
1652         {
1653           // highest segment, don't clip at the top
1654           clipY -= 2;
1655           clipH += 2;
1656         }
1657       }
1658       g2d.setClip(clipX, clipY, clipW, clipH);
1659       drawLine(g2d, x1, y1, x2, y2);
1660       yy1 = yy2;
1661       thisCol = col;
1662     }
1663     g2d.dispose();
1664   }
1665
1666   private static int yValueToPixelHeight(float value, float min,
1667           float range, int graphHeight)
1668   {
1669     return (int) (((value - min) / range) * graphHeight);
1670   }
1671
1672   @SuppressWarnings("unused")
1673   void drawBarGraph(Graphics g, AlignmentAnnotation _aa,
1674           Annotation[] aa_annotations, int sRes, int eRes, float min,
1675           float max, int y, boolean renderHistogram, boolean renderProfile,
1676           boolean normaliseProfile)
1677   {
1678     if (sRes > aa_annotations.length)
1679     {
1680       return;
1681     }
1682     Font ofont = g.getFont();
1683     eRes = Math.min(eRes, aa_annotations.length);
1684
1685     int x = 0, y1 = y, y2 = y;
1686
1687     float range = max - min;
1688
1689     if (min < 0)
1690     {
1691       y2 = y - (int) ((0 - min / (range)) * _aa.graphHeight);
1692     }
1693
1694     g.setColor(Color.gray);
1695
1696     drawLine(g, x, y2, (eRes - sRes) * charWidth, y2);
1697
1698     int column;
1699     int aaMax = aa_annotations.length - 1;
1700     while (x < eRes - sRes)
1701     {
1702       column = sRes + x;
1703       if (hasHiddenColumns)
1704       {
1705         column = hiddenColumns.visibleToAbsoluteColumn(column);
1706       }
1707
1708       if (column > aaMax)
1709       {
1710         break;
1711       }
1712
1713       if (aa_annotations[column] == null)
1714       {
1715         x++;
1716         continue;
1717       }
1718       if (aa_annotations[column].colour == null)
1719       {
1720         g.setColor(Color.black);
1721       }
1722       else
1723       {
1724         g.setColor(aa_annotations[column].colour);
1725       }
1726
1727       y1 = y - (int) (((aa_annotations[column].value - min) / (range))
1728               * _aa.graphHeight);
1729
1730       if (renderHistogram)
1731       {
1732         if (y1 - y2 > 0)
1733         {
1734           fillRect(g, x * charWidth, y2, charWidth, y1 - y2);
1735         }
1736         else
1737         {
1738           fillRect(g, x * charWidth, y1, charWidth, y2 - y1);
1739         }
1740       }
1741       // draw profile if available
1742       if (renderProfile)
1743       {
1744
1745         /*
1746          * {profile type, #values, total count, char1, pct1, char2, pct2...}
1747          */
1748         int profl[] = getProfileFor(_aa, column);
1749
1750         // just try to draw the logo if profl is not null
1751         if (profl != null && profl[2] != 0)
1752         {
1753           boolean isStructureProfile = profl[0] == AlignmentAnnotation.STRUCTURE_PROFILE;
1754           boolean isCdnaProfile = profl[0] == AlignmentAnnotation.CDNA_PROFILE;
1755           float ht = normaliseProfile ? y - _aa.graphHeight : y1;
1756           final double normaliseFactor = normaliseProfile ? _aa.graphHeight
1757                   : (y2 - y1);
1758
1759           /**
1760            * Render a single base for a sequence profile, a base pair for
1761            * structure profile, and a triplet for a cdna profile
1762            */
1763           char[] dc = new char[isStructureProfile ? 2
1764                   : (isCdnaProfile ? 3 : 1)];
1765
1766           // lm is not necessary - we can just use fm - could be off by no more
1767           // than 0.5 px
1768           // LineMetrics lm = g.getFontMetrics(ofont).getLineMetrics("Q", g);
1769           // Console.info(asc + " " + dec + " " + (asc -
1770           // lm.getAscent())
1771           // + " " + (dec - lm.getDescent()));
1772
1773           double asc = fm.getAscent();
1774           double dec = fm.getDescent();
1775           double fht = fm.getHeight();
1776
1777           double scale = 1f / (normaliseProfile ? profl[2] : 100f);
1778           // float ofontHeight = 1f / fm.getAscent();// magnify to fill box
1779
1780           /*
1781            * Traverse the character(s)/percentage data in the array
1782            */
1783
1784           float ht2 = ht;
1785
1786           // profl[1] is the number of values in the profile
1787           for (int i = 0, c = 3, last = profl[1]; i < last; i++)
1788           {
1789
1790             String s;
1791             if (isStructureProfile)
1792             {
1793               // todo can we encode a structure pair as an int, like codons?
1794               dc[0] = (char) profl[c++];
1795               dc[1] = (char) profl[c++];
1796               s = new String(dc);
1797             }
1798             else if (isCdnaProfile)
1799             {
1800               CodingUtils.decodeCodon2(profl[c++], dc);
1801               s = new String(dc);
1802             }
1803             else
1804             {
1805               dc[0] = (char) profl[c++];
1806               s = new String(dc);
1807             }
1808             // next profl[] position is profile % for the character(s)
1809
1810             int percent = profl[c++];
1811             if (percent == 0)
1812             {
1813               // failsafe in case a count rounds down to 0%
1814               continue;
1815             }
1816             double newHeight = normaliseFactor * scale * percent;
1817
1818             /*
1819              * Set character colour as per alignment colour scheme; use the
1820              * codon translation if a cDNA profile
1821              */
1822             Color colour = null;
1823             if (isCdnaProfile)
1824             {
1825               final String codonTranslation = ResidueProperties
1826                       .codonTranslate(s);
1827               colour = profcolour.findColour(codonTranslation.charAt(0),
1828                       column, null);
1829             }
1830             else
1831             {
1832               colour = profcolour.findColour(dc[0], column, null);
1833             }
1834             g.setColor(colour == Color.white ? Color.lightGray : colour);
1835
1836             double sx = 1f * charWidth / fm.charsWidth(dc, 0, dc.length);
1837             double sy = newHeight / asc;
1838             double newAsc = asc * sy;
1839             double newDec = dec * sy;
1840             // it is not necessary to recalculate lm for the new font.
1841             // note: lm.getBaselineOffsets()[lm.getBaselineIndex()]) must be 0
1842             // by definition. Was:
1843             // int hght = (int) (ht + (newAsc - newDec);
1844             // - lm.getBaselineOffsets()[lm.getBaselineIndex()]));
1845
1846             if (Platform.isJS())
1847             {
1848               /*
1849                * SwingJS does not implement font.deriveFont()
1850                * so use a scaling transform to draw instead,
1851                * this is off by a very small amount
1852                */
1853               final int hght = (int) (ht2 + (newAsc - newDec));
1854               Graphics2D gg = (Graphics2D) g;
1855               int xShift = (int) Math.round(x * charWidth / sx);
1856               int yShift = (int) Math.round(hght / sy);
1857               gg.transform(AffineTransform.getScaleInstance(sx, sy));
1858               gg.drawString(s, xShift, yShift);
1859               gg.transform(
1860                       AffineTransform.getScaleInstance(1D / sx, 1D / sy));
1861               ht2 += newHeight;
1862             }
1863             else
1864             /**
1865              * Java only
1866              * 
1867              * @j2sIgnore
1868              */
1869             {
1870               // Java ('normal') method is to scale the font to fit
1871
1872               final int hght = (int) (ht + (newAsc - newDec));
1873               Font font = ofont
1874                       .deriveFont(AffineTransform.getScaleInstance(sx, sy));
1875               g.setFont(font);
1876               g.drawChars(dc, 0, dc.length, x * charWidth, hght);
1877               g.setFont(ofont);
1878
1879               ht += newHeight;
1880             }
1881           }
1882         }
1883       }
1884       x++;
1885     }
1886     if (_aa.threshold != null)
1887     {
1888       g.setColor(_aa.threshold.colour);
1889       Stroke s = new BasicStroke(1, BasicStroke.CAP_SQUARE,
1890               BasicStroke.JOIN_ROUND, 3f, new float[]
1891               { 5f, 3f }, 0f);
1892
1893       y2 = (int) (y
1894               - ((_aa.threshold.value - min) / range) * _aa.graphHeight);
1895       drawLine(g, s, 0, y2, (eRes - sRes) * charWidth, y2);
1896     }
1897   }
1898
1899   // used by overview window
1900   public void drawGraph(Graphics g, AlignmentAnnotation _aa,
1901           Annotation[] aa_annotations, int width, int y, int sRes, int eRes)
1902   {
1903     eRes = Math.min(eRes, aa_annotations.length);
1904     g.setColor(Color.white);
1905     fillRect(g, 0, 0, width, y);
1906     g.setColor(new Color(0, 0, 180));
1907
1908     int x = 0, height;
1909
1910     for (int j = sRes; j < eRes; j++)
1911     {
1912       if (aa_annotations[j] != null)
1913       {
1914         if (aa_annotations[j].colour == null)
1915         {
1916           g.setColor(Color.black);
1917         }
1918         else
1919         {
1920           g.setColor(aa_annotations[j].colour);
1921         }
1922
1923         height = (int) ((aa_annotations[j].value / _aa.graphMax) * y);
1924         if (height > y)
1925         {
1926           height = y;
1927         }
1928
1929         fillRect(g, x, y - height, charWidth, height);
1930       }
1931       x += charWidth;
1932     }
1933   }
1934
1935   Color getNotCanonicalColor(char lastss)
1936   {
1937     switch (lastss)
1938     {
1939     case '{':
1940     case '}':
1941       return new Color(255, 125, 5);
1942
1943     case '[':
1944     case ']':
1945       return new Color(245, 115, 10);
1946
1947     case '>':
1948     case '<':
1949       return new Color(235, 135, 15);
1950
1951     case 'A':
1952     case 'a':
1953       return new Color(225, 105, 20);
1954
1955     case 'B':
1956     case 'b':
1957       return new Color(215, 145, 30);
1958
1959     case 'C':
1960     case 'c':
1961       return new Color(205, 95, 35);
1962
1963     case 'D':
1964     case 'd':
1965       return new Color(195, 155, 45);
1966
1967     case 'E':
1968     case 'e':
1969       return new Color(185, 85, 55);
1970
1971     case 'F':
1972     case 'f':
1973       return new Color(175, 165, 65);
1974
1975     case 'G':
1976     case 'g':
1977       return new Color(170, 75, 75);
1978
1979     case 'H':
1980     case 'h':
1981       return new Color(160, 175, 85);
1982
1983     case 'I':
1984     case 'i':
1985       return new Color(150, 65, 95);
1986
1987     case 'J':
1988     case 'j':
1989       return new Color(140, 185, 105);
1990
1991     case 'K':
1992     case 'k':
1993       return new Color(130, 55, 110);
1994
1995     case 'L':
1996     case 'l':
1997       return new Color(120, 195, 120);
1998
1999     case 'M':
2000     case 'm':
2001       return new Color(110, 45, 130);
2002
2003     case 'N':
2004     case 'n':
2005       return new Color(100, 205, 140);
2006
2007     case 'O':
2008     case 'o':
2009       return new Color(90, 35, 150);
2010
2011     case 'P':
2012     case 'p':
2013       return new Color(85, 215, 160);
2014
2015     case 'Q':
2016     case 'q':
2017       return new Color(75, 25, 170);
2018
2019     case 'R':
2020     case 'r':
2021       return new Color(65, 225, 180);
2022
2023     case 'S':
2024     case 's':
2025       return new Color(55, 15, 185);
2026
2027     case 'T':
2028     case 't':
2029       return new Color(45, 235, 195);
2030
2031     case 'U':
2032     case 'u':
2033       return new Color(35, 5, 205);
2034
2035     case 'V':
2036     case 'v':
2037       return new Color(25, 245, 215);
2038
2039     case 'W':
2040     case 'w':
2041       return new Color(15, 0, 225);
2042
2043     case 'X':
2044     case 'x':
2045       return new Color(10, 255, 235);
2046
2047     case 'Y':
2048     case 'y':
2049       return new Color(5, 150, 245);
2050
2051     case 'Z':
2052     case 'z':
2053       return new Color(0, 80, 255);
2054
2055     default:
2056       Console.info("This is not a interaction : " + lastss);
2057       return null;
2058
2059     }
2060   }
2061
2062   private void fillPolygon(Graphics g, int[] xpoints, int[] ypoints, int n)
2063   {
2064     setAntialias(g);
2065     g.fillPolygon(xpoints, ypoints, n);
2066   }
2067
2068   /*
2069   private void fillRect(Graphics g, int a, int b, int c, int d)
2070   {
2071     fillRect(g, false, a, b, c, d);
2072   }*/
2073
2074   private void fillRect(Graphics g, int a, int b, int c, int d)
2075   {
2076     unsetAntialias(g);
2077     g.fillRect(a, b, c, d);
2078   }
2079
2080   private void fillRoundRect(Graphics g, int a, int b, int c, int d, int e,
2081           int f)
2082   {
2083     setAntialias(g);
2084     g.fillRoundRect(a, b, c, d, e, f);
2085   }
2086
2087   private void fillArc(Graphics g, int a, int b, int c, int d, int e, int f)
2088   {
2089     setAntialias(g);
2090     g.fillArc(a, b, c, d, e, f);
2091   }
2092
2093   private void drawLine(Graphics g, Stroke s, int a, int b, int c, int d)
2094   {
2095     Graphics2D g2d = (Graphics2D) g;
2096     Stroke p = g2d.getStroke();
2097     g2d.setStroke(s);
2098     drawLine(g, a, b, c, d);
2099     g2d.setStroke(p);
2100   }
2101
2102   private void drawLine(Graphics g, int x1, int y1, int x2, int y2)
2103   {
2104     setAntialias(g);
2105     g.drawLine(x1, y1, x2, y2);
2106   }
2107
2108   private void setAntialias(Graphics g)
2109   {
2110     setAntialias(g, false);
2111   }
2112
2113   private void setAntialias(Graphics g, boolean text)
2114   {
2115     if (isVectorRendering())
2116     {
2117       // no need to antialias vector drawings
2118       return;
2119     }
2120     if (Cache.getDefault("ANTI_ALIAS", true))
2121     {
2122       Graphics2D g2d = (Graphics2D) g;
2123       if (text)
2124       {
2125         g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
2126                 this.textAntialiasMethod);
2127       }
2128       else
2129       {
2130         g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
2131                 RenderingHints.VALUE_ANTIALIAS_ON);
2132       }
2133     }
2134   }
2135
2136   private void unsetAntialias(Graphics g)
2137   {
2138     if (isVectorRendering())
2139     {
2140       // no need to antialias vector drawings
2141       return;
2142     }
2143     Graphics2D g2d = (Graphics2D) g;
2144     g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
2145             RenderingHints.VALUE_ANTIALIAS_OFF);
2146   }
2147
2148   public void setVectorRendering(boolean b)
2149   {
2150     renderingVectors = b;
2151   }
2152
2153   public boolean isVectorRendering()
2154   {
2155     return renderingVectors;
2156   }
2157 }