JAL-2759 tidy
[jalview.git] / src / jalview / datamodel / HiddenColumns.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.BitSet;
25 import java.util.Iterator;
26 import java.util.List;
27 import java.util.concurrent.locks.ReentrantReadWriteLock;
28
29 public class HiddenColumns
30 {
31   private static final int HASH_MULTIPLIER = 31;
32
33   private static final ReentrantReadWriteLock LOCK = new ReentrantReadWriteLock();
34
35   private HiddenColumnsCursor cursor = new HiddenColumnsCursor();
36
37   /*
38    * list of hidden column [start, end] ranges; the list is maintained in
39    * ascending start column order
40    */
41   private ArrayList<int[]> hiddenColumns;
42
43   /**
44    * Constructor
45    */
46   public HiddenColumns()
47   {
48   }
49
50   /**
51    * Copy constructor
52    * 
53    * @param copy
54    */
55   public HiddenColumns(HiddenColumns copy)
56   {
57     try
58     {
59       LOCK.writeLock().lock();
60       if (copy != null)
61       {
62         if (copy.hiddenColumns != null)
63         {
64           hiddenColumns = new ArrayList<>();
65           Iterator<int[]> it = copy.iterator();
66           while (it.hasNext())
67           {
68             hiddenColumns.add(it.next());
69           }
70           cursor.resetCursor(hiddenColumns);
71         }
72       }
73     } finally
74     {
75       LOCK.writeLock().unlock();
76     }
77   }
78
79   /**
80    * Copy constructor within bounds and with offset. Copies hidden column
81    * regions fully contained between start and end, and offsets positions by
82    * subtracting offset.
83    * 
84    * @param copy
85    *          HiddenColumns instance to copy from
86    * @param start
87    *          lower bound to copy from
88    * @param end
89    *          upper bound to copy to
90    * @param offset
91    *          offset to subtract from each region boundary position
92    * 
93    */
94   public HiddenColumns(HiddenColumns copy, int start, int end, int offset)
95   {
96     try
97     {
98       LOCK.writeLock().lock();
99       if (copy != null)
100       {
101         hiddenColumns = new ArrayList<>();
102         Iterator<int[]> it = copy.getBoundedIterator(start, end);
103         while (it.hasNext())
104         {
105           int[] region = it.next();
106           // still need to check boundaries because iterator returns
107           // all overlapping regions and we need contained regions
108           if (region[0] >= start && region[1] <= end)
109           {
110             hiddenColumns.add(
111                     new int[]
112             { region[0] - offset, region[1] - offset });
113           }
114         }
115         cursor.resetCursor(hiddenColumns);
116       }
117     } finally
118     {
119       LOCK.writeLock().unlock();
120     }
121   }
122
123   /**
124    * Output regions data as a string. String is in the format:
125    * reg0[0]<between>reg0[1]<delimiter>reg1[0]<between>reg1[1] ... regn[1]
126    * 
127    * @param delimiter
128    *          string to delimit regions
129    * @param betweenstring
130    *          to put between start and end region values
131    * @return regions formatted according to delimiter and between strings
132    */
133   public String regionsToString(String delimiter, String between)
134   {
135     try
136     {
137       LOCK.readLock().lock();
138       StringBuilder regionBuilder = new StringBuilder();
139       if (hiddenColumns != null)
140       {
141         Iterator<int[]> it = hiddenColumns.iterator();
142         while (it.hasNext())
143         {
144           int[] range = it.next();
145           regionBuilder.append(delimiter).append(range[0]).append(between)
146                   .append(range[1]);
147           if (!it.hasNext())
148           {
149             regionBuilder.deleteCharAt(0);
150           }
151         }
152       }
153       return regionBuilder.toString();
154     } finally
155     {
156       LOCK.readLock().unlock();
157     }
158   }
159
160   /**
161    * Find the number of hidden columns
162    * 
163    * @return number of hidden columns
164    */
165   public int getSize()
166   {
167     try
168     {
169       LOCK.readLock().lock();
170       int size = 0;
171       if (hiddenColumns != null)
172       {
173         Iterator<int[]> it = hiddenColumns.iterator();
174         while (it.hasNext())
175         {
176           int[] range = it.next();
177           size += range[1] - range[0] + 1;
178         }
179       }
180       return size;
181     } finally
182     {
183       LOCK.readLock().unlock();
184     }
185   }
186
187   /**
188    * Get the number of distinct hidden regions
189    * 
190    * @return number of regions
191    */
192   public int getNumberOfRegions()
193   {
194     try
195     {
196       LOCK.readLock().lock();
197       int num = 0;
198       if (hasHiddenColumns())
199       {
200         num = hiddenColumns.size();
201       }
202       return num;
203     } finally
204     {
205       LOCK.readLock().unlock();
206     }
207   }
208
209   @Override
210   public boolean equals(Object obj)
211   {
212     try
213     {
214       LOCK.readLock().lock();
215
216       if (!(obj instanceof HiddenColumns))
217       {
218         return false;
219       }
220       HiddenColumns that = (HiddenColumns) obj;
221
222       /*
223        * check hidden columns are either both null, or match
224        */
225       if (this.hiddenColumns == null)
226       {
227         return (that.hiddenColumns == null);
228       }
229       if (that.hiddenColumns == null
230               || that.hiddenColumns.size() != this.hiddenColumns.size())
231       {
232         return false;
233       }
234
235       Iterator<int[]> it = hiddenColumns.iterator();
236       Iterator<int[]> thatit = that.iterator();
237       while (it.hasNext())
238       {
239         int[] thisRange = it.next();
240         int[] thatRange = thatit.next();
241         if (thisRange[0] != thatRange[0] || thisRange[1] != thatRange[1])
242         {
243           return false;
244         }
245       }
246       return true;
247     } finally
248     {
249       LOCK.readLock().unlock();
250     }
251   }
252
253   /**
254    * Return absolute column index for a visible column index
255    * 
256    * @param column
257    *          int column index in alignment view (count from zero)
258    * @return alignment column index for column
259    */
260   public int adjustForHiddenColumns(int column)
261   {
262     try
263     {
264       LOCK.readLock().lock();
265       int result = column;
266
267       if (hiddenColumns != null)
268       {
269         result += cursor.getHiddenOffset(column);
270       }
271
272       return result;
273     } finally
274     {
275       LOCK.readLock().unlock();
276     }
277   }
278
279   /**
280    * Use this method to find out where a column will appear in the visible
281    * alignment when hidden columns exist. If the column is not visible, then the
282    * left-most visible column will always be returned.
283    * 
284    * @param hiddenColumn
285    *          the column index in the full alignment including hidden columns
286    * @return the position of the column in the visible alignment
287    */
288   public int findColumnPosition(int hiddenColumn)
289   {
290     try
291     {
292       LOCK.readLock().lock();
293       int result = hiddenColumn;
294
295       if (hiddenColumns != null)
296       {
297         int index = cursor.findRegionForColumn(hiddenColumn);
298         int hiddenBeforeCol = cursor.getHiddenSoFar();
299     
300         // just subtract hidden cols count - this works fine if column is
301         // visible
302         result = hiddenColumn - hiddenBeforeCol;
303     
304         // now check in case column is hidden - it will be in the returned
305         // hidden region
306         if (index < hiddenColumns.size())
307         {
308           int[] region = hiddenColumns.get(index);
309           if (hiddenColumn >= region[0] && hiddenColumn <= region[1])
310           {
311             // actually col is hidden, return region[0]-1
312             // unless region[0]==0 in which case return 0
313             if (region[0] == 0)
314             {
315               result = 0;
316             }
317             else
318             {
319               result = region[0] - 1 - hiddenBeforeCol;
320             }
321           }
322         }
323       }
324
325       return result; // return the shifted position after removing hidden
326                      // columns.
327     } finally
328     {
329       LOCK.readLock().unlock();
330     }
331   }
332
333   /**
334    * Find the visible column which is a given visible number of columns to the
335    * left of another visible column. i.e. for a startColumn x, the column which
336    * is distance 1 away will be column x-1.
337    * 
338    * @param visibleDistance
339    *          the number of visible columns to offset by
340    * @param startColumn
341    *          the column to start from
342    * @return the position of the column in the visible alignment
343    */
344   public int subtractVisibleColumns(int visibleDistance, int startColumn)
345   {
346     try
347     {
348       LOCK.readLock().lock();
349       int distance = visibleDistance;
350
351       // in case startColumn is in a hidden region, move it to the left
352       int start = adjustForHiddenColumns(findColumnPosition(startColumn));
353
354       Iterator<int[]> it = new ReverseRegionsIterator(0, start,
355               hiddenColumns);
356
357       while (it.hasNext() && (distance > 0))
358       {
359         int[] region = it.next();
360
361         if (start > region[1])
362         {
363           // subtract the gap to right of region from distance
364           if (start - region[1] <= distance)
365           {
366             distance -= start - region[1];
367             start = region[0] - 1;
368           }
369           else
370           {
371             start = start - distance;
372             distance = 0;
373           }
374         }
375       }
376
377       return start - distance;
378
379     } finally
380     {
381       LOCK.readLock().unlock();
382     }
383   }
384
385   /**
386    * This method returns the rightmost limit of a region of an alignment with
387    * hidden columns. In otherwords, the next hidden column.
388    * 
389    * @param alPos
390    *          the absolute (visible) alignmentPosition to find the next hidden
391    *          column for
392    */
393   public int getHiddenBoundaryRight(int alPos)
394   {
395     try
396     {
397       LOCK.readLock().lock();
398       if (hiddenColumns != null)
399       {
400         int index = cursor.findRegionForColumn(alPos);
401         if (index < hiddenColumns.size())
402         {
403           int[] region = hiddenColumns.get(index);
404           if (alPos < region[0])
405           {
406             return region[0];
407           }
408           else if ((alPos <= region[1])
409                   && (index + 1 < hiddenColumns.size()))
410           {
411             // alPos is within a hidden region, return the next one
412             // if there is one
413             region = hiddenColumns.get(index + 1);
414             return region[0];
415           }
416         }
417       }
418       return alPos;
419     } finally
420     {
421       LOCK.readLock().unlock();
422     }
423   }
424
425   /**
426    * This method returns the leftmost limit of a region of an alignment with
427    * hidden columns. In otherwords, the previous hidden column.
428    * 
429    * @param alPos
430    *          the absolute (visible) alignmentPosition to find the previous
431    *          hidden column for
432    */
433   public int getHiddenBoundaryLeft(int alPos)
434   {
435     try
436     {
437       LOCK.readLock().lock();
438
439       if (hiddenColumns != null)
440       {
441         int index = cursor.findRegionForColumn(alPos);
442
443         if (index > 0)
444         {
445           int[] region = hiddenColumns.get(index - 1);
446           return region[1];
447         }
448       }
449       return alPos;
450     } finally
451     {
452       LOCK.readLock().unlock();
453     }
454   }
455
456   /**
457    * Adds the specified column range to the hidden columns collection
458    * 
459    * @param start
460    *          start of range to add (absolute position in alignment)
461    * @param end
462    *          end of range to add (absolute position in alignment)
463    */
464   public void hideColumns(int start, int end)
465   {
466     boolean wasAlreadyLocked = false;
467     try
468     {
469       // check if the write lock was already locked by this thread,
470       // as this method can be called internally in loops within HiddenColumns
471       if (!LOCK.isWriteLockedByCurrentThread())
472       {
473         LOCK.writeLock().lock();
474       }
475       else
476       {
477         wasAlreadyLocked = true;
478       }
479
480       if (hiddenColumns == null)
481       {
482         hiddenColumns = new ArrayList<>();
483       }
484
485       /*
486        * new range follows everything else; check first to avoid looping over whole hiddenColumns collection
487        */
488       if (hiddenColumns.isEmpty()
489               || start > hiddenColumns.get(hiddenColumns.size() - 1)[1])
490       {
491         hiddenColumns.add(new int[] { start, end });
492       }
493       else
494       {
495         /*
496          * traverse existing hidden ranges and insert / amend / append as
497          * appropriate
498          */
499         boolean added = false;
500         for (int i = 0; !added && i < hiddenColumns.size(); i++)
501         {
502           added = insertRangeAtRegion(i, start, end);
503         } // for
504       }
505       if (!wasAlreadyLocked)
506       {
507         cursor.resetCursor(hiddenColumns);
508       }
509     } finally
510     {
511       if (!wasAlreadyLocked)
512       {
513         LOCK.writeLock().unlock();
514       }
515     }
516   }
517
518   /**
519    * Insert [start, range] at the region at index i in hiddenColumns, if
520    * feasible
521    * 
522    * @param i
523    *          index to insert at
524    * @param start
525    *          start of range to insert
526    * @param end
527    *          end of range to insert
528    * @return true if range was successfully inserted
529    */
530   private boolean insertRangeAtRegion(int i, int start, int end)
531   {
532     boolean added = false;
533
534     int[] region = hiddenColumns.get(i);
535     if (end < region[0] - 1)
536     {
537       /*
538        * insert discontiguous preceding range
539        */
540       hiddenColumns.add(i, new int[] { start, end });
541       added = true;
542     }
543     else if (end <= region[1])
544     {
545       /*
546        * new range overlaps existing, or is contiguous preceding it - adjust
547        * start column
548        */
549       region[0] = Math.min(region[0], start);
550       added = true;
551     }
552     else if (start <= region[1] + 1)
553     {
554       /*
555        * new range overlaps existing, or is contiguous following it - adjust
556        * start and end columns
557        */
558       region[0] = Math.min(region[0], start);
559       region[1] = Math.max(region[1], end);
560
561       /*
562        * also update or remove any subsequent ranges 
563        * that are overlapped
564        */
565       while (i < hiddenColumns.size() - 1)
566       {
567         int[] nextRegion = hiddenColumns.get(i + 1);
568         if (nextRegion[0] > end + 1)
569         {
570           /*
571            * gap to next hidden range - no more to update
572            */
573           break;
574         }
575         region[1] = Math.max(nextRegion[1], end);
576         hiddenColumns.subList(i + 1, i + 2).clear();
577       }
578       added = true;
579     }
580     return added;
581   }
582
583   /**
584    * Answers if a column in the alignment is visible
585    * 
586    * @param column
587    *          absolute position of column in the alignment
588    * @return true if column is visible
589    */
590   public boolean isVisible(int column)
591   {
592     try
593     {
594       LOCK.readLock().lock();
595
596       Iterator<int[]> it = new RegionsIterator(column, column,
597               hiddenColumns, cursor);
598       while (it.hasNext())
599       {
600         int[] region = it.next();
601         if (column >= region[0] && column <= region[1])
602         {
603           return false;
604         }
605       }
606
607       return true;
608     } finally
609     {
610       LOCK.readLock().unlock();
611     }
612   }
613
614   /**
615    * Get the visible sections of a set of sequences
616    * 
617    * @param start
618    *          sequence position to start from
619    * @param end
620    *          sequence position to end at
621    * @param seqs
622    *          an array of sequences
623    * @return an array of strings encoding the visible parts of each sequence
624    */
625   public String[] getVisibleSequenceStrings(int start, int end,
626           SequenceI[] seqs)
627   {
628     try
629     {
630       LOCK.readLock().lock();
631       int iSize = seqs.length;
632       String[] selections = new String[iSize];
633       if (hiddenColumns != null && hiddenColumns.size() > 0)
634       {
635         for (int i = 0; i < iSize; i++)
636         {
637           StringBuffer visibleSeq = new StringBuffer();
638
639           Iterator<int[]> blocks = new VisibleContigsIterator(start,
640                   end + 1, hiddenColumns);
641
642           while (blocks.hasNext())
643           {
644             int[] block = blocks.next();
645             if (blocks.hasNext())
646             {
647               visibleSeq
648                       .append(seqs[i].getSequence(block[0], block[1] + 1));
649             }
650             else
651             {
652               visibleSeq
653                       .append(seqs[i].getSequence(block[0], block[1]));
654             }
655           }
656
657           selections[i] = visibleSeq.toString();
658         }
659       }
660       else
661       {
662         for (int i = 0; i < iSize; i++)
663         {
664           selections[i] = seqs[i].getSequenceAsString(start, end);
665         }
666       }
667
668       return selections;
669     } finally
670     {
671       LOCK.readLock().unlock();
672     }
673   }
674
675   /**
676    * Locate the first position visible for this sequence. If seq isn't visible
677    * then return the position of the left side of the hidden boundary region.
678    * 
679    * @param seq
680    *          sequence to find position for
681    * @return visible start position
682    */
683   public int locateVisibleStartOfSequence(SequenceI seq)
684   {
685     try
686     {
687       LOCK.readLock().lock();
688       int start = 0;
689
690       if (hiddenColumns == null || hiddenColumns.size() == 0)
691       {
692         return seq.findIndex(seq.getStart()) - 1;
693       }
694
695       // Simply walk along the sequence whilst watching for hidden column
696       // boundaries
697       Iterator<int[]> regions = hiddenColumns.iterator();
698       int hideStart = seq.getLength();
699       int hideEnd = -1;
700       int visPrev = 0;
701       int visNext = 0;
702       boolean foundStart = false;
703
704       // step through the non-gapped positions of the sequence
705       for (int i = seq.getStart(); i <= seq.getEnd() && (!foundStart); i++)
706       {
707         // get alignment position of this residue in the sequence
708         int p = seq.findIndex(i) - 1;
709
710         // update hidden region start/end
711         while (hideEnd < p && regions.hasNext())
712         {
713           int[] region = regions.next();
714           visPrev = visNext;
715           visNext += region[0] - visPrev;
716           hideStart = region[0];
717           hideEnd = region[1];
718         }
719         if (hideEnd < p)
720         {
721           hideStart = seq.getLength();
722         }
723         // update visible boundary for sequence
724         if (p < hideStart)
725         {
726           start = p;
727           foundStart = true;
728         }
729       }
730
731       if (foundStart)
732       {
733         return findColumnPosition(start);
734       }
735       // otherwise, sequence was completely hidden
736       return visPrev;
737     } finally
738     {
739       LOCK.readLock().unlock();
740     }
741   }
742
743   /**
744    * delete any columns in alignmentAnnotation that are hidden (including
745    * sequence associated annotation).
746    * 
747    * @param alignmentAnnotation
748    */
749   public void makeVisibleAnnotation(AlignmentAnnotation alignmentAnnotation)
750   {
751     makeVisibleAnnotation(0, alignmentAnnotation.annotations.length,
752             alignmentAnnotation);
753   }
754
755   /**
756    * delete any columns in alignmentAnnotation that are hidden (including
757    * sequence associated annotation).
758    * 
759    * @param start
760    *          remove any annotation to the right of this column
761    * @param end
762    *          remove any annotation to the left of this column
763    * @param alignmentAnnotation
764    *          the annotation to operate on
765    */
766   public void makeVisibleAnnotation(int start, int end,
767           AlignmentAnnotation alignmentAnnotation)
768   {
769     try
770     {
771       LOCK.readLock().lock();
772
773       int startFrom = start;
774       int endAt = end;
775
776       if (alignmentAnnotation.annotations != null)
777       {
778         if (hiddenColumns != null && hiddenColumns.size() > 0)
779         {
780           removeHiddenAnnotation(startFrom, endAt, alignmentAnnotation);
781         }
782         else
783         {
784           alignmentAnnotation.restrict(startFrom, endAt);
785         }
786       }
787     } finally
788     {
789       LOCK.readLock().unlock();
790     }
791   }
792
793   private void removeHiddenAnnotation(int start, int end,
794           AlignmentAnnotation alignmentAnnotation)
795   {
796     // mangle the alignmentAnnotation annotation array
797     ArrayList<Annotation[]> annels = new ArrayList<>();
798     Annotation[] els = null;
799
800     int w = 0;
801     
802     Iterator<int[]> blocks = new VisibleContigsIterator(start, end + 1,
803             hiddenColumns);
804
805     int copylength;
806     int annotationLength;
807     while (blocks.hasNext())
808     {
809       int[] block = blocks.next();
810       annotationLength = block[1] - block[0] + 1;
811     
812       if (blocks.hasNext())
813       {
814         // copy just the visible segment of the annotation row
815         copylength = annotationLength;
816       }
817       else
818       {
819         if (annotationLength + block[0] <= alignmentAnnotation.annotations.length)
820         {
821           // copy just the visible segment of the annotation row
822           copylength = annotationLength;
823         }
824         else
825         {
826           // copy to the end of the annotation row
827           copylength = alignmentAnnotation.annotations.length - block[0];
828         }
829       }
830       
831       els = new Annotation[annotationLength];
832       annels.add(els);
833       System.arraycopy(alignmentAnnotation.annotations, block[0], els, 0,
834               copylength);
835       w += annotationLength;
836     }
837     
838     if (w != 0)
839     {
840       alignmentAnnotation.annotations = new Annotation[w];
841
842       w = 0;
843       for (Annotation[] chnk : annels)
844       {
845         System.arraycopy(chnk, 0, alignmentAnnotation.annotations, w,
846                 chnk.length);
847         w += chnk.length;
848       }
849     }
850   }
851
852   /**
853    * 
854    * @return true if there are columns hidden
855    */
856   public boolean hasHiddenColumns()
857   {
858     try
859     {
860       LOCK.readLock().lock();
861       return hiddenColumns != null && hiddenColumns.size() > 0;
862     } finally
863     {
864       LOCK.readLock().unlock();
865     }
866   }
867
868   /**
869    * 
870    * @return true if there are more than one set of columns hidden
871    */
872   public boolean hasManyHiddenColumns()
873   {
874     try
875     {
876       LOCK.readLock().lock();
877       return hiddenColumns != null && hiddenColumns.size() > 1;
878     } finally
879     {
880       LOCK.readLock().unlock();
881     }
882   }
883
884   /**
885    * mark the columns corresponding to gap characters as hidden in the column
886    * selection
887    * 
888    * @param sr
889    */
890   public void hideInsertionsFor(SequenceI sr)
891   {
892     try
893     {
894       LOCK.writeLock().lock();
895       List<int[]> inserts = sr.getInsertions();
896       for (int[] r : inserts)
897       {
898         hideColumns(r[0], r[1]);
899       }
900       cursor.resetCursor(hiddenColumns);
901     } finally
902     {
903       LOCK.writeLock().unlock();
904     }
905   }
906
907   /**
908    * Unhides, and adds to the selection list, all hidden columns
909    */
910   public void revealAllHiddenColumns(ColumnSelection sel)
911   {
912     try
913     {
914       LOCK.writeLock().lock();
915       if (hiddenColumns != null)
916       {
917         Iterator<int[]> it = hiddenColumns.iterator();
918         while (it.hasNext())
919         {
920           int[] region = it.next();
921           for (int j = region[0]; j < region[1] + 1; j++)
922           {
923             sel.addElement(j);
924           }
925         }
926         hiddenColumns = null;
927         cursor.resetCursor(hiddenColumns);
928       }
929     } finally
930     {
931       LOCK.writeLock().unlock();
932     }
933   }
934
935   /**
936    * Reveals, and marks as selected, the hidden column range with the given
937    * start column
938    * 
939    * @param start
940    */
941   public void revealHiddenColumns(int start, ColumnSelection sel)
942   {
943     try
944     {
945       LOCK.writeLock().lock();
946
947       if (hiddenColumns != null)
948       {
949         int regionIndex = cursor.findRegionForColumn(start);
950
951         if (regionIndex != -1 && regionIndex != hiddenColumns.size())
952         {
953           // regionIndex is the region which either contains start
954           // or lies to the right of start
955           int[] region = hiddenColumns.get(regionIndex);
956           if (start == region[0])
957           {
958             for (int j = region[0]; j < region[1] + 1; j++)
959             {
960               sel.addElement(j);
961             }
962             hiddenColumns.remove(regionIndex);
963
964             if (hiddenColumns.isEmpty())
965             {
966               hiddenColumns = null;
967             }
968             cursor.updateForDeletedRegion(hiddenColumns);
969           }
970         }
971       }
972     } finally
973     {
974       LOCK.writeLock().unlock();
975     }
976   }
977
978   /**
979    * Add gaps into the sequences aligned to profileseq under the given
980    * AlignmentView
981    * 
982    * @param profileseq
983    * @param al
984    *          - alignment to have gaps inserted into it
985    * @param input
986    *          - alignment view where sequence corresponding to profileseq is
987    *          first entry
988    * @return new HiddenColumns for new alignment view, with insertions into
989    *         profileseq marked as hidden.
990    */
991   public static HiddenColumns propagateInsertions(SequenceI profileseq,
992           AlignmentI al, AlignmentView input)
993   {
994     int profsqpos = 0;
995
996     char gc = al.getGapCharacter();
997     Object[] alandhidden = input.getAlignmentAndHiddenColumns(gc);
998     HiddenColumns nview = (HiddenColumns) alandhidden[1];
999     SequenceI origseq = ((SequenceI[]) alandhidden[0])[profsqpos];
1000     nview.propagateInsertions(profileseq, al, origseq);
1001     return nview;
1002   }
1003
1004   /**
1005    * 
1006    * @param profileseq
1007    *          - sequence in al which corresponds to origseq
1008    * @param al
1009    *          - alignment which is to have gaps inserted into it
1010    * @param origseq
1011    *          - sequence corresponding to profileseq which defines gap map for
1012    *          modifying al
1013    */
1014   private void propagateInsertions(SequenceI profileseq, AlignmentI al,
1015           SequenceI origseq)
1016   {
1017     try
1018     {
1019       LOCK.writeLock().lock();
1020
1021       char gc = al.getGapCharacter();
1022
1023       // take the set of hidden columns, and the set of gaps in origseq,
1024       // and remove all the hidden gaps from hiddenColumns
1025
1026       // first get the gaps as a Bitset
1027       BitSet gaps = origseq.gapBitset();
1028
1029       // now calculate hidden ^ not(gap)
1030       BitSet hidden = new BitSet();
1031       markHiddenRegions(hidden);
1032       hidden.andNot(gaps);
1033       hiddenColumns = null;
1034       this.hideMarkedBits(hidden);
1035
1036       // for each sequence in the alignment, except the profile sequence,
1037       // insert gaps corresponding to each hidden region
1038       // but where each hidden column region is shifted backwards by the number
1039       // of
1040       // preceding visible gaps
1041       // update hidden columns at the same time
1042       Iterator<int[]> regions = hiddenColumns.iterator();
1043       ArrayList<int[]> newhidden = new ArrayList<>();
1044
1045       int numGapsBefore = 0;
1046       int gapPosition = 0;
1047       while (regions.hasNext())
1048       {
1049         // get region coordinates accounting for gaps
1050         // we can rely on gaps not being *in* hidden regions because we already
1051         // removed those
1052         int[] region = regions.next();
1053         while (gapPosition < region[0])
1054         {
1055           gapPosition++;
1056           if (gaps.get(gapPosition))
1057           {
1058             numGapsBefore++;
1059           }
1060         }
1061
1062         int left = region[0] - numGapsBefore;
1063         int right = region[1] - numGapsBefore;
1064         newhidden.add(new int[] { left, right });
1065
1066         // make a string with number of gaps = length of hidden region
1067         StringBuffer sb = new StringBuffer();
1068         for (int s = 0; s < right - left + 1; s++)
1069         {
1070           sb.append(gc);
1071         }
1072         padGaps(sb, left, profileseq, al);
1073
1074       }
1075       hiddenColumns = newhidden;
1076       cursor.resetCursor(hiddenColumns);
1077     } finally
1078     {
1079       LOCK.writeLock().unlock();
1080     }
1081   }
1082
1083   /**
1084    * Pad gaps in all sequences in alignment except profileseq
1085    * 
1086    * @param sb
1087    *          gap string to insert
1088    * @param left
1089    *          position to insert at
1090    * @param profileseq
1091    *          sequence not to pad
1092    * @param al
1093    *          alignment to pad sequences in
1094    */
1095   private void padGaps(StringBuffer sb, int pos, SequenceI profileseq,
1096           AlignmentI al)
1097   {
1098     // loop over the sequences and pad with gaps where required
1099     for (int s = 0, ns = al.getHeight(); s < ns; s++)
1100     {
1101       SequenceI sqobj = al.getSequenceAt(s);
1102       if (sqobj != profileseq)
1103       {
1104         String sq = al.getSequenceAt(s).getSequenceAsString();
1105         if (sq.length() <= pos)
1106         {
1107           // pad sequence
1108           int diff = pos - sq.length() - 1;
1109           if (diff > 0)
1110           {
1111             // pad gaps
1112             sq = sq + sb;
1113             while ((diff = pos - sq.length() - 1) > 0)
1114             {
1115               if (diff >= sb.length())
1116               {
1117                 sq += sb.toString();
1118               }
1119               else
1120               {
1121                 char[] buf = new char[diff];
1122                 sb.getChars(0, diff, buf, 0);
1123                 sq += buf.toString();
1124               }
1125             }
1126           }
1127           sq += sb.toString();
1128         }
1129         else
1130         {
1131           al.getSequenceAt(s).setSequence(
1132                   sq.substring(0, pos) + sb.toString() + sq.substring(pos));
1133         }
1134       }
1135     }
1136   }
1137
1138   /**
1139    * Returns a hashCode built from hidden column ranges
1140    */
1141   @Override
1142   public int hashCode()
1143   {
1144     try
1145     {
1146       LOCK.readLock().lock();
1147       int hashCode = 1;
1148       Iterator<int[]> it = hiddenColumns.iterator();
1149       while (it.hasNext())
1150       {
1151         int[] hidden = it.next();
1152         hashCode = HASH_MULTIPLIER * hashCode + hidden[0];
1153         hashCode = HASH_MULTIPLIER * hashCode + hidden[1];
1154       }
1155       return hashCode;
1156     } finally
1157     {
1158       LOCK.readLock().unlock();
1159     }
1160   }
1161
1162   /**
1163    * Hide columns corresponding to the marked bits
1164    * 
1165    * @param inserts
1166    *          - columns map to bits starting from zero
1167    */
1168   public void hideMarkedBits(BitSet inserts)
1169   {
1170     try
1171     {
1172       LOCK.writeLock().lock();
1173       for (int firstSet = inserts
1174               .nextSetBit(0), lastSet = 0; firstSet >= 0; firstSet = inserts
1175                       .nextSetBit(lastSet))
1176       {
1177         lastSet = inserts.nextClearBit(firstSet);
1178         hideColumns(firstSet, lastSet - 1);
1179       }
1180       cursor.resetCursor(hiddenColumns);
1181     } finally
1182     {
1183       LOCK.writeLock().unlock();
1184     }
1185   }
1186
1187   /**
1188    * 
1189    * @param inserts
1190    *          BitSet where hidden columns will be marked
1191    */
1192   public void markHiddenRegions(BitSet inserts)
1193   {
1194     try
1195     {
1196       LOCK.readLock().lock();
1197       if (hiddenColumns == null)
1198       {
1199         return;
1200       }
1201       Iterator<int[]> it = hiddenColumns.iterator();
1202       while (it.hasNext())
1203       {
1204         int[] range = it.next();
1205         inserts.set(range[0], range[1] + 1);
1206       }
1207     } finally
1208     {
1209       LOCK.readLock().unlock();
1210     }
1211   }
1212
1213   /**
1214    * Calculate the visible start and end index of an alignment.
1215    * 
1216    * @param width
1217    *          full alignment width
1218    * @return integer array where: int[0] = startIndex, and int[1] = endIndex
1219    */
1220   public int[] getVisibleStartAndEndIndex(int width)
1221   {
1222     try
1223     {
1224       LOCK.readLock().lock();
1225       int[] alignmentStartEnd = new int[] { 0, width - 1 };
1226       int startPos = alignmentStartEnd[0];
1227       int endPos = alignmentStartEnd[1];
1228
1229       int[] lowestRange = new int[] { -1, -1 };
1230       int[] higestRange = new int[] { -1, -1 };
1231
1232       if (hiddenColumns == null)
1233       {
1234         return new int[] { startPos, endPos };
1235       }
1236
1237       Iterator<int[]> it = hiddenColumns.iterator();
1238       while (it.hasNext())
1239       {
1240         int[] range = it.next();
1241         lowestRange = (range[0] <= startPos) ? range : lowestRange;
1242         higestRange = (range[1] >= endPos) ? range : higestRange;
1243       }
1244
1245       if (lowestRange[0] == -1 && lowestRange[1] == -1)
1246       {
1247         startPos = alignmentStartEnd[0];
1248       }
1249       else
1250       {
1251         startPos = lowestRange[1] + 1;
1252       }
1253
1254       if (higestRange[0] == -1 && higestRange[1] == -1)
1255       {
1256         endPos = alignmentStartEnd[1];
1257       }
1258       else
1259       {
1260         endPos = higestRange[0] - 1;
1261       }
1262       return new int[] { startPos, endPos };
1263     } finally
1264     {
1265       LOCK.readLock().unlock();
1266     }
1267   }
1268
1269   /**
1270    * Finds the hidden region (if any) which starts or ends at res
1271    * 
1272    * @param res
1273    *          visible residue position, unadjusted for hidden columns
1274    * @return region as [start,end] or null if no matching region is found
1275    */
1276   public int[] getRegionWithEdgeAtRes(int res)
1277   {
1278     try
1279     {
1280       LOCK.readLock().lock();
1281       int adjres = adjustForHiddenColumns(res);
1282
1283       int[] reveal = null;
1284
1285       if (hiddenColumns != null)
1286       {
1287         int regionindex = cursor.findRegionForColumn(adjres - 1);
1288         if (regionindex < hiddenColumns.size()
1289                 && hiddenColumns.get(regionindex)[1] == adjres - 1)
1290         {
1291           reveal = hiddenColumns.get(regionindex);
1292         }
1293         else
1294         {
1295           regionindex = cursor.findRegionForColumn(adjres + 1);
1296           if (regionindex < hiddenColumns.size()
1297                   && hiddenColumns.get(regionindex)[0] == adjres + 1)
1298           {
1299             reveal = hiddenColumns.get(regionindex);
1300           }
1301         }
1302       }
1303       return reveal;
1304
1305     } finally
1306     {
1307       LOCK.readLock().unlock();
1308     }
1309   }
1310
1311   /**
1312    * Return an iterator over the hidden regions
1313    */
1314   public Iterator<int[]> iterator()
1315   {
1316     try
1317     {
1318       LOCK.readLock().lock();
1319       return new BoundedHiddenColsIterator(hiddenColumns);
1320     } finally
1321     {
1322       LOCK.readLock().unlock();
1323     }
1324   }
1325
1326   /**
1327    * Return a bounded iterator over the hidden regions
1328    * 
1329    * @param start
1330    *          position to start from (inclusive, absolute column position)
1331    * @param end
1332    *          position to end at (inclusive, absolute column position)
1333    * @return
1334    */
1335   public Iterator<int[]> getBoundedIterator(int start, int end)
1336   {
1337     try
1338     {
1339       LOCK.readLock().lock();
1340       return new BoundedHiddenColsIterator(start, end, hiddenColumns);
1341     } finally
1342     {
1343       LOCK.readLock().unlock();
1344     }
1345   }
1346
1347   /**
1348    * Return a bounded iterator over the *visible* start positions of hidden
1349    * regions
1350    * 
1351    * @param start
1352    *          position to start from (inclusive, visible column position)
1353    * @param end
1354    *          position to end at (inclusive, visible column position)
1355    */
1356   public Iterator<Integer> getBoundedStartIterator(int start, int end)
1357   {
1358     try
1359     {
1360       LOCK.readLock().lock();
1361       return new BoundedStartRegionIterator(start, end, hiddenColumns);
1362     } finally
1363     {
1364       LOCK.readLock().unlock();
1365     }
1366   }
1367
1368   /**
1369    * Return an iterator over visible *columns* (not regions) between the given
1370    * start and end boundaries
1371    * 
1372    * @param start
1373    *          first column (inclusive)
1374    * @param end
1375    *          last column (inclusive)
1376    */
1377   public Iterator<Integer> getVisibleColsIterator(int start, int end)
1378   {
1379     try
1380     {
1381       LOCK.readLock().lock();
1382       return new VisibleColsIterator(start, end, hiddenColumns);
1383     } finally
1384     {
1385       LOCK.readLock().unlock();
1386     }
1387   }
1388
1389   /**
1390    * return an iterator over visible segments between the given start and end
1391    * boundaries
1392    * 
1393    * @param start
1394    *          (first column inclusive from 0)
1395    * @param end
1396    *          (last column - not inclusive)
1397    */
1398   public Iterator<int[]> getVisContigsIterator(int start, int end)
1399   {
1400     try
1401     {
1402       LOCK.readLock().lock();
1403       return new VisibleContigsIterator(start, end, hiddenColumns);
1404     } finally
1405     {
1406       LOCK.readLock().unlock();
1407     }
1408   }
1409
1410   /**
1411    * return an iterator over visible segments between the given start and end
1412    * boundaries
1413    * 
1414    * @param start
1415    *          (first column - inclusive from 0)
1416    * @param end
1417    *          (last column - inclusive)
1418    * @param useVisibleCoords
1419    *          if true, start and end are visible column positions, not absolute
1420    *          positions
1421    */
1422   public Iterator<int[]> getVisibleBlocksIterator(int start, int end,
1423           boolean useVisibleCoords)
1424   {
1425     if (useVisibleCoords)
1426     {
1427       // TODO
1428       // we should really just convert start and end here with
1429       // adjustForHiddenColumns
1430       // and then create a VisibleContigsIterator
1431       // but without a cursor this will be horribly slow in some situations
1432       // ... so until then...
1433       return new VisibleBlocksVisBoundsIterator(start, end, true);
1434     }
1435     else
1436     {
1437       try
1438       {
1439         LOCK.readLock().lock();
1440         return new VisibleContigsIterator(start, end + 1, hiddenColumns);
1441       } finally
1442     {
1443         LOCK.readLock().unlock();
1444       }
1445     }
1446   }
1447
1448   /**
1449    * An iterator which iterates over visible regions in a range. The range is
1450    * specified in terms of visible column positions. Provides a special
1451    * "endsAtHidden" indicator to allow callers to determine if the final visible
1452    * column is adjacent to a hidden region.
1453    */
1454   public class VisibleBlocksVisBoundsIterator implements Iterator<int[]>
1455   {
1456     private List<int[]> vcontigs = new ArrayList<>();
1457
1458     private int currentPosition = 0;
1459
1460     private boolean endsAtHidden = false;
1461
1462     /**
1463      * Constructor for iterator over visible regions in a range.
1464      * 
1465      * @param start
1466      *          start position in terms of visible column position
1467      * @param end
1468      *          end position in terms of visible column position
1469      * @param usecopy
1470      *          whether to use a local copy of hidden columns
1471      */
1472     VisibleBlocksVisBoundsIterator(int start, int end, boolean usecopy)
1473     {
1474       /* actually this implementation always uses a local copy but this may change in future */
1475       try
1476       {
1477         if (usecopy)
1478         {
1479           LOCK.readLock().lock();
1480         }
1481
1482         if (hiddenColumns != null && hiddenColumns.size() > 0)
1483         {
1484           int blockStart = start;
1485           int blockEnd = end;
1486           int hiddenSoFar = 0;
1487           int visSoFar = 0;
1488
1489           // iterate until a region begins within (start,end]
1490           int i = 0;
1491           while ((i < hiddenColumns.size())
1492                   && (hiddenColumns.get(i)[0] <= blockStart + hiddenSoFar))
1493           {
1494             hiddenSoFar += hiddenColumns.get(i)[1] - hiddenColumns.get(i)[0]
1495                     + 1;
1496             i++;
1497           }
1498
1499           blockStart += hiddenSoFar; // convert start to absolute position
1500           blockEnd += hiddenSoFar; // convert end to absolute position
1501
1502           // iterate from start to end, adding each visible region. Positions
1503           // are
1504           // absolute, and all hidden regions which overlap [start,end] are
1505           // used.
1506           while (i < hiddenColumns.size()
1507                   && (hiddenColumns.get(i)[0] <= blockEnd))
1508           {
1509             int[] region = hiddenColumns.get(i);
1510
1511             // end position of this visible region is either just before the
1512             // start of the next hidden region, or the absolute position of
1513             // 'end', whichever is lowest
1514             blockEnd = Math.min(blockEnd, region[0] - 1);
1515
1516             vcontigs.add(new int[] { blockStart, blockEnd });
1517
1518             visSoFar += blockEnd - blockStart + 1;
1519
1520             // next visible region starts after this hidden region
1521             blockStart = region[1] + 1;
1522
1523             hiddenSoFar += region[1] - region[0] + 1;
1524
1525             // reset blockEnd to absolute position of 'end', assuming we've now
1526             // passed all hidden regions before end
1527             blockEnd = end + hiddenSoFar;
1528
1529             i++;
1530           }
1531           if (visSoFar < end - start + 1)
1532           {
1533             // the number of visible columns we've accounted for is less than
1534             // the number specified by end-start; work out the end position of
1535             // the last visible region
1536             blockEnd = blockStart + end - start - visSoFar;
1537             vcontigs.add(new int[] { blockStart, blockEnd });
1538
1539             // if the last visible region ends at the next hidden region, set
1540             // endsAtHidden=true
1541             if (i < hiddenColumns.size()
1542                     && hiddenColumns.get(i)[0] - 1 == blockEnd)
1543             {
1544               endsAtHidden = true;
1545             }
1546           }
1547         }
1548         else
1549         {
1550           // there are no hidden columns, return a single visible contig
1551           vcontigs.add(new int[] { start, end });
1552           endsAtHidden = false;
1553         }
1554       } finally
1555       {
1556         if (usecopy)
1557         {
1558           LOCK.readLock().unlock();
1559         }
1560       }
1561     }
1562
1563     @Override
1564     public boolean hasNext()
1565     {
1566       return (currentPosition < vcontigs.size());
1567     }
1568
1569     @Override
1570     public int[] next()
1571     {
1572       int[] result = vcontigs.get(currentPosition);
1573       currentPosition++;
1574       return result;
1575     }
1576
1577     public boolean endsAtHidden()
1578     {
1579       return endsAtHidden;
1580     }
1581   }
1582 }