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