Merge branch 'releases/Release_2_11_3_Branch'
[jalview.git] / src / jalview / datamodel / ColumnSelection.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.Collections;
26 import java.util.List;
27 import java.util.regex.PatternSyntaxException;
28
29 import jalview.viewmodel.annotationfilter.AnnotationFilterParameter;
30 import jalview.viewmodel.annotationfilter.AnnotationFilterParameter.SearchableAnnotationField;
31
32 /**
33  * Data class holding the selected columns and hidden column ranges for a view.
34  * Ranges are base 1.
35  */
36 public class ColumnSelection
37 {
38   /**
39    * A class to hold an efficient representation of selected columns
40    */
41   private class IntList
42   {
43     /*
44      * list of selected columns (ordered by selection order, not column order)
45      */
46     private List<Integer> order;
47
48     /*
49      * an unmodifiable view of the selected columns list
50      */
51     private List<Integer> _uorder;
52
53     /**
54      * bitfield for column selection - allows quick lookup
55      */
56     private BitSet selected;
57
58     /**
59      * Constructor
60      */
61     IntList()
62     {
63       order = new ArrayList<>();
64       _uorder = Collections.unmodifiableList(order);
65       selected = new BitSet();
66     }
67
68     /**
69      * Copy constructor
70      * 
71      * @param other
72      */
73     IntList(IntList other)
74     {
75       this();
76       if (other != null)
77       {
78         int j = other.size();
79         for (int i = 0; i < j; i++)
80         {
81           add(other.elementAt(i));
82         }
83       }
84     }
85
86     /**
87      * adds a new column i to the selection - only if i is not already selected
88      * 
89      * @param i
90      */
91     void add(int i)
92     {
93       if (!selected.get(i))
94       {
95         order.add(Integer.valueOf(i));
96         selected.set(i);
97       }
98     }
99
100     void clear()
101     {
102       order.clear();
103       selected.clear();
104     }
105
106     void remove(int col)
107     {
108
109       Integer colInt = Integer.valueOf(col);
110
111       if (selected.get(col))
112       {
113         // if this ever changes to List.remove(), ensure Integer not int
114         // argument
115         // as List.remove(int i) removes the i'th item which is wrong
116         order.remove(colInt);
117         selected.clear(col);
118       }
119     }
120
121     boolean contains(Integer colInt)
122     {
123       return selected.get(colInt);
124     }
125
126     boolean isEmpty()
127     {
128       return order.isEmpty();
129     }
130
131     /**
132      * Returns a read-only view of the selected columns list
133      * 
134      * @return
135      */
136     List<Integer> getList()
137     {
138       return _uorder;
139     }
140
141     int size()
142     {
143       return order.size();
144     }
145
146     /**
147      * gets the column that was selected first, second or i'th
148      * 
149      * @param i
150      * @return
151      */
152     int elementAt(int i)
153     {
154       return order.get(i);
155     }
156
157     protected boolean pruneColumnList(final List<int[]> shifts)
158     {
159       int s = 0, t = shifts.size();
160       int[] sr = shifts.get(s++);
161       boolean pruned = false;
162       int i = 0, j = order.size();
163       while (i < j && s <= t)
164       {
165         int c = order.get(i++).intValue();
166         if (sr[0] <= c)
167         {
168           if (sr[1] + sr[0] >= c)
169           { // sr[1] -ve means inseriton.
170             order.remove(--i);
171             selected.clear(c);
172             j--;
173           }
174           else
175           {
176             if (s < t)
177             {
178               sr = shifts.get(s);
179             }
180             s++;
181           }
182         }
183       }
184       return pruned;
185     }
186
187     /**
188      * shift every selected column at or above start by change
189      * 
190      * @param start
191      *          - leftmost column to be shifted
192      * @param change
193      *          - delta for shift
194      */
195     void compensateForEdits(int start, int change)
196     {
197       BitSet mask = new BitSet();
198       for (int i = 0; i < order.size(); i++)
199       {
200         int temp = order.get(i);
201
202         if (temp >= start)
203         {
204           // clear shifted bits and update List of selected columns
205           selected.clear(temp);
206           mask.set(temp - change);
207           order.set(i, Integer.valueOf(temp - change));
208         }
209       }
210       // lastly update the bitfield all at once
211       selected.or(mask);
212     }
213
214     boolean isSelected(int column)
215     {
216       return selected.get(column);
217     }
218
219     int getMaxColumn()
220     {
221       return selected.length() - 1;
222     }
223
224     int getMinColumn()
225     {
226       return selected.get(0) ? 0 : selected.nextSetBit(0);
227     }
228
229     /**
230      * @return a series of selection intervals along the range
231      */
232     List<int[]> getRanges()
233     {
234       List<int[]> rlist = new ArrayList<>();
235       if (selected.isEmpty())
236       {
237         return rlist;
238       }
239       int next = selected.nextSetBit(0), clear = -1;
240       while (next != -1)
241       {
242         clear = selected.nextClearBit(next);
243         rlist.add(new int[] { next, clear - 1 });
244         next = selected.nextSetBit(clear);
245       }
246       return rlist;
247     }
248
249     @Override
250     public int hashCode()
251     {
252       // TODO Auto-generated method stub
253       return selected.hashCode();
254     }
255
256     @Override
257     public boolean equals(Object obj)
258     {
259       if (obj instanceof IntList)
260       {
261         return ((IntList) obj).selected.equals(selected);
262       }
263       return false;
264     }
265   }
266
267   private IntList selection = new IntList();
268
269   /**
270    * Add a column to the selection
271    * 
272    * @param col
273    *          index of column
274    */
275   public void addElement(int col)
276   {
277     selection.add(col);
278   }
279
280   /**
281    * add a series of start,end (inclusive) ranges to the column selection
282    * 
283    * @param rng
284    *          [start_0, end_0, start_1, end_1, ... ]
285    * @param baseOne
286    *          - when true, ranges are base 1 and will be mapped to base 0
287    */
288   public void addRangeOfElements(int[] rng, boolean baseOne)
289   {
290     int base = baseOne ? -1 : 0;
291     for (int c = 0; c < rng.length; c += 2)
292     {
293       for (int p = rng[c]; p <= rng[c + 1]; p++)
294       {
295         selection.add(base + p);
296       }
297     }
298
299   }
300
301   /**
302    * clears column selection
303    */
304   public void clear()
305   {
306     selection.clear();
307   }
308
309   /**
310    * Removes value 'col' from the selection (not the col'th item)
311    * 
312    * @param col
313    *          index of column to be removed
314    */
315   public void removeElement(int col)
316   {
317     selection.remove(col);
318   }
319
320   /**
321    * removes a range of columns from the selection
322    * 
323    * @param start
324    *          int - first column in range to be removed
325    * @param end
326    *          int - last col
327    */
328   public void removeElements(int start, int end)
329   {
330     Integer colInt;
331     for (int i = start; i < end; i++)
332     {
333       colInt = Integer.valueOf(i);
334       if (selection.contains(colInt))
335       {
336         selection.remove(colInt);
337       }
338     }
339   }
340
341   /**
342    * Returns a read-only view of the (possibly empty) list of selected columns
343    * (base 1)
344    * <p>
345    * The list contains no duplicates but is not necessarily ordered. Columns are
346    * reported in alignment coordinates (base 1), so may also include columns
347    * hidden from the current view. To modify (for example sort) the list, you
348    * should first make a copy.
349    * <p>
350    * The list is not thread-safe: iterating over it could result in
351    * ConcurrentModificationException if it is modified by another thread.
352    */
353   public List<Integer> getSelected()
354   {
355     return selection.getList();
356   }
357
358   /**
359    * @return list of int arrays containing start and end column position for
360    *         runs of selected columns ordered from right to left.
361    */
362   public List<int[]> getSelectedRanges()
363   {
364     return selection.getRanges();
365   }
366
367   /**
368    * 
369    * @param col
370    *          index to search for in column selection
371    * 
372    * @return true if col is selected
373    */
374   public boolean contains(int col)
375   {
376     return (col > -1) ? selection.isSelected(col) : false;
377   }
378
379   /**
380    * 
381    */
382   public boolean intersects(int from, int to)
383   {
384     // TODO: do this in a more efficient bitwise way
385     for (int f = from; f <= to; f++)
386     {
387       if (selection.isSelected(f))
388       {
389         return true;
390       }
391     }
392     return false;
393   }
394
395   /**
396    * Answers true if no columns are selected, else false
397    */
398   public boolean isEmpty()
399   {
400     return selection == null || selection.isEmpty();
401   }
402
403   /**
404    * rightmost selected column
405    * 
406    * @return rightmost column in alignment that is selected
407    */
408   public int getMax()
409   {
410     if (selection.isEmpty())
411     {
412       return -1;
413     }
414     return selection.getMaxColumn();
415   }
416
417   /**
418    * Leftmost column in selection
419    * 
420    * @return column index of leftmost column in selection
421    */
422   public int getMin()
423   {
424     if (selection.isEmpty())
425     {
426       return 1000000000;
427     }
428     return selection.getMinColumn();
429   }
430
431   public void hideSelectedColumns(AlignmentI al)
432   {
433     synchronized (selection)
434     {
435       for (int[] selregions : selection.getRanges())
436       {
437         al.getHiddenColumns().hideColumns(selregions[0], selregions[1]);
438       }
439       selection.clear();
440     }
441
442   }
443
444   /**
445    * Hides the specified column and any adjacent selected columns
446    * 
447    * @param res
448    *          int
449    */
450   public void hideSelectedColumns(int col, HiddenColumns hidden)
451   {
452     /*
453      * deselect column (whether selected or not!)
454      */
455     removeElement(col);
456
457     /*
458      * find adjacent selected columns
459      */
460     int min = col - 1, max = col + 1;
461     while (contains(min))
462     {
463       removeElement(min);
464       min--;
465     }
466
467     while (contains(max))
468     {
469       removeElement(max);
470       max++;
471     }
472
473     /*
474      * min, max are now the closest unselected columns
475      */
476     min++;
477     max--;
478     if (min > max)
479     {
480       min = max;
481     }
482
483     hidden.hideColumns(min, max);
484   }
485
486   /**
487    * Copy constructor
488    * 
489    * @param copy
490    */
491   public ColumnSelection(ColumnSelection copy)
492   {
493     if (copy != null)
494     {
495       selection = new IntList(copy.selection);
496     }
497   }
498
499   /**
500    * ColumnSelection
501    */
502   public ColumnSelection()
503   {
504   }
505
506   /**
507    * Invert the column selection from first to end-1. leaves hiddenColumns
508    * untouched (and unselected)
509    * 
510    * @param first
511    * @param end
512    */
513   public void invertColumnSelection(int first, int width, AlignmentI al)
514   {
515     boolean hasHidden = al.getHiddenColumns().hasHiddenColumns();
516     for (int i = first; i < width; i++)
517     {
518       if (contains(i))
519       {
520         removeElement(i);
521       }
522       else
523       {
524         if (!hasHidden || al.getHiddenColumns().isVisible(i))
525         {
526           addElement(i);
527         }
528       }
529     }
530   }
531
532   /**
533    * set the selected columns to the given column selection, excluding any
534    * columns that are hidden.
535    * 
536    * @param colsel
537    */
538   public void setElementsFrom(ColumnSelection colsel,
539           HiddenColumns hiddenColumns)
540   {
541     selection = new IntList();
542     if (colsel.selection != null && colsel.selection.size() > 0)
543     {
544       if (hiddenColumns.hasHiddenColumns())
545       {
546         // only select visible columns in this columns selection
547         for (Integer col : colsel.getSelected())
548         {
549           if (hiddenColumns != null
550                   && hiddenColumns.isVisible(col.intValue()))
551           {
552             selection.add(col);
553           }
554         }
555       }
556       else
557       {
558         // add everything regardless
559         for (Integer col : colsel.getSelected())
560         {
561           addElement(col);
562         }
563       }
564     }
565   }
566
567   /**
568    * 
569    * @return true if there are columns marked
570    */
571   public boolean hasSelectedColumns()
572   {
573     return (selection != null && selection.size() > 0);
574   }
575
576   /**
577    * Selects columns where the given annotation matches the provided filter
578    * condition(s). Any existing column selections are first cleared. Answers the
579    * number of columns added.
580    * 
581    * @param annotations
582    * @param filterParams
583    * @return
584    */
585   public int filterAnnotations(AlignmentAnnotation ann_row,
586           AnnotationFilterParameter filterParams)
587   {
588     Annotation[] annotations = ann_row.annotations;
589     // JBPNote - this method needs to be refactored to become independent of
590     // viewmodel package
591     this.clear();
592
593     if (ann_row.graph == AlignmentAnnotation.CONTACT_MAP && (filterParams
594             .getThresholdType() == AnnotationFilterParameter.ThresholdType.ABOVE_THRESHOLD
595             || filterParams
596                     .getThresholdType() == AnnotationFilterParameter.ThresholdType.BELOW_THRESHOLD))
597     {
598       float tVal = filterParams.getThresholdValue();
599       if (ann_row.sequenceRef != null)
600       {
601         // TODO - get ContactList from AlignmentView for non-seq-ref associatd
602         for (int column = 0; column < annotations.length; column++)
603         {
604           if (ann_row.annotations[column] == null)
605           {
606             continue;
607           }
608
609           int cpos = ann_row.sequenceRef.findPosition(column) - 1;
610           ContactListI clist = ann_row.sequenceRef
611                   .getContactListFor(ann_row, cpos);
612           for (int row = column + 8, rowEnd = clist
613                   .getContactHeight(); row < rowEnd; row++)
614           {
615             if (filterParams
616                     .getThresholdType() == AnnotationFilterParameter.ThresholdType.ABOVE_THRESHOLD
617                             ? (clist.getContactAt(row) > tVal)
618                             : (clist.getContactAt(row) < tVal))
619             {
620               addElement(column);
621               break;
622               // int column_forrowpos = ann_row.sequenceRef.findIndex(row + 1);
623               // addElement(column_forrowpos);
624             }
625           }
626         }
627       }
628       return selection.size();
629     }
630
631     int addedCount = 0;
632     int column = 0;
633     do
634     {
635       Annotation ann = annotations[column];
636       if (ann != null)
637       {
638         float value = ann.value;
639         boolean matched = false;
640
641         /*
642          * filter may have multiple conditions - 
643          * these are or'd until a match is found
644          */
645         if (filterParams
646                 .getThresholdType() == AnnotationFilterParameter.ThresholdType.ABOVE_THRESHOLD
647                 && value > filterParams.getThresholdValue())
648         {
649           matched = true;
650         }
651
652         if (!matched && filterParams
653                 .getThresholdType() == AnnotationFilterParameter.ThresholdType.BELOW_THRESHOLD
654                 && value < filterParams.getThresholdValue())
655         {
656           matched = true;
657         }
658
659         if (!matched && filterParams.isFilterAlphaHelix()
660                 && ann.secondaryStructure == 'H')
661         {
662           matched = true;
663         }
664
665         if (!matched && filterParams.isFilterBetaSheet()
666                 && ann.secondaryStructure == 'E')
667         {
668           matched = true;
669         }
670
671         if (!matched && filterParams.isFilterTurn()
672                 && ann.secondaryStructure == 'S')
673         {
674           matched = true;
675         }
676
677         String regexSearchString = filterParams.getRegexString();
678         if (!matched && regexSearchString != null)
679         {
680           List<SearchableAnnotationField> fields = filterParams
681                   .getRegexSearchFields();
682           for (SearchableAnnotationField field : fields)
683           {
684             String compareTo = field == SearchableAnnotationField.DISPLAY_STRING
685                     ? ann.displayCharacter // match 'Label'
686                     : ann.description; // and/or 'Description'
687             if (compareTo != null)
688             {
689               try
690               {
691                 if (compareTo.matches(regexSearchString))
692                 {
693                   matched = true;
694                 }
695               } catch (PatternSyntaxException pse)
696               {
697                 if (compareTo.equals(regexSearchString))
698                 {
699                   matched = true;
700                 }
701               }
702               if (matched)
703               {
704                 break;
705               }
706             }
707           }
708         }
709
710         if (matched)
711         {
712           this.addElement(column);
713           addedCount++;
714         }
715       }
716       column++;
717     } while (column < annotations.length);
718
719     return addedCount;
720   }
721
722   /**
723    * Returns a hashCode built from selected columns ranges
724    */
725   @Override
726   public int hashCode()
727   {
728     return selection.hashCode();
729   }
730
731   /**
732    * Answers true if comparing to a ColumnSelection with the same selected
733    * columns and hidden columns, else false
734    */
735   @Override
736   public boolean equals(Object obj)
737   {
738     if (!(obj instanceof ColumnSelection))
739     {
740       return false;
741     }
742     ColumnSelection that = (ColumnSelection) obj;
743
744     /*
745      * check columns selected are either both null, or match
746      */
747     if (this.selection == null)
748     {
749       if (that.selection != null)
750       {
751         return false;
752       }
753     }
754     if (!this.selection.equals(that.selection))
755     {
756       return false;
757     }
758
759     return true;
760   }
761
762   /**
763    * Updates the column selection depending on the parameters, and returns true
764    * if any change was made to the selection
765    * 
766    * @param markedColumns
767    *          a set identifying marked columns (base 0)
768    * @param startCol
769    *          the first column of the range to operate over (base 0)
770    * @param endCol
771    *          the last column of the range to operate over (base 0)
772    * @param invert
773    *          if true, deselect marked columns and select unmarked
774    * @param extendCurrent
775    *          if true, extend rather than replacing the current column selection
776    * @param toggle
777    *          if true, toggle the selection state of marked columns
778    * 
779    * @return
780    */
781   public boolean markColumns(BitSet markedColumns, int startCol, int endCol,
782           boolean invert, boolean extendCurrent, boolean toggle)
783   {
784     boolean changed = false;
785     if (!extendCurrent && !toggle)
786     {
787       changed = !this.isEmpty();
788       clear();
789     }
790     if (invert)
791     {
792       // invert only in the currently selected sequence region
793       int i = markedColumns.nextClearBit(startCol);
794       int ibs = markedColumns.nextSetBit(startCol);
795       while (i >= startCol && i <= endCol)
796       {
797         if (ibs < 0 || i < ibs)
798         {
799           changed = true;
800           if (toggle && contains(i))
801           {
802             removeElement(i++);
803           }
804           else
805           {
806             addElement(i++);
807           }
808         }
809         else
810         {
811           i = markedColumns.nextClearBit(ibs);
812           ibs = markedColumns.nextSetBit(i);
813         }
814       }
815     }
816     else
817     {
818       int i = markedColumns.nextSetBit(startCol);
819       while (i >= startCol && i <= endCol)
820       {
821         changed = true;
822         if (toggle && contains(i))
823         {
824           removeElement(i);
825         }
826         else
827         {
828           addElement(i);
829         }
830         i = markedColumns.nextSetBit(i + 1);
831       }
832     }
833     return changed;
834   }
835
836   /**
837    * Adjusts column selections, and the given selection group, to match the
838    * range of a stretch (e.g. mouse drag) operation
839    * <p>
840    * Method refactored from ScalePanel.mouseDragged
841    * 
842    * @param res
843    *          current column position, adjusted for hidden columns
844    * @param sg
845    *          current selection group
846    * @param min
847    *          start position of the stretch group
848    * @param max
849    *          end position of the stretch group
850    */
851   public void stretchGroup(int res, SequenceGroup sg, int min, int max)
852   {
853     if (!contains(res))
854     {
855       addElement(res);
856     }
857
858     if (res > sg.getStartRes())
859     {
860       // expand selection group to the right
861       sg.setEndRes(res);
862     }
863     if (res < sg.getStartRes())
864     {
865       // expand selection group to the left
866       sg.setStartRes(res);
867     }
868
869     /*
870      * expand or shrink column selection to match the
871      * range of the drag operation
872      */
873     for (int col = min; col <= max; col++)
874     {
875       if (col < sg.getStartRes() || col > sg.getEndRes())
876       {
877         // shrinking drag - remove from selection
878         removeElement(col);
879       }
880       else
881       {
882         // expanding drag - add to selection
883         addElement(col);
884       }
885     }
886   }
887 }