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