Merge branch 'develop' into spike/JAL-1950_hmmer3client
[jalview.git] / src / jalview / datamodel / AlignmentAnnotation.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.datamodel;
22
23 import jalview.analysis.Rna;
24 import jalview.analysis.SecStrConsensus.SimpleBP;
25 import jalview.analysis.WUSSParseException;
26
27 import java.util.ArrayList;
28 import java.util.Collection;
29 import java.util.Collections;
30 import java.util.HashMap;
31 import java.util.Iterator;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.Map.Entry;
35
36 /**
37  * DOCUMENT ME!
38  * 
39  * @author $author$
40  * @version $Revision$
41  */
42 public class AlignmentAnnotation
43 {
44   private static final String ANNOTATION_ID_PREFIX = "ann";
45
46   /*
47    * Identifers for different types of profile data
48    */
49   public static final int SEQUENCE_PROFILE = 0;
50
51   public static final int STRUCTURE_PROFILE = 1;
52
53   public static final int CDNA_PROFILE = 2;
54
55   private static long counter = 0;
56
57   /**
58    * If true, this annotations is calculated every edit, eg consensus, quality
59    * or conservation graphs
60    */
61   public boolean autoCalculated = false;
62
63   /**
64    * unique ID for this annotation, used to match up the same annotation row
65    * shown in multiple views and alignments
66    */
67   public String annotationId;
68
69   /**
70    * the sequence this annotation is associated with (or null)
71    */
72   public SequenceI sequenceRef;
73
74   /** label shown in dropdown menus and in the annotation label area */
75   public String label;
76
77   /** longer description text shown as a tooltip */
78   public String description;
79
80   /** Array of annotations placed in the current coordinate system */
81   public Annotation[] annotations;
82
83   public List<SimpleBP> bps = null;
84
85   /**
86    * RNA secondary structure contact positions
87    */
88   public SequenceFeature[] _rnasecstr = null;
89
90   /**
91    * position of annotation resulting in invalid WUSS parsing or -1. -2 means
92    * there was no RNA structure in this annotation
93    */
94   private long invalidrnastruc = -2;
95
96   /**
97    * Updates the _rnasecstr field Determines the positions that base pair and
98    * the positions of helices based on secondary structure from a Stockholm file
99    * 
100    * @param rnaAnnotation
101    */
102   private void _updateRnaSecStr(CharSequence rnaAnnotation)
103   {
104     try
105     {
106       _rnasecstr = Rna.getHelixMap(rnaAnnotation);
107       invalidrnastruc = -1;
108     } catch (WUSSParseException px)
109     {
110       // DEBUG System.out.println(px);
111       invalidrnastruc = px.getProblemPos();
112     }
113     if (invalidrnastruc > -1)
114     {
115       return;
116     }
117
118     if (_rnasecstr != null && _rnasecstr.length > 0)
119     {
120       // show all the RNA secondary structure annotation symbols.
121       isrna = true;
122       showAllColLabels = true;
123       scaleColLabel = true;
124       _markRnaHelices();
125     }
126     // System.out.println("featuregroup " + _rnasecstr[0].getFeatureGroup());
127
128   }
129
130   private void _markRnaHelices()
131   {
132     int mxval = 0;
133     // Figure out number of helices
134     // Length of rnasecstr is the number of pairs of positions that base pair
135     // with each other in the secondary structure
136     for (int x = 0; x < _rnasecstr.length; x++)
137     {
138
139       /*
140        * System.out.println(this.annotation._rnasecstr[x] + " Begin" +
141        * this.annotation._rnasecstr[x].getBegin());
142        */
143       // System.out.println(this.annotation._rnasecstr[x].getFeatureGroup());
144       int val = 0;
145       try
146       {
147         val = Integer.valueOf(_rnasecstr[x].getFeatureGroup());
148         if (mxval < val)
149         {
150           mxval = val;
151         }
152       } catch (NumberFormatException q)
153       {
154       }
155       ;
156
157       annotations[_rnasecstr[x].getBegin()].value = val;
158       annotations[_rnasecstr[x].getEnd()].value = val;
159
160       // annotations[_rnasecstr[x].getBegin()].displayCharacter = "" + val;
161       // annotations[_rnasecstr[x].getEnd()].displayCharacter = "" + val;
162     }
163     setScore(mxval);
164   }
165
166   /**
167    * map of positions in the associated annotation
168    */
169   private Map<Integer, Annotation> sequenceMapping;
170
171   /**
172    * lower range for quantitative data
173    */
174   public float graphMin;
175
176   /**
177    * Upper range for quantitative data
178    */
179   public float graphMax;
180
181   /**
182    * Score associated with label and description.
183    */
184   public double score = Double.NaN;
185
186   /**
187    * flag indicating if annotation has a score.
188    */
189   public boolean hasScore = false;
190
191   public GraphLine threshold;
192
193   // Graphical hints and tips
194
195   /** Can this row be edited by the user ? */
196   public boolean editable = false;
197
198   /** Indicates if annotation has a graphical symbol track */
199   public boolean hasIcons; //
200
201   /** Indicates if annotation has a text character label */
202   public boolean hasText;
203
204   /** is the row visible */
205   public boolean visible = true;
206
207   public int graphGroup = -1;
208
209   /** Displayed height of row in pixels */
210   public int height = 0;
211
212   public int graph = 0;
213
214   public int graphHeight = 40;
215
216   public boolean padGaps = false;
217
218   public static final int NO_GRAPH = 0;
219
220   public static final int BAR_GRAPH = 1;
221
222   public static final int LINE_GRAPH = 2;
223
224   public boolean belowAlignment = true;
225
226   public SequenceGroup groupRef = null;
227
228   /**
229    * display every column label, even if there is a row of identical labels
230    */
231   public boolean showAllColLabels = false;
232
233   /**
234    * scale the column label to fit within the alignment column.
235    */
236   public boolean scaleColLabel = false;
237
238   /**
239    * centre the column labels relative to the alignment column
240    */
241   public boolean centreColLabels = false;
242
243   private boolean isrna;
244
245   public static int getGraphValueFromString(String string)
246   {
247     if (string.equalsIgnoreCase("BAR_GRAPH"))
248     {
249       return BAR_GRAPH;
250     }
251     else if (string.equalsIgnoreCase("LINE_GRAPH"))
252     {
253       return LINE_GRAPH;
254     }
255     else
256     {
257       return NO_GRAPH;
258     }
259   }
260
261   /**
262    * Creates a new AlignmentAnnotation object.
263    * 
264    * @param label
265    *          short label shown under sequence labels
266    * @param description
267    *          text displayed on mouseover
268    * @param annotations
269    *          set of positional annotation elements
270    */
271   public AlignmentAnnotation(String label, String description,
272           Annotation[] annotations)
273   {
274     setAnnotationId();
275     // always editable?
276     editable = true;
277     this.label = label;
278     this.description = description;
279     this.annotations = annotations;
280
281     validateRangeAndDisplay();
282   }
283
284   /**
285    * Checks if annotation labels represent secondary structures
286    * 
287    */
288   void areLabelsSecondaryStructure()
289   {
290     boolean nonSSLabel = false;
291     isrna = false;
292     StringBuffer rnastring = new StringBuffer();
293
294     char firstChar = 0;
295     for (int i = 0; i < annotations.length; i++)
296     {
297       if (annotations[i] == null)
298       {
299         continue;
300       }
301       if (annotations[i].secondaryStructure == 'H'
302               || annotations[i].secondaryStructure == 'E')
303       {
304         hasIcons |= true;
305       }
306       else
307       // Check for RNA secondary structure
308       {
309         // System.out.println(annotations[i].secondaryStructure);
310         // TODO: 2.8.2 should this ss symbol validation check be a function in
311         // RNA/ResidueProperties ?
312         if (annotations[i].secondaryStructure == '('
313                 || annotations[i].secondaryStructure == '['
314                 || annotations[i].secondaryStructure == '<'
315                 || annotations[i].secondaryStructure == '{'
316                 || annotations[i].secondaryStructure == 'A'
317                 || annotations[i].secondaryStructure == 'B'
318                 || annotations[i].secondaryStructure == 'C'
319                 || annotations[i].secondaryStructure == 'D'
320                 || annotations[i].secondaryStructure == 'E'
321                 || annotations[i].secondaryStructure == 'F'
322                 || annotations[i].secondaryStructure == 'G'
323                 || annotations[i].secondaryStructure == 'H'
324                 || annotations[i].secondaryStructure == 'I'
325                 || annotations[i].secondaryStructure == 'J'
326                 || annotations[i].secondaryStructure == 'K'
327                 || annotations[i].secondaryStructure == 'L'
328                 || annotations[i].secondaryStructure == 'M'
329                 || annotations[i].secondaryStructure == 'N'
330                 || annotations[i].secondaryStructure == 'O'
331                 || annotations[i].secondaryStructure == 'P'
332                 || annotations[i].secondaryStructure == 'Q'
333                 || annotations[i].secondaryStructure == 'R'
334                 || annotations[i].secondaryStructure == 'S'
335                 || annotations[i].secondaryStructure == 'T'
336                 || annotations[i].secondaryStructure == 'U'
337                 || annotations[i].secondaryStructure == 'V'
338                 || annotations[i].secondaryStructure == 'W'
339                 || annotations[i].secondaryStructure == 'X'
340                 || annotations[i].secondaryStructure == 'Y'
341                 || annotations[i].secondaryStructure == 'Z')
342         {
343           hasIcons |= true;
344           isrna |= true;
345         }
346       }
347
348       // System.out.println("displaychar " + annotations[i].displayCharacter);
349
350       if (annotations[i].displayCharacter == null
351               || annotations[i].displayCharacter.length() == 0)
352       {
353         rnastring.append('.');
354         continue;
355       }
356       if (annotations[i].displayCharacter.length() == 1)
357       {
358         firstChar = annotations[i].displayCharacter.charAt(0);
359         // check to see if it looks like a sequence or is secondary structure
360         // labelling.
361         if (annotations[i].secondaryStructure != ' ' && !hasIcons &&
362         // Uncomment to only catch case where
363         // displayCharacter==secondary
364         // Structure
365         // to correctly redisplay SS annotation imported from Stockholm,
366         // exported to JalviewXML and read back in again.
367         // &&
368         // annotations[i].displayCharacter.charAt(0)==annotations[i].secondaryStructure
369                 firstChar != ' ' && firstChar != '$' && firstChar != 0xCE
370                 && firstChar != '(' && firstChar != '[' && firstChar != '>'
371                 && firstChar != '{' && firstChar != 'A' && firstChar != 'B'
372                 && firstChar != 'C' && firstChar != 'D' && firstChar != 'E'
373                 && firstChar != 'F' && firstChar != 'G' && firstChar != 'H'
374                 && firstChar != 'I' && firstChar != 'J' && firstChar != 'K'
375                 && firstChar != 'L' && firstChar != 'M' && firstChar != 'N'
376                 && firstChar != 'O' && firstChar != 'P' && firstChar != 'Q'
377                 && firstChar != 'R' && firstChar != 'S' && firstChar != 'T'
378                 && firstChar != 'U' && firstChar != 'V' && firstChar != 'W'
379                 && firstChar != 'X' && firstChar != 'Y' && firstChar != 'Z'
380                 && firstChar != '-'
381                 && firstChar < jalview.schemes.ResidueProperties.aaIndex.length)
382         {
383           if (jalview.schemes.ResidueProperties.aaIndex[firstChar] < 23) // TODO:
384                                                                          // parameterise
385                                                                          // to
386                                                                          // gap
387                                                                          // symbol
388                                                                          // number
389           {
390             nonSSLabel = true;
391           }
392         }
393       }
394       else
395       {
396         rnastring.append(annotations[i].displayCharacter.charAt(1));
397       }
398
399       if (annotations[i].displayCharacter.length() > 0)
400       {
401         hasText = true;
402       }
403     }
404
405     if (nonSSLabel)
406     {
407       hasIcons = false;
408       for (int j = 0; j < annotations.length; j++)
409       {
410         if (annotations[j] != null
411                 && annotations[j].secondaryStructure != ' ')
412         {
413           annotations[j].displayCharacter = String
414                   .valueOf(annotations[j].secondaryStructure);
415           annotations[j].secondaryStructure = ' ';
416         }
417
418       }
419     }
420     else
421     {
422       if (isrna)
423       {
424         _updateRnaSecStr(new AnnotCharSequence());
425       }
426     }
427   }
428
429   /**
430    * flyweight access to positions in the alignment annotation row for RNA
431    * processing
432    * 
433    * @author jimp
434    * 
435    */
436   private class AnnotCharSequence implements CharSequence
437   {
438     int offset = 0;
439
440     int max = 0;
441
442     public AnnotCharSequence()
443     {
444       this(0, annotations.length);
445     }
446
447     AnnotCharSequence(int start, int end)
448     {
449       offset = start;
450       max = end;
451     }
452
453     @Override
454     public CharSequence subSequence(int start, int end)
455     {
456       return new AnnotCharSequence(offset + start, offset + end);
457     }
458
459     @Override
460     public int length()
461     {
462       return max - offset;
463     }
464
465     @Override
466     public char charAt(int index)
467     {
468       return ((index + offset < 0) || (index + offset) >= max
469               || annotations[index + offset] == null
470               || (annotations[index + offset].secondaryStructure <= ' ')
471                       ? ' '
472                       : annotations[index + offset].displayCharacter == null
473                               || annotations[index
474                                       + offset].displayCharacter
475                                               .length() == 0
476                                                       ? annotations[index
477                                                               + offset].secondaryStructure
478                                                       : annotations[index
479                                                               + offset].displayCharacter
480                                                                       .charAt(0));
481     }
482
483     @Override
484     public String toString()
485     {
486       char[] string = new char[max - offset];
487       int mx = annotations.length;
488
489       for (int i = offset; i < mx; i++)
490       {
491         string[i] = (annotations[i] == null
492                 || (annotations[i].secondaryStructure <= 32))
493                         ? ' '
494                         : (annotations[i].displayCharacter == null
495                                 || annotations[i].displayCharacter
496                                         .length() == 0
497                                                 ? annotations[i].secondaryStructure
498                                                 : annotations[i].displayCharacter
499                                                         .charAt(0));
500       }
501       return new String(string);
502     }
503   };
504
505   private long _lastrnaannot = -1;
506
507   public String getRNAStruc()
508   {
509     if (isrna)
510     {
511       String rnastruc = new AnnotCharSequence().toString();
512       if (_lastrnaannot != rnastruc.hashCode())
513       {
514         // ensure rna structure contacts are up to date
515         _lastrnaannot = rnastruc.hashCode();
516         _updateRnaSecStr(rnastruc);
517       }
518       return rnastruc;
519     }
520     return null;
521   }
522
523   /**
524    * Creates a new AlignmentAnnotation object.
525    * 
526    * @param label
527    *          DOCUMENT ME!
528    * @param description
529    *          DOCUMENT ME!
530    * @param annotations
531    *          DOCUMENT ME!
532    * @param min
533    *          DOCUMENT ME!
534    * @param max
535    *          DOCUMENT ME!
536    * @param winLength
537    *          DOCUMENT ME!
538    */
539   public AlignmentAnnotation(String label, String description,
540           Annotation[] annotations, float min, float max, int graphType)
541   {
542     setAnnotationId();
543     // graphs are not editable
544     editable = graphType == 0;
545
546     this.label = label;
547     this.description = description;
548     this.annotations = annotations;
549     graph = graphType;
550     graphMin = min;
551     graphMax = max;
552     validateRangeAndDisplay();
553   }
554
555   /**
556    * checks graphMin and graphMax, secondary structure symbols, sets graphType
557    * appropriately, sets null labels to the empty string if appropriate.
558    */
559   public void validateRangeAndDisplay()
560   {
561
562     if (annotations == null)
563     {
564       visible = false; // try to prevent renderer from displaying.
565       invalidrnastruc = -1;
566       return; // this is a non-annotation row annotation - ie a sequence score.
567     }
568
569     int graphType = graph;
570     float min = graphMin;
571     float max = graphMax;
572     boolean drawValues = true;
573     _linecolour = null;
574     if (min == max)
575     {
576       min = 999999999;
577       for (int i = 0; i < annotations.length; i++)
578       {
579         if (annotations[i] == null)
580         {
581           continue;
582         }
583
584         if (drawValues && annotations[i].displayCharacter != null
585                 && annotations[i].displayCharacter.length() > 1)
586         {
587           drawValues = false;
588         }
589
590         if (annotations[i].value > max)
591         {
592           max = annotations[i].value;
593         }
594
595         if (annotations[i].value < min)
596         {
597           min = annotations[i].value;
598         }
599         if (_linecolour == null && annotations[i].colour != null)
600         {
601           _linecolour = annotations[i].colour;
602         }
603       }
604       // ensure zero is origin for min/max ranges on only one side of zero
605       if (min > 0)
606       {
607         min = 0;
608       }
609       else
610       {
611         if (max < 0)
612         {
613           max = 0;
614         }
615       }
616     }
617
618     graphMin = min;
619     graphMax = max;
620
621     areLabelsSecondaryStructure();
622
623     if (!drawValues && graphType != NO_GRAPH)
624     {
625       for (int i = 0; i < annotations.length; i++)
626       {
627         if (annotations[i] != null)
628         {
629           annotations[i].displayCharacter = "";
630         }
631       }
632     }
633   }
634
635   /**
636    * Copy constructor creates a new independent annotation row with the same
637    * associated sequenceRef
638    * 
639    * @param annotation
640    */
641   public AlignmentAnnotation(AlignmentAnnotation annotation)
642   {
643     setAnnotationId();
644     this.label = new String(annotation.label);
645     if (annotation.description != null)
646     {
647       this.description = new String(annotation.description);
648     }
649     this.graphMin = annotation.graphMin;
650     this.graphMax = annotation.graphMax;
651     this.graph = annotation.graph;
652     this.graphHeight = annotation.graphHeight;
653     this.graphGroup = annotation.graphGroup;
654     this.groupRef = annotation.groupRef;
655     this.editable = annotation.editable;
656     this.autoCalculated = annotation.autoCalculated;
657     this.hasIcons = annotation.hasIcons;
658     this.hasText = annotation.hasText;
659     this.height = annotation.height;
660     this.label = annotation.label;
661     this.padGaps = annotation.padGaps;
662     this.visible = annotation.visible;
663     this.centreColLabels = annotation.centreColLabels;
664     this.scaleColLabel = annotation.scaleColLabel;
665     this.showAllColLabels = annotation.showAllColLabels;
666     this.calcId = annotation.calcId;
667     if (annotation.properties != null)
668     {
669       properties = new HashMap<>();
670       for (Map.Entry<String, String> val : annotation.properties.entrySet())
671       {
672         properties.put(val.getKey(), val.getValue());
673       }
674     }
675     if (this.hasScore = annotation.hasScore)
676     {
677       this.score = annotation.score;
678     }
679     if (annotation.threshold != null)
680     {
681       threshold = new GraphLine(annotation.threshold);
682     }
683     Annotation[] ann = annotation.annotations;
684     if (annotation.annotations != null)
685     {
686       this.annotations = new Annotation[ann.length];
687       for (int i = 0; i < ann.length; i++)
688       {
689         if (ann[i] != null)
690         {
691           annotations[i] = new Annotation(ann[i]);
692           if (_linecolour != null)
693           {
694             _linecolour = annotations[i].colour;
695           }
696         }
697       }
698     }
699     if (annotation.sequenceRef != null)
700     {
701       this.sequenceRef = annotation.sequenceRef;
702       if (annotation.sequenceMapping != null)
703       {
704         Integer p = null;
705         sequenceMapping = new HashMap<>();
706         Iterator<Integer> pos = annotation.sequenceMapping.keySet()
707                 .iterator();
708         while (pos.hasNext())
709         {
710           // could optimise this!
711           p = pos.next();
712           Annotation a = annotation.sequenceMapping.get(p);
713           if (a == null)
714           {
715             continue;
716           }
717           if (ann != null)
718           {
719             for (int i = 0; i < ann.length; i++)
720             {
721               if (ann[i] == a)
722               {
723                 sequenceMapping.put(p, annotations[i]);
724               }
725             }
726           }
727         }
728       }
729       else
730       {
731         this.sequenceMapping = null;
732       }
733     }
734     // TODO: check if we need to do this: JAL-952
735     // if (this.isrna=annotation.isrna)
736     {
737       // _rnasecstr=new SequenceFeature[annotation._rnasecstr];
738     }
739     validateRangeAndDisplay(); // construct hashcodes, etc.
740   }
741
742   /**
743    * clip the annotation to the columns given by startRes and endRes (inclusive)
744    * and prune any existing sequenceMapping to just those columns.
745    * 
746    * @param startRes
747    * @param endRes
748    */
749   public void restrict(int startRes, int endRes)
750   {
751     if (annotations == null)
752     {
753       // non-positional
754       return;
755     }
756     if (startRes < 0)
757     {
758       startRes = 0;
759     }
760     if (startRes >= annotations.length)
761     {
762       startRes = annotations.length - 1;
763     }
764     if (endRes >= annotations.length)
765     {
766       endRes = annotations.length - 1;
767     }
768     if (annotations == null)
769     {
770       return;
771     }
772     Annotation[] temp = new Annotation[endRes - startRes + 1];
773     if (startRes < annotations.length)
774     {
775       System.arraycopy(annotations, startRes, temp, 0,
776               endRes - startRes + 1);
777     }
778     if (sequenceRef != null)
779     {
780       // Clip the mapping, if it exists.
781       int spos = sequenceRef.findPosition(startRes);
782       int epos = sequenceRef.findPosition(endRes);
783       if (sequenceMapping != null)
784       {
785         Map<Integer, Annotation> newmapping = new HashMap<>();
786         Iterator<Integer> e = sequenceMapping.keySet().iterator();
787         while (e.hasNext())
788         {
789           Integer pos = e.next();
790           if (pos.intValue() >= spos && pos.intValue() <= epos)
791           {
792             newmapping.put(pos, sequenceMapping.get(pos));
793           }
794         }
795         sequenceMapping.clear();
796         sequenceMapping = newmapping;
797       }
798     }
799     annotations = temp;
800   }
801
802   /**
803    * set the annotation row to be at least length Annotations
804    * 
805    * @param length
806    *          minimum number of columns required in the annotation row
807    * @return false if the annotation row is greater than length
808    */
809   public boolean padAnnotation(int length)
810   {
811     if (annotations == null)
812     {
813       return true; // annotation row is correct - null == not visible and
814       // undefined length
815     }
816     if (annotations.length < length)
817     {
818       Annotation[] na = new Annotation[length];
819       System.arraycopy(annotations, 0, na, 0, annotations.length);
820       annotations = na;
821       return true;
822     }
823     return annotations.length > length;
824
825   }
826
827   /**
828    * DOCUMENT ME!
829    * 
830    * @return DOCUMENT ME!
831    */
832   @Override
833   public String toString()
834   {
835     if (annotations == null)
836     {
837       return "";
838     }
839     StringBuilder buffer = new StringBuilder(256);
840
841     for (int i = 0; i < annotations.length; i++)
842     {
843       if (annotations[i] != null)
844       {
845         if (graph != 0)
846         {
847           buffer.append(annotations[i].value);
848         }
849         else if (hasIcons)
850         {
851           buffer.append(annotations[i].secondaryStructure);
852         }
853         else
854         {
855           buffer.append(annotations[i].displayCharacter);
856         }
857       }
858
859       buffer.append(", ");
860     }
861     // TODO: remove disgusting hack for 'special' treatment of consensus line.
862     if (label.indexOf("Consensus") == 0)
863     {
864       buffer.append("\n");
865
866       for (int i = 0; i < annotations.length; i++)
867       {
868         if (annotations[i] != null)
869         {
870           buffer.append(annotations[i].description);
871         }
872
873         buffer.append(", ");
874       }
875     }
876
877     return buffer.toString();
878   }
879
880   public void setThreshold(GraphLine line)
881   {
882     threshold = line;
883   }
884
885   public GraphLine getThreshold()
886   {
887     return threshold;
888   }
889
890   /**
891    * Attach the annotation to seqRef, starting from startRes position. If
892    * alreadyMapped is true then the indices of the annotation[] array are
893    * sequence positions rather than alignment column positions.
894    * 
895    * @param seqRef
896    * @param startRes
897    * @param alreadyMapped
898    *          - annotation are at aligned columns
899    */
900   public void createSequenceMapping(SequenceI seqRef, int startRes,
901           boolean alreadyMapped)
902   {
903
904     if (seqRef == null)
905     {
906       return;
907     }
908     sequenceRef = seqRef;
909     if (annotations == null)
910     {
911       return;
912     }
913     sequenceMapping = new HashMap<>();
914
915     int seqPos;
916
917     for (int i = 0; i < annotations.length; i++)
918     {
919       if (annotations[i] != null)
920       {
921         if (alreadyMapped)
922         {
923           seqPos = seqRef.findPosition(i);
924         }
925         else
926         {
927           seqPos = i + startRes;
928         }
929
930         sequenceMapping.put(new Integer(seqPos), annotations[i]);
931       }
932     }
933
934   }
935
936   /**
937    * When positional annotation and a sequence reference is present, clears and
938    * resizes the annotations array to the current alignment width, and adds
939    * annotation according to aligned positions of the sequenceRef given by
940    * sequenceMapping.
941    */
942   public void adjustForAlignment()
943   {
944     if (sequenceRef == null)
945     {
946       return;
947     }
948
949     if (annotations == null)
950     {
951       return;
952     }
953
954     int a = 0, aSize = sequenceRef.getLength();
955
956     if (aSize == 0)
957     {
958       // Its been deleted
959       return;
960     }
961
962     int position;
963     Annotation[] temp = new Annotation[aSize];
964     Integer index;
965     if (sequenceMapping != null)
966     {
967       for (a = sequenceRef.getStart(); a <= sequenceRef.getEnd(); a++)
968       {
969         index = new Integer(a);
970         Annotation annot = sequenceMapping.get(index);
971         if (annot != null)
972         {
973           position = sequenceRef.findIndex(a) - 1;
974
975           temp[position] = annot;
976         }
977       }
978     }
979     annotations = temp;
980   }
981
982   /**
983    * remove any null entries in annotation row and return the number of non-null
984    * annotation elements.
985    * 
986    * @return
987    */
988   public int compactAnnotationArray()
989   {
990     int i = 0, iSize = annotations.length;
991     while (i < iSize)
992     {
993       if (annotations[i] == null)
994       {
995         if (i + 1 < iSize)
996         {
997           System.arraycopy(annotations, i + 1, annotations, i,
998                   iSize - i - 1);
999         }
1000         iSize--;
1001       }
1002       else
1003       {
1004         i++;
1005       }
1006     }
1007     Annotation[] ann = annotations;
1008     annotations = new Annotation[i];
1009     System.arraycopy(ann, 0, annotations, 0, i);
1010     ann = null;
1011     return iSize;
1012   }
1013
1014   /**
1015    * Associate this annotation with the aligned residues of a particular
1016    * sequence. sequenceMapping will be updated in the following way: null
1017    * sequenceI - existing mapping will be discarded but annotations left in
1018    * mapped positions. valid sequenceI not equal to current sequenceRef: mapping
1019    * is discarded and rebuilt assuming 1:1 correspondence TODO: overload with
1020    * parameter to specify correspondence between current and new sequenceRef
1021    * 
1022    * @param sequenceI
1023    */
1024   public void setSequenceRef(SequenceI sequenceI)
1025   {
1026     if (sequenceI != null)
1027     {
1028       if (sequenceRef != null)
1029       {
1030         boolean rIsDs = sequenceRef.getDatasetSequence() == null,
1031                 tIsDs = sequenceI.getDatasetSequence() == null;
1032         if (sequenceRef != sequenceI
1033                 && (rIsDs && !tIsDs
1034                         && sequenceRef != sequenceI.getDatasetSequence())
1035                 && (!rIsDs && tIsDs
1036                         && sequenceRef.getDatasetSequence() != sequenceI)
1037                 && (!rIsDs && !tIsDs
1038                         && sequenceRef.getDatasetSequence() != sequenceI
1039                                 .getDatasetSequence())
1040                 && !sequenceRef.equals(sequenceI))
1041         {
1042           // if sequenceRef isn't intersecting with sequenceI
1043           // throw away old mapping and reconstruct.
1044           sequenceRef = null;
1045           if (sequenceMapping != null)
1046           {
1047             sequenceMapping = null;
1048             // compactAnnotationArray();
1049           }
1050           createSequenceMapping(sequenceI, 1, true);
1051           adjustForAlignment();
1052         }
1053         else
1054         {
1055           // Mapping carried over
1056           sequenceRef = sequenceI;
1057         }
1058       }
1059       else
1060       {
1061         // No mapping exists
1062         createSequenceMapping(sequenceI, 1, true);
1063         adjustForAlignment();
1064       }
1065     }
1066     else
1067     {
1068       // throw away the mapping without compacting.
1069       sequenceMapping = null;
1070       sequenceRef = null;
1071     }
1072   }
1073
1074   /**
1075    * @return the score
1076    */
1077   public double getScore()
1078   {
1079     return score;
1080   }
1081
1082   /**
1083    * @param score
1084    *          the score to set
1085    */
1086   public void setScore(double score)
1087   {
1088     hasScore = true;
1089     this.score = score;
1090   }
1091
1092   /**
1093    * 
1094    * @return true if annotation has an associated score
1095    */
1096   public boolean hasScore()
1097   {
1098     return hasScore || !Double.isNaN(score);
1099   }
1100
1101   /**
1102    * Score only annotation
1103    * 
1104    * @param label
1105    * @param description
1106    * @param score
1107    */
1108   public AlignmentAnnotation(String label, String description, double score)
1109   {
1110     this(label, description, null);
1111     setScore(score);
1112   }
1113
1114   /**
1115    * copy constructor with edit based on the hidden columns marked in colSel
1116    * 
1117    * @param alignmentAnnotation
1118    * @param colSel
1119    */
1120   public AlignmentAnnotation(AlignmentAnnotation alignmentAnnotation,
1121           HiddenColumns hidden)
1122   {
1123     this(alignmentAnnotation);
1124     if (annotations == null)
1125     {
1126       return;
1127     }
1128     makeVisibleAnnotation(hidden);
1129   }
1130
1131   public void setPadGaps(boolean padgaps, char gapchar)
1132   {
1133     this.padGaps = padgaps;
1134     if (padgaps)
1135     {
1136       hasText = true;
1137       for (int i = 0; i < annotations.length; i++)
1138       {
1139         if (annotations[i] == null)
1140         {
1141           annotations[i] = new Annotation(String.valueOf(gapchar), null,
1142                   ' ', 0f, null);
1143         }
1144         else if (annotations[i].displayCharacter == null
1145                 || annotations[i].displayCharacter.equals(" "))
1146         {
1147           annotations[i].displayCharacter = String.valueOf(gapchar);
1148         }
1149       }
1150     }
1151   }
1152
1153   /**
1154    * format description string for display
1155    * 
1156    * @param seqname
1157    * @return Get the annotation description string optionally prefixed by
1158    *         associated sequence name (if any)
1159    */
1160   public String getDescription(boolean seqname)
1161   {
1162     if (seqname && this.sequenceRef != null)
1163     {
1164       int i = description.toLowerCase().indexOf("<html>");
1165       if (i > -1)
1166       {
1167         // move the html tag to before the sequence reference.
1168         return "<html>" + sequenceRef.getName() + " : "
1169                 + description.substring(i + 6);
1170       }
1171       return sequenceRef.getName() + " : " + description;
1172     }
1173     return description;
1174   }
1175
1176   public boolean isValidStruc()
1177   {
1178     return invalidrnastruc == -1;
1179   }
1180
1181   public long getInvalidStrucPos()
1182   {
1183     return invalidrnastruc;
1184   }
1185
1186   /**
1187    * machine readable ID string indicating what generated this annotation
1188    */
1189   protected String calcId = "";
1190
1191   /**
1192    * properties associated with the calcId
1193    */
1194   protected Map<String, String> properties = new HashMap<>();
1195
1196   /**
1197    * base colour for line graphs. If null, will be set automatically by
1198    * searching the alignment annotation
1199    */
1200   public java.awt.Color _linecolour;
1201
1202   public String getCalcId()
1203   {
1204     return calcId;
1205   }
1206
1207   public void setCalcId(String calcId)
1208   {
1209     this.calcId = calcId;
1210   }
1211
1212   public boolean isRNA()
1213   {
1214     return isrna;
1215   }
1216
1217   /**
1218    * transfer annotation to the given sequence using the given mapping from the
1219    * current positions or an existing sequence mapping
1220    * 
1221    * @param sq
1222    * @param sp2sq
1223    *          map involving sq as To or From
1224    */
1225   public void liftOver(SequenceI sq, Mapping sp2sq)
1226   {
1227     if (sp2sq.getMappedWidth() != sp2sq.getWidth())
1228     {
1229       // TODO: employ getWord/MappedWord to transfer annotation between cDNA and
1230       // Protein reference frames
1231       throw new Error(
1232               "liftOver currently not implemented for transfer of annotation between different types of seqeunce");
1233     }
1234     boolean mapIsTo = (sp2sq != null)
1235             ? (sp2sq.getTo() == sq
1236                     || sp2sq.getTo() == sq.getDatasetSequence())
1237             : false;
1238
1239     // TODO build a better annotation element map and get rid of annotations[]
1240     Map<Integer, Annotation> mapForsq = new HashMap<>();
1241     if (sequenceMapping != null)
1242     {
1243       if (sp2sq != null)
1244       {
1245         for (Entry<Integer, Annotation> ie : sequenceMapping.entrySet())
1246         {
1247           Integer mpos = Integer
1248                   .valueOf(mapIsTo ? sp2sq.getMappedPosition(ie.getKey())
1249                           : sp2sq.getPosition(ie.getKey()));
1250           if (mpos >= sq.getStart() && mpos <= sq.getEnd())
1251           {
1252             mapForsq.put(mpos, ie.getValue());
1253           }
1254         }
1255         sequenceMapping = mapForsq;
1256         sequenceRef = sq;
1257         adjustForAlignment();
1258       }
1259       else
1260       {
1261         // trim positions
1262       }
1263     }
1264   }
1265
1266   /**
1267    * like liftOver but more general.
1268    * 
1269    * Takes an array of int pairs that will be used to update the internal
1270    * sequenceMapping and so shuffle the annotated positions
1271    * 
1272    * @param newref
1273    *          - new sequence reference for the annotation row - if null,
1274    *          sequenceRef is left unchanged
1275    * @param mapping
1276    *          array of ints containing corresponding positions
1277    * @param from
1278    *          - column for current coordinate system (-1 for index+1)
1279    * @param to
1280    *          - column for destination coordinate system (-1 for index+1)
1281    * @param idxoffset
1282    *          - offset added to index when referencing either coordinate system
1283    * @note no checks are made as to whether from and/or to are sensible
1284    * @note caller should add the remapped annotation to newref if they have not
1285    *       already
1286    */
1287   public void remap(SequenceI newref, HashMap<Integer, int[]> mapping,
1288           int from, int to, int idxoffset)
1289   {
1290     if (mapping != null)
1291     {
1292       Map<Integer, Annotation> old = sequenceMapping;
1293       Map<Integer, Annotation> remap = new HashMap<>();
1294       int index = -1;
1295       for (int mp[] : mapping.values())
1296       {
1297         if (index++ < 0)
1298         {
1299           continue;
1300         }
1301         Annotation ann = null;
1302         if (from == -1)
1303         {
1304           ann = sequenceMapping.get(Integer.valueOf(idxoffset + index));
1305         }
1306         else
1307         {
1308           if (mp != null && mp.length > from)
1309           {
1310             ann = sequenceMapping.get(Integer.valueOf(mp[from]));
1311           }
1312         }
1313         if (ann != null)
1314         {
1315           if (to == -1)
1316           {
1317             remap.put(Integer.valueOf(idxoffset + index), ann);
1318           }
1319           else
1320           {
1321             if (to > -1 && to < mp.length)
1322             {
1323               remap.put(Integer.valueOf(mp[to]), ann);
1324             }
1325           }
1326         }
1327       }
1328       sequenceMapping = remap;
1329       old.clear();
1330       if (newref != null)
1331       {
1332         sequenceRef = newref;
1333       }
1334       adjustForAlignment();
1335     }
1336   }
1337
1338   public String getProperty(String property)
1339   {
1340     if (properties == null)
1341     {
1342       return null;
1343     }
1344     return properties.get(property);
1345   }
1346
1347   public void setProperty(String property, String value)
1348   {
1349     if (properties == null)
1350     {
1351       properties = new HashMap<>();
1352     }
1353     properties.put(property, value);
1354   }
1355
1356   public boolean hasProperties()
1357   {
1358     return properties != null && properties.size() > 0;
1359   }
1360
1361   public Collection<String> getProperties()
1362   {
1363     if (properties == null)
1364     {
1365       return Collections.emptyList();
1366     }
1367     return properties.keySet();
1368   }
1369
1370   /**
1371    * Returns the Annotation for the given sequence position (base 1) if any,
1372    * else null
1373    * 
1374    * @param position
1375    * @return
1376    */
1377   public Annotation getAnnotationForPosition(int position)
1378   {
1379     return sequenceMapping == null ? null : sequenceMapping.get(position);
1380
1381   }
1382
1383   /**
1384    * Set the id to "ann" followed by a counter that increments so as to be
1385    * unique for the lifetime of the JVM
1386    */
1387   protected final void setAnnotationId()
1388   {
1389     this.annotationId = ANNOTATION_ID_PREFIX + Long.toString(nextId());
1390   }
1391
1392   /**
1393    * Returns the match for the last unmatched opening RNA helix pair symbol
1394    * preceding the given column, or '(' if nothing found to match.
1395    * 
1396    * @param column
1397    * @return
1398    */
1399   public String getDefaultRnaHelixSymbol(int column)
1400   {
1401     String result = "(";
1402     if (annotations == null)
1403     {
1404       return result;
1405     }
1406
1407     /*
1408      * for each preceding column, if it contains an open bracket, 
1409      * count whether it is still unmatched at column, if so return its pair
1410      * (likely faster than the fancy alternative using stacks)
1411      */
1412     for (int col = column - 1; col >= 0; col--)
1413     {
1414       Annotation annotation = annotations[col];
1415       if (annotation == null)
1416       {
1417         continue;
1418       }
1419       String displayed = annotation.displayCharacter;
1420       if (displayed == null || displayed.length() != 1)
1421       {
1422         continue;
1423       }
1424       char symbol = displayed.charAt(0);
1425       if (!Rna.isOpeningParenthesis(symbol))
1426       {
1427         continue;
1428       }
1429
1430       /*
1431        * found an opening bracket symbol
1432        * count (closing-opening) symbols of this type that follow it,
1433        * up to and excluding the target column; if the count is less
1434        * than 1, the opening bracket is unmatched, so return its match
1435        */
1436       String closer = String
1437               .valueOf(Rna.getMatchingClosingParenthesis(symbol));
1438       String opener = String.valueOf(symbol);
1439       int count = 0;
1440       for (int j = col + 1; j < column; j++)
1441       {
1442         if (annotations[j] != null)
1443         {
1444           String s = annotations[j].displayCharacter;
1445           if (closer.equals(s))
1446           {
1447             count++;
1448           }
1449           else if (opener.equals(s))
1450           {
1451             count--;
1452           }
1453         }
1454       }
1455       if (count < 1)
1456       {
1457         return closer;
1458       }
1459     }
1460     return result;
1461   }
1462
1463   protected static synchronized long nextId()
1464   {
1465     return counter++;
1466   }
1467
1468   /**
1469    * 
1470    * @return true for rows that have a range of values in their annotation set
1471    */
1472   public boolean isQuantitative()
1473   {
1474     return graphMin < graphMax;
1475   }
1476
1477   /**
1478    * delete any columns in alignmentAnnotation that are hidden (including
1479    * sequence associated annotation).
1480    * 
1481    * @param hiddenColumns
1482    *          the set of hidden columns
1483    */
1484   public void makeVisibleAnnotation(HiddenColumns hiddenColumns)
1485   {
1486     if (annotations != null)
1487     {
1488       makeVisibleAnnotation(0, annotations.length, hiddenColumns);
1489     }
1490   }
1491
1492   /**
1493    * delete any columns in alignmentAnnotation that are hidden (including
1494    * sequence associated annotation).
1495    * 
1496    * @param start
1497    *          remove any annotation to the right of this column
1498    * @param end
1499    *          remove any annotation to the left of this column
1500    * @param hiddenColumns
1501    *          the set of hidden columns
1502    */
1503   public void makeVisibleAnnotation(int start, int end,
1504           HiddenColumns hiddenColumns)
1505   {
1506     if (annotations != null)
1507     {
1508       if (hiddenColumns.hasHiddenColumns())
1509       {
1510         removeHiddenAnnotation(start, end, hiddenColumns);
1511       }
1512       else
1513       {
1514         restrict(start, end);
1515       }
1516     }
1517   }
1518
1519   /**
1520    * The actual implementation of deleting hidden annotation columns
1521    * 
1522    * @param start
1523    *          remove any annotation to the right of this column
1524    * @param end
1525    *          remove any annotation to the left of this column
1526    * @param hiddenColumns
1527    *          the set of hidden columns
1528    */
1529   private void removeHiddenAnnotation(int start, int end,
1530           HiddenColumns hiddenColumns)
1531   {
1532     // mangle the alignmentAnnotation annotation array
1533     ArrayList<Annotation[]> annels = new ArrayList<>();
1534     Annotation[] els = null;
1535
1536     int w = 0;
1537
1538     Iterator<int[]> blocks = hiddenColumns.getVisContigsIterator(start,
1539             end + 1, false);
1540
1541     int copylength;
1542     int annotationLength;
1543     while (blocks.hasNext())
1544     {
1545       int[] block = blocks.next();
1546       annotationLength = block[1] - block[0] + 1;
1547
1548       if (blocks.hasNext())
1549       {
1550         // copy just the visible segment of the annotation row
1551         copylength = annotationLength;
1552       }
1553       else
1554       {
1555         if (annotationLength + block[0] <= annotations.length)
1556         {
1557           // copy just the visible segment of the annotation row
1558           copylength = annotationLength;
1559         }
1560         else
1561         {
1562           // copy to the end of the annotation row
1563           copylength = annotations.length - block[0];
1564         }
1565       }
1566
1567       els = new Annotation[annotationLength];
1568       annels.add(els);
1569       System.arraycopy(annotations, block[0], els, 0, copylength);
1570       w += annotationLength;
1571     }
1572
1573     if (w != 0)
1574     {
1575       annotations = new Annotation[w];
1576
1577       w = 0;
1578       for (Annotation[] chnk : annels)
1579       {
1580         System.arraycopy(chnk, 0, annotations, w, chnk.length);
1581         w += chnk.length;
1582       }
1583     }
1584   }
1585
1586   public static Iterable<AlignmentAnnotation> findAnnotations(
1587           Iterable<AlignmentAnnotation> list, SequenceI seq, String calcId,
1588           String label)
1589   {
1590
1591     ArrayList<AlignmentAnnotation> aa = new ArrayList<>();
1592     for (AlignmentAnnotation ann : list)
1593     {
1594       if ((calcId == null || (ann.getCalcId() != null
1595               && ann.getCalcId().equals(calcId)))
1596               && (seq == null || (ann.sequenceRef != null
1597                       && ann.sequenceRef == seq))
1598               && (label == null
1599                       || (ann.label != null && ann.label.equals(label))))
1600       {
1601         aa.add(ann);
1602       }
1603     }
1604     return aa;
1605   }
1606
1607   /**
1608    * Answer true if any annotation matches the calcId passed in (if not null).
1609    * 
1610    * @param list
1611    *          annotation to search
1612    * @param calcId
1613    * @return
1614    */
1615   public static boolean hasAnnotation(List<AlignmentAnnotation> list,
1616           String calcId)
1617   {
1618
1619     if (calcId != null && !"".equals(calcId))
1620     {
1621       for (AlignmentAnnotation a : list)
1622       {
1623         if (a.getCalcId() == calcId)
1624         {
1625           return true;
1626         }
1627       }
1628     }
1629     return false;
1630   }
1631
1632   public static Iterable<AlignmentAnnotation> findAnnotation(
1633           List<AlignmentAnnotation> list, String calcId)
1634   {
1635
1636     List<AlignmentAnnotation> aa = new ArrayList<>();
1637     if (calcId == null)
1638     {
1639       return aa;
1640     }
1641     for (AlignmentAnnotation a : list)
1642     {
1643
1644       if (a.getCalcId() == calcId || (a.getCalcId() != null
1645               && calcId != null && a.getCalcId().equals(calcId)))
1646       {
1647         aa.add(a);
1648       }
1649     }
1650     return aa;
1651   }
1652 }