Merge branch 'bug/JAL-2784' into develop
[jalview.git] / src / jalview / viewmodel / ViewportRanges.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.viewmodel;
22
23 import jalview.datamodel.AlignmentI;
24 import jalview.datamodel.HiddenColumns;
25
26 /**
27  * Supplies and updates viewport properties relating to position such as: start
28  * and end residues and sequences; ideally will serve hidden columns/rows too.
29  * Intention also to support calculations for positioning, scrolling etc. such
30  * as finding the middle of the viewport, checking for scrolls off screen
31  */
32 public class ViewportRanges extends ViewportProperties
33 {
34   public static final String STARTRES = "startres";
35
36   public static final String ENDRES = "endres";
37
38   public static final String STARTSEQ = "startseq";
39
40   public static final String ENDSEQ = "endseq";
41
42   public static final String STARTRESANDSEQ = "startresandseq";
43
44   public static final String MOVE_VIEWPORT = "move_viewport";
45
46   private boolean wrappedMode = false;
47
48   // start residue of viewport
49   private int startRes;
50
51   // end residue of viewport
52   private int endRes;
53
54   // start sequence of viewport
55   private int startSeq;
56
57   // end sequence of viewport
58   private int endSeq;
59
60   // alignment
61   private AlignmentI al;
62
63   /**
64    * Constructor
65    * 
66    * @param alignment
67    *          the viewport's alignment
68    */
69   public ViewportRanges(AlignmentI alignment)
70   {
71     // initial values of viewport settings
72     this.startRes = 0;
73     this.endRes = alignment.getWidth() - 1;
74     this.startSeq = 0;
75     this.endSeq = alignment.getHeight() - 1;
76     this.al = alignment;
77   }
78
79   /**
80    * Get alignment width in cols, including hidden cols
81    */
82   public int getAbsoluteAlignmentWidth()
83   {
84     return al.getWidth();
85   }
86
87   /**
88    * Get alignment height in rows, including hidden rows
89    */
90   public int getAbsoluteAlignmentHeight()
91   {
92     return al.getHeight() + al.getHiddenSequences().getSize();
93   }
94
95   /**
96    * Get alignment width in cols, excluding hidden cols
97    */
98   public int getVisibleAlignmentWidth()
99   {
100     return al.getWidth() - al.getHiddenColumns().getSize();
101   }
102
103   /**
104    * Get alignment height in rows, excluding hidden rows
105    */
106   public int getVisibleAlignmentHeight()
107   {
108     return al.getHeight();
109   }
110
111   /**
112    * Set first residue visible in the viewport, and retain the current width.
113    * Fires a property change event.
114    * 
115    * @param res
116    *          residue position
117    */
118   public void setStartRes(int res)
119   {
120     int width = getViewportWidth();
121     setStartEndRes(res, res + width - 1);
122   }
123
124   /**
125    * Set start and end residues at the same time. This method only fires one
126    * event for the two changes, and should be used in preference to separate
127    * calls to setStartRes and setEndRes.
128    * 
129    * @param start
130    *          the start residue
131    * @param end
132    *          the end residue
133    */
134   public void setStartEndRes(int start, int end)
135   {
136     int[] oldvalues = updateStartEndRes(start, end);
137     int oldstartres = oldvalues[0];
138     int oldendres = oldvalues[1];
139
140     changeSupport.firePropertyChange(STARTRES, oldstartres, startRes);
141     if (oldstartres == startRes)
142     {
143       // event won't be fired if start positions are same
144       // fire an event for the end positions in case they changed
145       changeSupport.firePropertyChange(ENDRES, oldendres, endRes);
146     }
147   }
148
149   /**
150    * Update start and end residue values, adjusting for width constraints if
151    * necessary
152    * 
153    * @param start
154    *          start residue
155    * @param end
156    *          end residue
157    * @return array containing old start and end residue values
158    */
159   private int[] updateStartEndRes(int start, int end)
160   {
161     int oldstartres = this.startRes;
162
163     /*
164      * if not wrapped, don't leave white space at the right margin
165      */
166     int lastColumn = getVisibleAlignmentWidth() - 1;
167     if (!wrappedMode && (start > lastColumn))
168     {
169       startRes = Math.max(lastColumn, 0);
170     }
171     else if (start < 0)
172     {
173       startRes = 0;
174     }
175     else
176     {
177       startRes = start;
178     }
179
180     int oldendres = this.endRes;
181     if (end < 0)
182     {
183       endRes = 0;
184     }
185     else if (!wrappedMode && (end > lastColumn))
186     {
187       endRes = Math.max(lastColumn, 0);
188     }
189     else
190     {
191       endRes = end;
192     }
193     return new int[] { oldstartres, oldendres };
194   }
195
196   /**
197    * Set the first sequence visible in the viewport, maintaining the height. If
198    * the viewport would extend past the last sequence, sets the viewport so it
199    * sits at the bottom of the alignment. Fires a property change event.
200    * 
201    * @param seq
202    *          sequence position
203    */
204   public void setStartSeq(int seq)
205   {
206     int startseq = seq;
207     int height = getViewportHeight();
208     if (startseq + height - 1 > getVisibleAlignmentHeight() - 1)
209     {
210       startseq = getVisibleAlignmentHeight() - height;
211     }
212     setStartEndSeq(startseq, startseq + height - 1);
213   }
214
215   /**
216    * Set start and end sequences at the same time. The viewport height may
217    * change. This method only fires one event for the two changes, and should be
218    * used in preference to separate calls to setStartSeq and setEndSeq.
219    * 
220    * @param start
221    *          the start sequence
222    * @param end
223    *          the end sequence
224    */
225   public void setStartEndSeq(int start, int end)
226   {
227     int[] oldvalues = updateStartEndSeq(start, end);
228     int oldstartseq = oldvalues[0];
229     int oldendseq = oldvalues[1];
230
231     changeSupport.firePropertyChange(STARTSEQ, oldstartseq, startSeq);
232     if (oldstartseq == startSeq)
233     {
234       // event won't be fired if start positions are the same
235       // fire in case the end positions changed
236       changeSupport.firePropertyChange(ENDSEQ, oldendseq, endSeq);
237     }
238   }
239
240   /**
241    * Update start and end sequence values, adjusting for height constraints if
242    * necessary
243    * 
244    * @param start
245    *          start sequence
246    * @param end
247    *          end sequence
248    * @return array containing old start and end sequence values
249    */
250   private int[] updateStartEndSeq(int start, int end)
251   {
252     int oldstartseq = this.startSeq;
253     int visibleHeight = getVisibleAlignmentHeight();
254     if (start > visibleHeight - 1)
255     {
256       startSeq = Math.max(visibleHeight - 1, 0);
257     }
258     else if (start < 0)
259     {
260       startSeq = 0;
261     }
262     else
263     {
264       startSeq = start;
265     }
266
267     int oldendseq = this.endSeq;
268     if (end >= visibleHeight)
269     {
270       endSeq = Math.max(visibleHeight - 1, 0);
271     }
272     else if (end < 0)
273     {
274       endSeq = 0;
275     }
276     else
277     {
278       endSeq = end;
279     }
280     return new int[] { oldstartseq, oldendseq };
281   }
282
283   /**
284    * Set the last sequence visible in the viewport. Fires a property change
285    * event.
286    * 
287    * @param seq
288    *          sequence position
289    */
290   public void setEndSeq(int seq)
291   {
292     int height = getViewportHeight();
293     setStartEndSeq(seq - height + 1, seq);
294   }
295
296   /**
297    * Set start residue and start sequence together (fires single event). The
298    * event supplies a pair of old values and a pair of new values: [old start
299    * residue, old start sequence] and [new start residue, new start sequence]
300    * 
301    * @param res
302    *          the start residue
303    * @param seq
304    *          the start sequence
305    */
306   public void setStartResAndSeq(int res, int seq)
307   {
308     int width = getViewportWidth();
309     int[] oldresvalues = updateStartEndRes(res, res + width - 1);
310
311     int startseq = seq;
312     int height = getViewportHeight();
313     if (startseq + height - 1 > getVisibleAlignmentHeight() - 1)
314     {
315       startseq = getVisibleAlignmentHeight() - height;
316     }
317     int[] oldseqvalues = updateStartEndSeq(startseq, startseq + height - 1);
318
319     int[] old = new int[] { oldresvalues[0], oldseqvalues[0] };
320     int[] newresseq = new int[] { startRes, startSeq };
321     changeSupport.firePropertyChange(STARTRESANDSEQ, old, newresseq);
322   }
323
324   /**
325    * Get start residue of viewport
326    */
327   public int getStartRes()
328   {
329     return startRes;
330   }
331
332   /**
333    * Get end residue of viewport
334    */
335   public int getEndRes()
336   {
337     return endRes;
338   }
339
340   /**
341    * Get start sequence of viewport
342    */
343   public int getStartSeq()
344   {
345     return startSeq;
346   }
347
348   /**
349    * Get end sequence of viewport
350    */
351   public int getEndSeq()
352   {
353     return endSeq;
354   }
355
356   /**
357    * Set viewport width in residues, without changing startRes. Use in
358    * preference to calculating endRes from the width, to avoid out by one
359    * errors! Fires a property change event.
360    * 
361    * @param w
362    *          width in residues
363    */
364   public void setViewportWidth(int w)
365   {
366     setStartEndRes(startRes, startRes + w - 1);
367   }
368
369   /**
370    * Set viewport height in residues, without changing startSeq. Use in
371    * preference to calculating endSeq from the height, to avoid out by one
372    * errors! Fires a property change event.
373    * 
374    * @param h
375    *          height in sequences
376    */
377   public void setViewportHeight(int h)
378   {
379     setStartEndSeq(startSeq, startSeq + h - 1);
380   }
381
382   /**
383    * Set viewport horizontal start position and width. Use in preference to
384    * calculating endRes from the width, to avoid out by one errors! Fires a
385    * property change event.
386    * 
387    * @param start
388    *          start residue
389    * @param w
390    *          width in residues
391    */
392   public void setViewportStartAndWidth(int start, int w)
393   {
394     int vpstart = start;
395     if (vpstart < 0)
396     {
397       vpstart = 0;
398     }
399
400     /*
401      * if not wrapped, don't leave white space at the right margin
402      */
403     if (!wrappedMode)
404     {
405       if ((w <= getVisibleAlignmentWidth())
406               && (vpstart + w - 1 > getVisibleAlignmentWidth() - 1))
407       {
408         vpstart = getVisibleAlignmentWidth() - w;
409       }
410
411     }
412     setStartEndRes(vpstart, vpstart + w - 1);
413   }
414
415   /**
416    * Set viewport vertical start position and height. Use in preference to
417    * calculating endSeq from the height, to avoid out by one errors! Fires a
418    * property change event.
419    * 
420    * @param start
421    *          start sequence
422    * @param h
423    *          height in sequences
424    */
425   public void setViewportStartAndHeight(int start, int h)
426   {
427     int vpstart = start;
428     if (vpstart < 0)
429     {
430       vpstart = 0;
431     }
432     else if ((h <= getVisibleAlignmentHeight())
433             && (vpstart + h - 1 > getVisibleAlignmentHeight() - 1))
434     // viewport height is less than the full alignment and we are running off
435     // the bottom
436     {
437       vpstart = getVisibleAlignmentHeight() - h;
438     }
439     setStartEndSeq(vpstart, vpstart + h - 1);
440   }
441
442   /**
443    * Get width of viewport in residues
444    * 
445    * @return width of viewport
446    */
447   public int getViewportWidth()
448   {
449     return (endRes - startRes + 1);
450   }
451
452   /**
453    * Get height of viewport in residues
454    * 
455    * @return height of viewport
456    */
457   public int getViewportHeight()
458   {
459     return (endSeq - startSeq + 1);
460   }
461
462   /**
463    * Scroll the viewport range vertically. Fires a property change event.
464    * 
465    * @param up
466    *          true if scrolling up, false if down
467    * 
468    * @return true if the scroll is valid
469    */
470   public boolean scrollUp(boolean up)
471   {
472     /*
473      * if in unwrapped mode, scroll up or down one sequence row;
474      * if in wrapped mode, scroll by one visible width of columns
475      */
476     if (up)
477     {
478       if (wrappedMode)
479       {
480         pageUp();
481       }
482       else
483       {
484         if (startSeq < 1)
485         {
486           return false;
487         }
488         setStartSeq(startSeq - 1);
489       }
490     }
491     else
492     {
493       if (wrappedMode)
494       {
495         pageDown();
496       }
497       else
498       {
499         if (endSeq >= getVisibleAlignmentHeight() - 1)
500         {
501           return false;
502         }
503         setStartSeq(startSeq + 1);
504       }
505     }
506     return true;
507   }
508
509   /**
510    * Scroll the viewport range horizontally. Fires a property change event.
511    * 
512    * @param right
513    *          true if scrolling right, false if left
514    * 
515    * @return true if the scroll is valid
516    */
517   public boolean scrollRight(boolean right)
518   {
519     if (!right)
520     {
521       if (startRes < 1)
522       {
523         return false;
524       }
525
526       setStartRes(startRes - 1);
527     }
528     else
529     {
530       if (endRes >= getVisibleAlignmentWidth() - 1)
531       {
532         return false;
533       }
534
535       setStartRes(startRes + 1);
536     }
537
538     return true;
539   }
540
541   /**
542    * Scroll a wrapped alignment so that the specified residue is in the first
543    * repeat of the wrapped view. Fires a property change event. Answers true if
544    * the startRes changed, else false.
545    * 
546    * @param res
547    *          residue position to scroll to NB visible position not absolute
548    *          alignment position
549    * @return
550    */
551   public boolean scrollToWrappedVisible(int res)
552   {
553     int newStartRes = calcWrappedStartResidue(res);
554     if (newStartRes == startRes)
555     {
556       return false;
557     }
558     setStartRes(newStartRes);
559
560     return true;
561   }
562
563   /**
564    * Calculate wrapped start residue from visible start residue
565    * 
566    * @param res
567    *          visible start residue
568    * @return left column of panel res will be located in
569    */
570   private int calcWrappedStartResidue(int res)
571   {
572     int oldStartRes = startRes;
573     int width = getViewportWidth();
574
575     boolean up = res < oldStartRes;
576     int widthsToScroll = Math.abs((res - oldStartRes) / width);
577     if (up)
578     {
579       widthsToScroll++;
580     }
581
582     int residuesToScroll = width * widthsToScroll;
583     int newStartRes = up ? oldStartRes - residuesToScroll : oldStartRes
584             + residuesToScroll;
585     if (newStartRes < 0)
586     {
587       newStartRes = 0;
588     }
589     return newStartRes;
590   }
591
592   /**
593    * Scroll so that (x,y) is visible. Fires a property change event.
594    * 
595    * @param x
596    *          x position in alignment (absolute position)
597    * @param y
598    *          y position in alignment (absolute position)
599    */
600   public void scrollToVisible(int x, int y)
601   {
602     while (y < startSeq)
603     {
604       scrollUp(true);
605     }
606     while (y > endSeq)
607     {
608       scrollUp(false);
609     }
610     
611     HiddenColumns hidden = al.getHiddenColumns();
612     while (x < hidden.adjustForHiddenColumns(startRes))
613     {
614       if (!scrollRight(false))
615       {
616         break;
617       }
618     }
619     while (x > hidden.adjustForHiddenColumns(endRes))
620     {
621       if (!scrollRight(true))
622       {
623         break;
624       }
625     }
626   }
627
628   /**
629    * Set the viewport location so that a position is visible
630    * 
631    * @param x
632    *          column to be visible: absolute position in alignment
633    * @param y
634    *          row to be visible: absolute position in alignment
635    */
636   public boolean setViewportLocation(int x, int y)
637   {
638     boolean changedLocation = false;
639
640     // convert the x,y location to visible coordinates
641     int visX = al.getHiddenColumns().findColumnPosition(x);
642     int visY = al.getHiddenSequences().findIndexWithoutHiddenSeqs(y);
643
644     // if (vis_x,vis_y) is already visible don't do anything
645     if (startRes > visX || visX > endRes
646             || startSeq > visY && visY > endSeq)
647     {
648       int[] old = new int[] { startRes, startSeq };
649       int[] newresseq;
650       if (wrappedMode)
651       {
652         int newstartres = calcWrappedStartResidue(visX);
653         setStartRes(newstartres);
654         newresseq = new int[] { startRes, startSeq };
655       }
656       else
657       {
658         // set the viewport x location to contain vis_x
659         int newstartres = visX;
660         int width = getViewportWidth();
661         if (newstartres + width - 1 > getVisibleAlignmentWidth() - 1)
662         {
663           newstartres = getVisibleAlignmentWidth() - width;
664         }
665         updateStartEndRes(newstartres, newstartres + width - 1);
666
667         // set the viewport y location to contain vis_y
668         int newstartseq = visY;
669         int height = getViewportHeight();
670         if (newstartseq + height - 1 > getVisibleAlignmentHeight() - 1)
671         {
672           newstartseq = getVisibleAlignmentHeight() - height;
673         }
674         updateStartEndSeq(newstartseq, newstartseq + height - 1);
675
676         newresseq = new int[] { startRes, startSeq };
677       }
678       changedLocation = true;
679       changeSupport.firePropertyChange(MOVE_VIEWPORT, old, newresseq);
680     }
681     return changedLocation;
682   }
683
684   /**
685    * Adjust sequence position for page up. Fires a property change event.
686    */
687   public void pageUp()
688   {
689     if (wrappedMode)
690     {
691       setStartRes(Math.max(0, getStartRes() - getViewportWidth()));
692     }
693     else
694     {
695       setViewportStartAndHeight(startSeq - (endSeq - startSeq),
696               getViewportHeight());
697     }
698   }
699
700   /**
701    * Adjust sequence position for page down. Fires a property change event.
702    */
703   public void pageDown()
704   {
705     if (wrappedMode)
706     {
707       /*
708        * if height is more than width (i.e. not all sequences fit on screen),
709        * increase page down to height
710        */
711       int newStart = getStartRes()
712               + Math.max(getViewportHeight(), getViewportWidth());
713
714       /*
715        * don't page down beyond end of alignment, or if not all
716        * sequences fit in the visible height
717        */
718       if (newStart < getVisibleAlignmentWidth())
719       {
720         setStartRes(newStart);
721       }
722     }
723     else
724     {
725       setViewportStartAndHeight(endSeq, getViewportHeight());
726     }
727   }
728
729   public void setWrappedMode(boolean wrapped)
730   {
731     wrappedMode = wrapped;
732   }
733
734   public boolean isWrappedMode()
735   {
736     return wrappedMode;
737   }
738
739   /**
740    * Answers the vertical scroll position (0..) to set, given the visible column
741    * that is at top left.
742    * 
743    * <pre>
744    * Example:
745    *    viewport width 40 columns (0-39, 40-79, 80-119...)
746    *    column 0 returns scroll position 0
747    *    columns 1-40 return scroll position 1
748    *    columns 41-80 return scroll position 2
749    *    etc
750    * </pre>
751    * 
752    * @param topLeftColumn
753    *          (0..)
754    * @return
755    */
756   public int getWrappedScrollPosition(final int topLeftColumn)
757   {
758     int w = getViewportWidth();
759
760     /*
761      * visible whole widths
762      */
763     int scroll = topLeftColumn / w;
764
765     /*
766      * add 1 for a part width if there is one
767      */
768     scroll += topLeftColumn % w > 0 ? 1 : 0;
769
770     return scroll;
771   }
772
773   /**
774    * Answers the maximum wrapped vertical scroll value, given the column
775    * position (0..) to show at top left of the visible region.
776    * 
777    * @param topLeftColumn
778    * @return
779    */
780   public int getWrappedMaxScroll(int topLeftColumn)
781   {
782     int scrollPosition = getWrappedScrollPosition(topLeftColumn);
783
784     /*
785      * how many more widths could be drawn after this one?
786      */
787     int columnsRemaining = getVisibleAlignmentWidth() - topLeftColumn;
788     int width = getViewportWidth();
789     int widthsRemaining = columnsRemaining / width
790             + (columnsRemaining % width > 0 ? 1 : 0) - 1;
791     int maxScroll = scrollPosition + widthsRemaining;
792
793     return maxScroll;
794   }
795 }