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