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