4882597912dd1150886c7ecbbfc8ac085c6f3481
[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    * @param rng [start_0, end_0, start_1, end_1, ... ]
283    * @param baseOne - when true, ranges are base 1 and will be mapped to base 0
284    */
285   public void addRangeOfElements(int[] rng,boolean baseOne)
286   {
287     int base=baseOne ? -1 : 0;
288     for (int c = 0; c < rng.length; c += 2)
289     {
290       for (int p = rng[c]; p <= rng[c + 1]; p++)
291       {
292         selection.add(base+p);
293       }
294     }
295     
296   }
297   /**
298    * clears column selection
299    */
300   public void clear()
301   {
302     selection.clear();
303   }
304
305   /**
306    * Removes value 'col' from the selection (not the col'th item)
307    * 
308    * @param col
309    *          index of column to be removed
310    */
311   public void removeElement(int col)
312   {
313     selection.remove(col);
314   }
315
316   /**
317    * removes a range of columns from the selection
318    * 
319    * @param start
320    *          int - first column in range to be removed
321    * @param end
322    *          int - last col
323    */
324   public void removeElements(int start, int end)
325   {
326     Integer colInt;
327     for (int i = start; i < end; i++)
328     {
329       colInt = Integer.valueOf(i);
330       if (selection.contains(colInt))
331       {
332         selection.remove(colInt);
333       }
334     }
335   }
336
337   /**
338    * Returns a read-only view of the (possibly empty) list of selected columns
339    * <p>
340    * The list contains no duplicates but is not necessarily ordered. It also may
341    * include columns hidden from the current view. To modify (for example sort)
342    * the list, you should first make a copy.
343    * <p>
344    * The list is not thread-safe: iterating over it could result in
345    * ConcurrentModificationException if it is modified by another thread.
346    */
347   public List<Integer> getSelected()
348   {
349     return selection.getList();
350   }
351
352   /**
353    * @return list of int arrays containing start and end column position for
354    *         runs of selected columns ordered from right to left.
355    */
356   public List<int[]> getSelectedRanges()
357   {
358     return selection.getRanges();
359   }
360
361   /**
362    * 
363    * @param col
364    *          index to search for in column selection
365    * 
366    * @return true if col is selected
367    */
368   public boolean contains(int col)
369   {
370     return (col > -1) ? selection.isSelected(col) : false;
371   }
372
373   /**
374    * 
375    */
376   public boolean intersects(int from, int to)
377   {
378     // TODO: do this in a more efficient bitwise way
379     for (int f = from; f <= to; f++)
380     {
381       if (selection.isSelected(f))
382       {
383         return true;
384       }
385     }
386     return false;
387   }
388
389   /**
390    * Answers true if no columns are selected, else false
391    */
392   public boolean isEmpty()
393   {
394     return selection == null || selection.isEmpty();
395   }
396
397   /**
398    * rightmost selected column
399    * 
400    * @return rightmost column in alignment that is selected
401    */
402   public int getMax()
403   {
404     if (selection.isEmpty())
405     {
406       return -1;
407     }
408     return selection.getMaxColumn();
409   }
410
411   /**
412    * Leftmost column in selection
413    * 
414    * @return column index of leftmost column in selection
415    */
416   public int getMin()
417   {
418     if (selection.isEmpty())
419     {
420       return 1000000000;
421     }
422     return selection.getMinColumn();
423   }
424
425   public void hideSelectedColumns(AlignmentI al)
426   {
427     synchronized (selection)
428     {
429       for (int[] selregions : selection.getRanges())
430       {
431         al.getHiddenColumns().hideColumns(selregions[0], selregions[1]);
432       }
433       selection.clear();
434     }
435
436   }
437
438   /**
439    * Hides the specified column and any adjacent selected columns
440    * 
441    * @param res
442    *          int
443    */
444   public void hideSelectedColumns(int col, HiddenColumns hidden)
445   {
446     /*
447      * deselect column (whether selected or not!)
448      */
449     removeElement(col);
450
451     /*
452      * find adjacent selected columns
453      */
454     int min = col - 1, max = col + 1;
455     while (contains(min))
456     {
457       removeElement(min);
458       min--;
459     }
460
461     while (contains(max))
462     {
463       removeElement(max);
464       max++;
465     }
466
467     /*
468      * min, max are now the closest unselected columns
469      */
470     min++;
471     max--;
472     if (min > max)
473     {
474       min = max;
475     }
476
477     hidden.hideColumns(min, max);
478   }
479
480   /**
481    * Copy constructor
482    * 
483    * @param copy
484    */
485   public ColumnSelection(ColumnSelection copy)
486   {
487     if (copy != null)
488     {
489       selection = new IntList(copy.selection);
490     }
491   }
492
493   /**
494    * ColumnSelection
495    */
496   public ColumnSelection()
497   {
498   }
499
500   /**
501    * Invert the column selection from first to end-1. leaves hiddenColumns
502    * untouched (and unselected)
503    * 
504    * @param first
505    * @param end
506    */
507   public void invertColumnSelection(int first, int width, AlignmentI al)
508   {
509     boolean hasHidden = al.getHiddenColumns().hasHiddenColumns();
510     for (int i = first; i < width; i++)
511     {
512       if (contains(i))
513       {
514         removeElement(i);
515       }
516       else
517       {
518         if (!hasHidden || al.getHiddenColumns().isVisible(i))
519         {
520           addElement(i);
521         }
522       }
523     }
524   }
525
526   /**
527    * set the selected columns to the given column selection, excluding any
528    * columns that are hidden.
529    * 
530    * @param colsel
531    */
532   public void setElementsFrom(ColumnSelection colsel,
533           HiddenColumns hiddenColumns)
534   {
535     selection = new IntList();
536     if (colsel.selection != null && colsel.selection.size() > 0)
537     {
538       if (hiddenColumns.hasHiddenColumns())
539       {
540         // only select visible columns in this columns selection
541         for (Integer col : colsel.getSelected())
542         {
543           if (hiddenColumns != null
544                   && hiddenColumns.isVisible(col.intValue()))
545           {
546             selection.add(col);
547           }
548         }
549       }
550       else
551       {
552         // add everything regardless
553         for (Integer col : colsel.getSelected())
554         {
555           addElement(col);
556         }
557       }
558     }
559   }
560
561   /**
562    * 
563    * @return true if there are columns marked
564    */
565   public boolean hasSelectedColumns()
566   {
567     return (selection != null && selection.size() > 0);
568   }
569
570   /**
571    * Selects columns where the given annotation matches the provided filter
572    * condition(s). Any existing column selections are first cleared. Answers the
573    * number of columns added.
574    * 
575    * @param annotations
576    * @param filterParams
577    * @return
578    */
579   public int filterAnnotations(AlignmentAnnotation ann_row,
580           AnnotationFilterParameter filterParams)
581   {
582     Annotation[] annotations = ann_row.annotations;
583     // JBPNote - this method needs to be refactored to become independent of
584     // viewmodel package
585     this.clear();
586
587     if (ann_row.graph == AlignmentAnnotation.CONTACT_MAP && (filterParams
588             .getThresholdType() == AnnotationFilterParameter.ThresholdType.ABOVE_THRESHOLD
589             || filterParams
590                     .getThresholdType() == AnnotationFilterParameter.ThresholdType.BELOW_THRESHOLD))
591     {
592       float tVal = filterParams.getThresholdValue();
593       if (ann_row.sequenceRef != null)
594       {
595         // TODO - get ContactList from AlignmentView for non-seq-ref associatd
596         for (int column = 0; column < annotations.length; column++)
597         {
598           if (ann_row.annotations[column] == null)
599           {
600             continue;
601           }
602
603           int cpos = ann_row.sequenceRef.findPosition(column) - 1;
604           ContactListI clist = ann_row.sequenceRef
605                   .getContactListFor(ann_row, cpos);
606           for (int row = column + 8,
607                   rowEnd = clist.getContactHeight(); row < rowEnd; row++)
608           {
609             if (filterParams
610                     .getThresholdType() == AnnotationFilterParameter.ThresholdType.ABOVE_THRESHOLD
611                             ? (clist.getContactAt(row) > tVal)
612                             : (clist.getContactAt(row) < tVal))
613             {
614               addElement(column);
615               break;
616               // int column_forrowpos = ann_row.sequenceRef.findIndex(row + 1);
617               // addElement(column_forrowpos);
618             }
619           }
620         }
621       }
622       return selection.size();
623     }
624
625     int addedCount = 0;
626     int column = 0;
627     do
628     {
629       Annotation ann = annotations[column];
630       if (ann != null)
631       {
632         float value = ann.value;
633         boolean matched = false;
634
635         /*
636          * filter may have multiple conditions - 
637          * these are or'd until a match is found
638          */
639         if (filterParams
640                 .getThresholdType() == AnnotationFilterParameter.ThresholdType.ABOVE_THRESHOLD
641                 && value > filterParams.getThresholdValue())
642         {
643           matched = true;
644         }
645
646         if (!matched && filterParams
647                 .getThresholdType() == AnnotationFilterParameter.ThresholdType.BELOW_THRESHOLD
648                 && value < filterParams.getThresholdValue())
649         {
650           matched = true;
651         }
652
653         if (!matched && filterParams.isFilterAlphaHelix()
654                 && ann.secondaryStructure == 'H')
655         {
656           matched = true;
657         }
658
659         if (!matched && filterParams.isFilterBetaSheet()
660                 && ann.secondaryStructure == 'E')
661         {
662           matched = true;
663         }
664
665         if (!matched && filterParams.isFilterTurn()
666                 && ann.secondaryStructure == 'S')
667         {
668           matched = true;
669         }
670
671         String regexSearchString = filterParams.getRegexString();
672         if (!matched && regexSearchString != null)
673         {
674           List<SearchableAnnotationField> fields = filterParams
675                   .getRegexSearchFields();
676           for (SearchableAnnotationField field : fields)
677           {
678             String compareTo = field == SearchableAnnotationField.DISPLAY_STRING
679                     ? ann.displayCharacter // match 'Label'
680                     : ann.description; // and/or 'Description'
681             if (compareTo != null)
682             {
683               try
684               {
685                 if (compareTo.matches(regexSearchString))
686                 {
687                   matched = true;
688                 }
689               } catch (PatternSyntaxException pse)
690               {
691                 if (compareTo.equals(regexSearchString))
692                 {
693                   matched = true;
694                 }
695               }
696               if (matched)
697               {
698                 break;
699               }
700             }
701           }
702         }
703
704         if (matched)
705         {
706           this.addElement(column);
707           addedCount++;
708         }
709       }
710       column++;
711     } while (column < annotations.length);
712
713     return addedCount;
714   }
715
716   /**
717    * Returns a hashCode built from selected columns ranges
718    */
719   @Override
720   public int hashCode()
721   {
722     return selection.hashCode();
723   }
724
725   /**
726    * Answers true if comparing to a ColumnSelection with the same selected
727    * columns and hidden columns, else false
728    */
729   @Override
730   public boolean equals(Object obj)
731   {
732     if (!(obj instanceof ColumnSelection))
733     {
734       return false;
735     }
736     ColumnSelection that = (ColumnSelection) obj;
737
738     /*
739      * check columns selected are either both null, or match
740      */
741     if (this.selection == null)
742     {
743       if (that.selection != null)
744       {
745         return false;
746       }
747     }
748     if (!this.selection.equals(that.selection))
749     {
750       return false;
751     }
752
753     return true;
754   }
755
756   /**
757    * Updates the column selection depending on the parameters, and returns true
758    * if any change was made to the selection
759    * 
760    * @param markedColumns
761    *          a set identifying marked columns (base 0)
762    * @param startCol
763    *          the first column of the range to operate over (base 0)
764    * @param endCol
765    *          the last column of the range to operate over (base 0)
766    * @param invert
767    *          if true, deselect marked columns and select unmarked
768    * @param extendCurrent
769    *          if true, extend rather than replacing the current column selection
770    * @param toggle
771    *          if true, toggle the selection state of marked columns
772    * 
773    * @return
774    */
775   public boolean markColumns(BitSet markedColumns, int startCol, int endCol,
776           boolean invert, boolean extendCurrent, boolean toggle)
777   {
778     boolean changed = false;
779     if (!extendCurrent && !toggle)
780     {
781       changed = !this.isEmpty();
782       clear();
783     }
784     if (invert)
785     {
786       // invert only in the currently selected sequence region
787       int i = markedColumns.nextClearBit(startCol);
788       int ibs = markedColumns.nextSetBit(startCol);
789       while (i >= startCol && i <= endCol)
790       {
791         if (ibs < 0 || i < ibs)
792         {
793           changed = true;
794           if (toggle && contains(i))
795           {
796             removeElement(i++);
797           }
798           else
799           {
800             addElement(i++);
801           }
802         }
803         else
804         {
805           i = markedColumns.nextClearBit(ibs);
806           ibs = markedColumns.nextSetBit(i);
807         }
808       }
809     }
810     else
811     {
812       int i = markedColumns.nextSetBit(startCol);
813       while (i >= startCol && i <= endCol)
814       {
815         changed = true;
816         if (toggle && contains(i))
817         {
818           removeElement(i);
819         }
820         else
821         {
822           addElement(i);
823         }
824         i = markedColumns.nextSetBit(i + 1);
825       }
826     }
827     return changed;
828   }
829
830   /**
831    * Adjusts column selections, and the given selection group, to match the
832    * range of a stretch (e.g. mouse drag) operation
833    * <p>
834    * Method refactored from ScalePanel.mouseDragged
835    * 
836    * @param res
837    *          current column position, adjusted for hidden columns
838    * @param sg
839    *          current selection group
840    * @param min
841    *          start position of the stretch group
842    * @param max
843    *          end position of the stretch group
844    */
845   public void stretchGroup(int res, SequenceGroup sg, int min, int max)
846   {
847     if (!contains(res))
848     {
849       addElement(res);
850     }
851
852     if (res > sg.getStartRes())
853     {
854       // expand selection group to the right
855       sg.setEndRes(res);
856     }
857     if (res < sg.getStartRes())
858     {
859       // expand selection group to the left
860       sg.setStartRes(res);
861     }
862
863     /*
864      * expand or shrink column selection to match the
865      * range of the drag operation
866      */
867     for (int col = min; col <= max; col++)
868     {
869       if (col < sg.getStartRes() || col > sg.getEndRes())
870       {
871         // shrinking drag - remove from selection
872         removeElement(col);
873       }
874       else
875       {
876         // expanding drag - add to selection
877         addElement(col);
878       }
879     }
880   }
881 }