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