/* * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) * Copyright (C) $$Year-Rel$$ The Jalview Authors * * This file is part of Jalview. * * Jalview is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * Jalview is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty * of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Jalview. If not, see . * The Jalview Authors are detailed in the 'AUTHORS' file. */ package jalview.viewmodel; import jalview.datamodel.AlignmentI; import jalview.datamodel.HiddenColumns; /** * Supplies and updates viewport properties relating to position such as: start * and end residues and sequences; ideally will serve hidden columns/rows too. * Intention also to support calculations for positioning, scrolling etc. such * as finding the middle of the viewport, checking for scrolls off screen */ public class ViewportRanges extends ViewportProperties { public static final String STARTRES = "startres"; public static final String ENDRES = "endres"; public static final String STARTSEQ = "startseq"; public static final String ENDSEQ = "endseq"; public static final String STARTRESANDSEQ = "startresandseq"; public static final String MOVE_VIEWPORT = "move_viewport"; private boolean wrappedMode = false; // start residue of viewport private int startRes; // end residue of viewport private int endRes; // start sequence of viewport private int startSeq; // end sequence of viewport private int endSeq; // alignment private AlignmentI al; /** * Constructor * * @param alignment * the viewport's alignment */ public ViewportRanges(AlignmentI alignment) { // initial values of viewport settings this.startRes = 0; this.endRes = alignment.getWidth() - 1; this.startSeq = 0; this.endSeq = alignment.getHeight() - 1; this.al = alignment; } /** * Get alignment width in cols, including hidden cols */ public int getAbsoluteAlignmentWidth() { return al.getWidth(); } /** * Get alignment height in rows, including hidden rows */ public int getAbsoluteAlignmentHeight() { return al.getHeight() + al.getHiddenSequences().getSize(); } /** * Get alignment width in cols, excluding hidden cols */ public int getVisibleAlignmentWidth() { return al.getVisibleWidth(); } /** * Get alignment height in rows, excluding hidden rows */ public int getVisibleAlignmentHeight() { return al.getHeight(); } /** * Set first residue visible in the viewport, and retain the current width. * Fires a property change event. * * @param res * residue position */ public void setStartRes(int res) { int width = getViewportWidth(); setStartEndRes(res, res + width - 1); } /** * Set start and end residues at the same time. This method only fires one * event for the two changes, and should be used in preference to separate * calls to setStartRes and setEndRes. * * @param start * the start residue * @param end * the end residue */ public void setStartEndRes(int start, int end) { int[] oldvalues = updateStartEndRes(start, end); int oldstartres = oldvalues[0]; int oldendres = oldvalues[1]; changeSupport.firePropertyChange(STARTRES, oldstartres, startRes); if (oldstartres == startRes) { // event won't be fired if start positions are same // fire an event for the end positions in case they changed changeSupport.firePropertyChange(ENDRES, oldendres, endRes); } } /** * Update start and end residue values, adjusting for width constraints if * necessary * * @param start * start residue * @param end * end residue * @return array containing old start and end residue values */ private int[] updateStartEndRes(int start, int end) { int oldstartres = this.startRes; /* * if not wrapped, don't leave white space at the right margin */ int lastColumn = getVisibleAlignmentWidth() - 1; if (!wrappedMode && (start > lastColumn)) { startRes = Math.max(lastColumn, 0); } else if (start < 0) { startRes = 0; } else { startRes = start; } int oldendres = this.endRes; if (end < 0) { endRes = 0; } else if (!wrappedMode && (end > lastColumn)) { endRes = Math.max(lastColumn, 0); } else { endRes = end; } return new int[] { oldstartres, oldendres }; } /** * Set the first sequence visible in the viewport, maintaining the height. If * the viewport would extend past the last sequence, sets the viewport so it * sits at the bottom of the alignment. Fires a property change event. * * @param seq * sequence position */ public void setStartSeq(int seq) { int startseq = seq; int height = getViewportHeight(); if (startseq + height - 1 > getVisibleAlignmentHeight() - 1) { startseq = getVisibleAlignmentHeight() - height; } setStartEndSeq(startseq, startseq + height - 1); } /** * Set start and end sequences at the same time. The viewport height may * change. This method only fires one event for the two changes, and should be * used in preference to separate calls to setStartSeq and setEndSeq. * * @param start * the start sequence * @param end * the end sequence */ public void setStartEndSeq(int start, int end) { // System.out.println("ViewportRange setStartEndSeq " + start + " " + end); int[] oldvalues = updateStartEndSeq(start, end); int oldstartseq = oldvalues[0]; int oldendseq = oldvalues[1]; changeSupport.firePropertyChange(STARTSEQ, oldstartseq, startSeq); if (oldstartseq == startSeq) { // event won't be fired if start positions are the same // fire in case the end positions changed changeSupport.firePropertyChange(ENDSEQ, oldendseq, endSeq); } } /** * Update start and end sequence values, adjusting for height constraints if * necessary * * @param start * start sequence * @param end * end sequence * @return array containing old start and end sequence values */ private int[] updateStartEndSeq(int start, int end) { int oldstartseq = this.startSeq; int visibleHeight = getVisibleAlignmentHeight(); if (start > visibleHeight - 1) { startSeq = Math.max(visibleHeight - 1, 0); } else if (start < 0) { startSeq = 0; } else { startSeq = start; } int oldendseq = this.endSeq; if (end >= visibleHeight) { endSeq = Math.max(visibleHeight - 1, 0); } else if (end < 0) { endSeq = 0; } else { endSeq = end; } return new int[] { oldstartseq, oldendseq }; } /** * Set the last sequence visible in the viewport. Fires a property change * event. * * @param seq * sequence position in the range [0, height) */ public void setEndSeq(int seq) { // BH 2018.04.18 added safety for seq < 0; comment about not being >= height setStartEndSeq(Math.max(0, seq + 1 - getViewportHeight()), seq); } /** * Set start residue and start sequence together (fires single event). The * event supplies a pair of old values and a pair of new values: [old start * residue, old start sequence] and [new start residue, new start sequence] * * @param res * the start residue * @param seq * the start sequence */ public void setStartResAndSeq(int res, int seq) { int width = getViewportWidth(); int[] oldresvalues = updateStartEndRes(res, res + width - 1); int startseq = seq; int height = getViewportHeight(); if (startseq + height - 1 > getVisibleAlignmentHeight() - 1) { startseq = getVisibleAlignmentHeight() - height; } int[] oldseqvalues = updateStartEndSeq(startseq, startseq + height - 1); int[] old = new int[] { oldresvalues[0], oldseqvalues[0] }; int[] newresseq = new int[] { startRes, startSeq }; changeSupport.firePropertyChange(STARTRESANDSEQ, old, newresseq); } /** * Get start residue of viewport */ public int getStartRes() { return startRes; } /** * Get end residue of viewport */ public int getEndRes() { return endRes; } /** * Get start sequence of viewport */ public int getStartSeq() { return startSeq; } /** * Get end sequence of viewport */ public int getEndSeq() { return endSeq; } /** * Set viewport width in residues, without changing startRes. Use in * preference to calculating endRes from the width, to avoid out by one * errors! Fires a property change event. * * @param w * width in residues */ public void setViewportWidth(int w) { setStartEndRes(startRes, startRes + w - 1); } /** * Set viewport height in residues, without changing startSeq. Use in * preference to calculating endSeq from the height, to avoid out by one * errors! Fires a property change event. * * @param h * height in sequences */ public void setViewportHeight(int h) { setStartEndSeq(startSeq, startSeq + h - 1); } /** * Set viewport horizontal start position and width. Use in preference to * calculating endRes from the width, to avoid out by one errors! Fires a * property change event. * * @param start * start residue * @param w * width in residues */ public void setViewportStartAndWidth(int start, int w) { int vpstart = start; if (vpstart < 0) { vpstart = 0; } /* * if not wrapped, don't leave white space at the right margin */ if (!wrappedMode) { if ((w <= getVisibleAlignmentWidth()) && (vpstart + w - 1 > getVisibleAlignmentWidth() - 1)) { vpstart = getVisibleAlignmentWidth() - w; } } setStartEndRes(vpstart, vpstart + w - 1); } /** * Set viewport vertical start position and height. Use in preference to * calculating endSeq from the height, to avoid out by one errors! Fires a * property change event. * * @param start * start sequence * @param h * height in sequences */ public void setViewportStartAndHeight(int start, int h) { int vpstart = start; int visHeight = getVisibleAlignmentHeight(); if (vpstart < 0) { vpstart = 0; } else if (h <= visHeight && vpstart + h > visHeight) // viewport height is less than the full alignment and we are running off // the bottom { vpstart = visHeight - h; } // System.out.println("ViewportRanges setviewportStartAndHeight " + vpstart // + " " + start + " " + h + " " + getVisibleAlignmentHeight()); setStartEndSeq(vpstart, vpstart + h - 1); } /** * Get width of viewport in residues * * @return width of viewport */ public int getViewportWidth() { return (endRes - startRes + 1); } /** * Get height of viewport in residues * * @return height of viewport */ public int getViewportHeight() { return (endSeq - startSeq + 1); } /** * Scroll the viewport range vertically. Fires a property change event. * * @param up * true if scrolling up, false if down * * @return true if the scroll is valid */ public boolean scrollUp(boolean up) { /* * if in unwrapped mode, scroll up or down one sequence row; * if in wrapped mode, scroll by one visible width of columns */ if (up) { if (wrappedMode) { pageUp(); } else { if (startSeq < 1) { return false; } setStartSeq(startSeq - 1); } } else { if (wrappedMode) { pageDown(); } else { if (endSeq >= getVisibleAlignmentHeight() - 1) { return false; } setStartSeq(startSeq + 1); } } return true; } /** * Scroll the viewport range horizontally. Fires a property change event. * * @param right * true if scrolling right, false if left * * @return true if the scroll is valid */ public boolean scrollRight(boolean right) { if (!right) { if (startRes < 1) { return false; } setStartRes(startRes - 1); } else { if (endRes >= getVisibleAlignmentWidth() - 1) { return false; } setStartRes(startRes + 1); } return true; } /** * Scroll a wrapped alignment so that the specified residue is in the first * repeat of the wrapped view. Fires a property change event. Answers true if * the startRes changed, else false. * * @param res * residue position to scroll to NB visible position not absolute * alignment position * @return */ public boolean scrollToWrappedVisible(int res) { int newStartRes = calcWrappedStartResidue(res); if (newStartRes == startRes) { return false; } setStartRes(newStartRes); return true; } /** * Calculate wrapped start residue from visible start residue * * @param res * visible start residue * @return left column of panel res will be located in */ private int calcWrappedStartResidue(int res) { int oldStartRes = startRes; int width = getViewportWidth(); boolean up = res < oldStartRes; int widthsToScroll = Math.abs((res - oldStartRes) / width); if (up) { widthsToScroll++; } int residuesToScroll = width * widthsToScroll; int newStartRes = up ? oldStartRes - residuesToScroll : oldStartRes + residuesToScroll; if (newStartRes < 0) { newStartRes = 0; } return newStartRes; } /** * Scroll so that (x,y) is visible. Fires a property change event. * * @param x * x position in alignment (absolute position) * @param y * y position in alignment (absolute position) */ public void scrollToVisible(int x, int y) { while (y < startSeq) { scrollUp(true); } while (y > endSeq) { scrollUp(false); } HiddenColumns hidden = al.getHiddenColumns(); while (x < hidden.visibleToAbsoluteColumn(startRes)) { if (!scrollRight(false)) { break; } } while (x > hidden.visibleToAbsoluteColumn(endRes)) { if (!scrollRight(true)) { break; } } } /** * Set the viewport location so that a position is visible * * @param x * column to be visible: absolute position in alignment * @param y * row to be visible: absolute position in alignment */ public boolean setViewportLocation(int x, int y) { boolean changedLocation = false; // convert the x,y location to visible coordinates int visX = al.getHiddenColumns().absoluteToVisibleColumn(x); int visY = al.getHiddenSequences().findIndexWithoutHiddenSeqs(y); // if (vis_x,vis_y) is already visible don't do anything if (startRes > visX || visX > endRes || startSeq > visY && visY > endSeq) { int[] old = new int[] { startRes, startSeq }; int[] newresseq; if (wrappedMode) { int newstartres = calcWrappedStartResidue(visX); setStartRes(newstartres); newresseq = new int[] { startRes, startSeq }; } else { // set the viewport x location to contain vis_x int newstartres = visX; int width = getViewportWidth(); if (newstartres + width - 1 > getVisibleAlignmentWidth() - 1) { newstartres = getVisibleAlignmentWidth() - width; } updateStartEndRes(newstartres, newstartres + width - 1); // set the viewport y location to contain vis_y int newstartseq = visY; int height = getViewportHeight(); if (newstartseq + height - 1 > getVisibleAlignmentHeight() - 1) { newstartseq = getVisibleAlignmentHeight() - height; } updateStartEndSeq(newstartseq, newstartseq + height - 1); newresseq = new int[] { startRes, startSeq }; } changedLocation = true; changeSupport.firePropertyChange(MOVE_VIEWPORT, old, newresseq); } return changedLocation; } /** * Adjust sequence position for page up. Fires a property change event. */ public void pageUp() { if (wrappedMode) { setStartRes(Math.max(0, getStartRes() - getViewportWidth())); } else { setViewportStartAndHeight(startSeq - (endSeq - startSeq), getViewportHeight()); } } /** * Adjust sequence position for page down. Fires a property change event. */ public void pageDown() { if (wrappedMode) { /* * if height is more than width (i.e. not all sequences fit on screen), * increase page down to height */ int newStart = getStartRes() + Math.max(getViewportHeight(), getViewportWidth()); /* * don't page down beyond end of alignment, or if not all * sequences fit in the visible height */ if (newStart < getVisibleAlignmentWidth()) { setStartRes(newStart); } } else { setViewportStartAndHeight(endSeq, getViewportHeight()); } } public void setWrappedMode(boolean wrapped) { wrappedMode = wrapped; } public boolean isWrappedMode() { return wrappedMode; } /** * Answers the vertical scroll position (0..) to set, given the visible column * that is at top left. * *
   * Example:
   *    viewport width 40 columns (0-39, 40-79, 80-119...)
   *    column 0 returns scroll position 0
   *    columns 1-40 return scroll position 1
   *    columns 41-80 return scroll position 2
   *    etc
   * 
* * @param topLeftColumn * (0..) * @return */ public int getWrappedScrollPosition(final int topLeftColumn) { int w = getViewportWidth(); /* * visible whole widths */ int scroll = topLeftColumn / w; /* * add 1 for a part width if there is one */ scroll += topLeftColumn % w > 0 ? 1 : 0; return scroll; } /** * Answers the maximum wrapped vertical scroll value, given the column * position (0..) to show at top left of the visible region. * * @param topLeftColumn * @return */ public int getWrappedMaxScroll(int topLeftColumn) { int scrollPosition = getWrappedScrollPosition(topLeftColumn); /* * how many more widths could be drawn after this one? */ int columnsRemaining = getVisibleAlignmentWidth() - topLeftColumn; int width = getViewportWidth(); int widthsRemaining = columnsRemaining / width + (columnsRemaining % width > 0 ? 1 : 0) - 1; int maxScroll = scrollPosition + widthsRemaining; return maxScroll; } }