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