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