savant.view.swing.GraphPane.java Source code

Java tutorial

Introduction

Here is the source code for savant.view.swing.GraphPane.java

Source

/**
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package savant.view.swing;

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.Area;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.*;
import org.apache.commons.httpclient.NameValuePair;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.ut.biolab.savant.analytics.savantanalytics.AnalyticsAgent;

import savant.api.adapter.FrameAdapter;
import savant.api.adapter.GraphPaneAdapter;
import savant.api.data.ContinuousRecord;
import savant.api.data.Record;
import savant.api.event.ExportEvent;
import savant.api.event.PopupEvent;
import savant.api.util.Listener;
import savant.controller.GraphPaneController;
import savant.controller.LocationController;
import savant.controller.event.GraphPaneEvent;
import savant.data.types.BAMIntervalRecord;
import savant.exception.RenderingException;
import savant.selection.PopupThread;
import savant.selection.PopupPanel;
import savant.settings.ColourSettings;
import savant.settings.InterfaceSettings;
import savant.util.swing.ProgressPanel;
import savant.util.*;
import savant.view.tracks.BAMTrack;
import savant.view.tracks.BAMTrackRenderer;
import savant.view.tracks.ContinuousTrackRenderer;
import savant.view.tracks.Track;
import savant.view.tracks.TrackCancellationListener;

/**
 *
 * @author mfiume
 */
public class GraphPane extends JPanel
        implements GraphPaneAdapter, MouseWheelListener, MouseListener, MouseMotionListener {

    private static final Log LOG = LogFactory.getLog(GraphPane.class);
    private Track[] tracks;
    private Frame parentFrame;
    private int mouseX = 0;
    private int mouseY = 0;
    /**
     * min / max axis values
     */
    private int xMin;
    private int xMax;
    protected int yMin;
    protected int yMax;
    private double unitWidth = Double.NaN;
    protected double unitHeight;
    private AxisType yAxisType = AxisType.NONE;
    private AxisType xAxisType = AxisType.NONE;
    private boolean mouseInside = false;
    // Locking
    private Range lockedRange;
    /**
     * By default, tracks adjust their unitHeight to accommodate the contents
     * without a scroll-bar.
     */
    protected boolean scaledToFit = true;
    /**
     * Selection Variables
     */
    private Rectangle2D selectionRect = new Rectangle2D.Double();
    private boolean isDragging = false;
    //scrolling...
    private BufferedImage bufferedImage;
    private Range prevRange = null;
    private DrawingMode prevMode = null;
    private Dimension prevSize = null;
    private String prevRef = null;
    private boolean paneResize = false;
    private int newHeight;
    private int oldWidth = -1;
    private int oldHeight = -1;
    private int oldViewHeight = -1;
    private boolean renderRequired = false;
    private int posOffset = 0;
    protected boolean forcedHeight = false;
    //dragging
    private int startX;
    private int startY;
    private int baseX;
    private int initialScroll;
    private boolean panVert = false;
    //popup
    public Thread popupThread;
    private Record currentOverRecord = null;
    private Shape currentOverShape = null;
    /**
     * Provides progress indication when loading a track.
     */
    private ProgressPanel progressPanel;
    //awaiting exported images
    private final List<Listener<ExportEvent>> exportListeners = new ArrayList<Listener<ExportEvent>>();
    private final List<Listener<PopupEvent>> popupListeners = new ArrayList<Listener<PopupEvent>>();
    private boolean yAxisLocked;

    /**
     * CONSTRUCTOR
     */
    public GraphPane(Frame parent) {
        this.parentFrame = parent;
        addMouseListener(this); // listens for own mouse and
        addMouseMotionListener(this); // mouse-motion events
        //addKeyListener( this );
        getInputMap().allKeys();
        addMouseWheelListener(this);

        //trackToYRangeMap = new HashMap<Track, Range>();

        popupThread = new Thread(new PopupThread(this), "PopupThread");
        popupThread.start();

        //
        GraphPaneController gpc = GraphPaneController.getInstance();

        gpc.addListener(new Listener<GraphPaneEvent>() {
            @Override
            public void handleEvent(GraphPaneEvent event) {
                if (event.getType() == GraphPaneEvent.Type.HIGHLIGHTING) {
                    repaint();
                }
            }
        });
    }

    /**
     * Lock the Y Axes from changing automatically
     *
     * @param b
     */
    public void setYMaxLocked(boolean b) {
        System.out.println("Locking Y max: " + b);
        this.yAxisLocked = b;
    }

    /**
     * Set the tracks to be displayed in this GraphPane
     *
     * @param tracks an array of Track objects to be added
     */
    public void setTracks(Track[] tracks) {
        this.tracks = tracks;
        setYAxisType(AxisType.NONE); // We don't get a y-axis until the renderer kicks in.
    }

    /**
     * Return the list of tracks associated with this GraphPane.
     */
    public Track[] getTracks() {
        return tracks;
    }

    /**
     * Render the contents of the GraphPane. Includes drawing a common
     * background for all tracks.
     *
     * @param g2 the Graphics object into which to draw.
     */
    public boolean render(Graphics2D g2) {
        return render(g2, new Range(xMin, xMax), null);
    }

    public boolean render(Graphics2D g2, Range xRange, Range yRange) {
        LOG.trace("GraphPane.render(g2, " + xRange + ", " + yRange + ")");
        double oldUnitHeight = unitHeight;
        int oldYMax = yMax;

        // Paint a gradient from top to bottom
        GradientPaint gp0 = new GradientPaint(0, 0, ColourSettings.getColor(ColourKey.GRAPH_PANE_BACKGROUND_TOP), 0,
                getHeight(), ColourSettings.getColor(ColourKey.GRAPH_PANE_BACKGROUND_BOTTOM));
        g2.setPaint(gp0);
        g2.fillRect(0, 0, getWidth(), getHeight());

        GraphPaneController gpc = GraphPaneController.getInstance();
        LocationController lc = LocationController.getInstance();
        JScrollBar scroller = getVerticalScrollBar();

        if (gpc.isPanning() && !isLocked()) {

            double fromX = transformXPos(gpc.getMouseClickPosition());
            double toX = transformXPos(gpc.getMouseReleasePosition());
            g2.translate(toX - fromX, 0);
        }

        // Deal with the progress-bar.
        if (tracks == null) {
            parentFrame.updateProgress();
            return false;
        } else {
            for (Track t : tracks) {
                if (t.getRenderer().isWaitingForData()) {
                    String progressMsg = (String) t.getRenderer().getInstruction(DrawingInstruction.PROGRESS);
                    setPreferredSize(new Dimension(getWidth(), 0));
                    showProgress(progressMsg, -1.0);
                    return false;
                }
            }
        }
        if (progressPanel != null) {
            remove(progressPanel);
            progressPanel = null;
        }

        int minYRange = Integer.MAX_VALUE;
        int maxYRange = Integer.MIN_VALUE;
        AxisType bestYAxis = AxisType.NONE;
        for (Track t : tracks) {

            // ask renderers for extra info on range; consolidate to maximum Y range
            AxisRange axisRange = (AxisRange) t.getRenderer().getInstruction(DrawingInstruction.AXIS_RANGE);

            if (axisRange != null) {
                int axisYMin = axisRange.getYMin();
                int axisYMax = axisRange.getYMax();
                if (axisYMin < minYRange) {
                    minYRange = axisYMin;
                }
                if (axisYMax > maxYRange) {
                    maxYRange = axisYMax;
                }
            }

            // Ask renderers if they want horizontal grid-lines; if any say yes, draw them.
            switch (t.getYAxisType(t.getResolution(xRange))) {
            case INTEGER_GRIDLESS:
                if (bestYAxis == AxisType.NONE) {
                    bestYAxis = AxisType.INTEGER_GRIDLESS;
                }
                break;
            case INTEGER:
                if (bestYAxis != AxisType.REAL) {
                    bestYAxis = AxisType.INTEGER;
                }
                break;
            case REAL:
                bestYAxis = AxisType.REAL;
                break;
            }
        }

        setXAxisType(tracks[0].getXAxisType(tracks[0].getResolution(xRange)));
        setXRange(xRange);
        setYAxisType(bestYAxis);
        Range consolidatedYRange = new Range(minYRange, maxYRange);

        setYRange(consolidatedYRange);
        consolidatedYRange = new Range(yMin, yMax);

        DrawingMode currentMode = tracks[0].getDrawingMode();

        boolean sameRange = (prevRange != null && xRange.equals(prevRange));
        if (!sameRange) {
            PopupPanel.hidePopup();
        }
        boolean sameMode = currentMode == prevMode;
        boolean sameSize = prevSize != null && getSize().equals(prevSize)
                && parentFrame.getFrameLandscape().getWidth() == oldWidth
                && parentFrame.getFrameLandscape().getHeight() == oldHeight;
        boolean sameRef = prevRef != null && lc.getReferenceName().equals(prevRef);
        boolean withinScrollBounds = bufferedImage != null && scroller.getValue() >= getOffset()
                && scroller.getValue() < getOffset() + getViewportHeight() * 2;

        //bufferedImage stores the current graphic for future use. If nothing
        //has changed in the track since the last render, bufferedImage will
        //be used to redraw the current view. This method allows for fast repaints
        //on tracks where nothing has changed (panning, selection, plumbline,...)

        //if nothing has changed draw buffered image
        if (sameRange && sameMode && sameSize && sameRef && !renderRequired && withinScrollBounds) {
            g2.drawImage(bufferedImage, 0, getOffset(), this);
            renderCurrentSelected(g2);

            //force unitHeight from last render
            unitHeight = oldUnitHeight;
            yMax = oldYMax;

        } else {
            // Otherwise prepare for new render.
            renderRequired = false;

            int h = getHeight();
            if (!forcedHeight) {
                h = Math.min(h, getViewportHeight() * 3);
            }
            LOG.debug("Requesting " + getWidth() + "\u00D7" + h + " bufferedImage.");
            bufferedImage = new BufferedImage(getWidth(), h, BufferedImage.TYPE_INT_RGB);
            if (bufferedImage.getHeight() == getHeight()) {
                setOffset(0);
            } else {
                setOffset(scroller.getValue() - getViewportHeight());
            }
            LOG.debug("Rendering fresh " + bufferedImage.getWidth() + "\u00D7" + bufferedImage.getHeight()
                    + " bufferedImage at (0, " + getOffset() + ")");

            Graphics2D g3 = bufferedImage.createGraphics();
            g3.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            prevRange = xRange;
            prevSize = getSize();
            prevMode = tracks[0].getDrawingMode();
            prevRef = lc.getReferenceName();

            renderBackground(g3, xAxisType == AxisType.INTEGER || xAxisType == AxisType.REAL,
                    yAxisType == AxisType.INTEGER || yAxisType == AxisType.REAL);

            // Call the actual render() methods.
            boolean nothingRendered = true;
            String message = null;
            int priority = -1;

            for (Track t : tracks) {
                // Change renderers' drawing instructions to reflect consolidated YRange
                AxisRange axes = (AxisRange) t.getRenderer().getInstruction(DrawingInstruction.AXIS_RANGE);

                if (axes == null) {
                    axes = new AxisRange(xRange, consolidatedYRange);
                } else {
                    axes = new AxisRange(axes.getXRange(), consolidatedYRange);
                }

                //System.out.println("Consolidated y range for " + t.getName() + " is " + consolidatedYRange);
                t.getRenderer().addInstruction(DrawingInstruction.AXIS_RANGE, axes);

                try {
                    t.getRenderer().render(g3, this);
                    nothingRendered = false;
                } catch (RenderingException rx) {
                    if (rx.getPriority() > priority) {
                        // If we have more than one message with the same priority, the first one will end up being drawn.
                        message = rx.getMessage();
                        priority = rx.getPriority();
                    }
                } catch (Throwable x) {
                    // Renderer itself threw an exception.
                    LOG.error("Error rendering " + t, x);
                    message = MiscUtils.getMessage(x);
                    priority = RenderingException.ERROR_PRIORITY;
                }
            }
            if (nothingRendered && message != null) {
                setPreferredSize(new Dimension(getWidth(), 0));
                revalidate();
                drawMessage(g3, message);
            }

            updateYMax();

            // If a change has occured that affects scrollbar...
            if (paneResize) {
                paneResize = false;

                // Change size of current frame
                if (getHeight() != newHeight) {
                    Dimension newSize = new Dimension(getWidth(), newHeight);
                    setPreferredSize(newSize);
                    setSize(newSize);
                    parentFrame.validate(); // Ensures that scroller.getMaximum() is up to date.

                    // If pane is resized, scrolling always starts at the bottom.  The only place
                    // where looks wrong is when we have a continuous track with negative values.
                    scroller.setValue(scroller.getMaximum());
                    repaint();
                    return false;
                }
            } else if (oldViewHeight != -1 && oldViewHeight != getViewportHeight()) {
                int newViewHeight = getViewportHeight();
                int oldScroll = scroller.getValue();
                scroller.setValue(oldScroll + (oldViewHeight - newViewHeight));
                oldViewHeight = newViewHeight;
            }
            oldWidth = parentFrame.getFrameLandscape().getWidth();
            oldHeight = parentFrame.getFrameLandscape().getHeight();

            g2.drawImage(bufferedImage, 0, getOffset(), this);
            fireExportEvent(xRange, bufferedImage);

            renderCurrentSelected(g2);
        }
        return true;
    }

    /**
     * Get the height of the viewport. The viewport is the grandparent of this
     * GraphPane.
     */
    private int getViewportHeight() {
        return getParent().getParent().getHeight();
    }

    /**
     * Access to the scrollbar associated with this GraphPane. For ordinary
     * GraphPanes, the JScrollPane is our great-grandparent.
     */
    public JScrollBar getVerticalScrollBar() {
        return ((JScrollPane) getParent().getParent().getParent()).getVerticalScrollBar();
    }

    private void renderCurrentSelected(Graphics2D g2) {
        // Temporarily shift the origin
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2.translate(0, getOffset());
        for (Track t : tracks) {
            if (t.getRenderer().hasMappedValues()) {
                List<Shape> currentSelected = t.getRenderer().getCurrentSelectedShapes(this);
                if (currentSelected.size() > 0) {
                    boolean arcMode = t.getDrawingMode() == DrawingMode.ARC_PAIRED;
                    for (Shape selectedShape : currentSelected) {
                        if (selectedShape != currentOverShape) {
                            if (arcMode) {
                                g2.setColor(Color.GREEN);
                                g2.draw(selectedShape);
                            } else {
                                //g2.setColor(Color.GREEN);
                                g2.setColor(new Color(0, 255, 0, 150));
                                g2.fill(selectedShape);
                                if (selectedShape.getBounds().getWidth() > 5) {
                                    g2.setColor(Color.BLACK);
                                    g2.draw(selectedShape);
                                }
                            }
                        }
                    }
                }
                break;
            }
        }
        if (currentOverShape != null) {
            if (tracks[0].getDrawingMode() == DrawingMode.ARC_PAIRED) {
                g2.setColor(Color.RED);
                g2.draw(currentOverShape);

                //get record pair
                BAMIntervalRecord rec1 = (BAMIntervalRecord) currentOverRecord;
                BAMIntervalRecord rec2 = ((BAMTrack) tracks[0]).getMate(rec1); //mate

                //render reads with mismatches
                ((BAMTrackRenderer) tracks[0].getRenderer()).renderReadsFromArc(g2, this, rec1, rec2, prevRange);

            } else {
                g2.setColor(new Color(255, 0, 0, 150));
                g2.fill(currentOverShape);
                if (currentOverShape.getBounds() != null && currentOverShape.getBounds().getWidth() > 5
                        && currentOverShape.getBounds().getHeight() > 3) {
                    g2.setColor(Color.BLACK);
                    g2.draw(currentOverShape);
                }
            }
        }
        //shift the origin back
        g2.translate(0, -getOffset());
    }

    @Override
    public void setRenderForced() {
        renderRequired = true;
    }

    public boolean isRenderForced() {
        return renderRequired;
    }

    /**
     * Force the bufferedImage to contain entire height at current range.
     * Intended for creating images for track export. Make sure you unforce
     * immediately after!
     */
    public void forceFullHeight() {
        forcedHeight = true;
    }

    public void unforceFullHeight() {
        forcedHeight = false;
    }

    private void setOffset(int offset) {
        posOffset = offset;
    }

    @Override
    public int getOffset() {
        return posOffset;
    }

    private void requestHeight(int h) {
        newHeight = h;
        paneResize = true;
    }

    @Override
    protected void paintComponent(Graphics g) {
        if (tracks != null && tracks.length > 0) {
            LOG.trace("GraphPane.paintComponent(" + tracks[0].getName() + ")");
        }
        super.paintComponent(g);

        Graphics2D g2 = (Graphics2D) g;
        boolean trueRender = render(g2);

        GraphPaneController gpc = GraphPaneController.getInstance();
        int h = getHeight();

        // Aiming adjustments.
        if (gpc.isAiming() && mouseInside) {
            g2.setColor(Color.BLACK);
            Font thickfont = g2.getFont().deriveFont(Font.BOLD, 15.0F);
            g2.setFont(thickfont);
            int genomeX = gpc.getMouseXPosition();
            double genomeY = gpc.getMouseYPosition();
            String target = "";
            target += "X: " + MiscUtils.numToString(genomeX);
            if (!Double.isNaN(genomeY)) {
                target += " Y: " + MiscUtils.numToString(genomeY);
            }

            g2.drawLine(mouseX, 0, mouseX, h);
            if (genomeY != -1) {
                g.drawLine(0, mouseY, this.getWidth(), mouseY);
            }
            g2.drawString(target, mouseX + 5, mouseY - 5);
        }

        double x1 = transformXPos(gpc.getMouseClickPosition());
        double x2 = transformXPos(gpc.getMouseReleasePosition());

        double width = x1 - x2;

        selectionRect = new Rectangle2D.Double(width < 0 ? x1 : x2, 0.0, Math.max(2.0, Math.abs(width)), h);

        if (gpc.isPanning()) {
            // Panning adjustments (none).
        } else if (gpc.isZooming() || gpc.isSelecting()) {
            // Zooming adjustments.
            Rectangle2D rectangle = new Rectangle2D.Double(selectionRect.getX(), selectionRect.getY() - 10.0,
                    selectionRect.getWidth(), selectionRect.getHeight() + 10.0);
            g2.setColor(Color.gray);
            g2.setStroke(
                    new BasicStroke(1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 3f, new float[] { 4f }, 4f));
            g2.draw(rectangle);

            if (gpc.isZooming()) {
                g.setColor(ColourSettings.getColor(ColourKey.GRAPH_PANE_ZOOM_FILL));
            } else if (gpc.isSelecting()) {
                g.setColor(ColourSettings.getColor(ColourKey.GRAPH_PANE_SELECTION_FILL));
            }
            g2.fill(selectionRect);
        }

        // Plumbing adjustments.
        Range xRange = getXRange();
        if (gpc.isPlumbing()) {
            g2.setColor(Color.BLACK);
            double spos = transformXPos(gpc.getMouseXPosition());
            g2.draw(new Line2D.Double(spos, 0, spos, h));
            double rpos = transformXPos(gpc.getMouseXPosition() + 1);
            g2.draw(new Line2D.Double(rpos, 0, rpos, h));
        }

        // Spotlight
        if (gpc.isSpotlight() && !gpc.isZooming()) {

            int center = gpc.getMouseXPosition();
            int left = center - gpc.getSpotlightSize() / 2;
            int right = left + gpc.getSpotlightSize();

            g2.setColor(new Color(0, 0, 0, 200));

            // draw left of spotlight
            if (left >= xRange.getFrom()) {
                g2.fill(new Rectangle2D.Double(0.0, 0.0, transformXPos(left), h));
            }
            // draw right of spotlight
            if (right <= xRange.getTo()) {
                double pix = transformXPos(right);
                g2.fill(new Rectangle2D.Double(pix, 0, getWidth() - pix, h));
            }
        }

        if (isLocked()) {
            drawMessage((Graphics2D) g, "Locked");
        }
        if (trueRender) {
            gpc.delistRenderingGraphpane(this);
        }
    }

    /**
     * Render the background of this GraphPane
     *
     * @param g The graphics object to use
     */
    private void renderBackground(Graphics2D g2, boolean xGridOn, boolean yGridOn) {
        int h = getHeight();
        int w = getWidth();

        // Paint a gradient from top to bottom
        GradientPaint gp0 = new GradientPaint(0, 0, ColourSettings.getColor(ColourKey.GRAPH_PANE_BACKGROUND_TOP), 0,
                h, ColourSettings.getColor(ColourKey.GRAPH_PANE_BACKGROUND_BOTTOM));
        g2.setPaint(gp0);
        g2.fillRect(0, 0, w, h);

        // We don't want the axes stomping on our labels, so make sure the clip excludes them.
        Area clipArea = new Area(new Rectangle(0, 0, w, h));
        Color gridColor = ColourSettings.getColor(ColourKey.AXIS_GRID);

        if (yGridOn) {
            // Smallish font for tick labels.
            Font tickFont = g2.getFont().deriveFont(Font.PLAIN, 9);

            int[] yTicks = MiscUtils.getTickPositions(transformYPixel(getHeight()), transformYPixel(0.0));

            g2.setColor(gridColor);
            g2.setFont(tickFont);
            for (int t : yTicks) {
                double y = transformYPos(t);

                // Skip labels at the top or bottom of the window because they look stupid.
                if (y != 0.0 && y != getHeight()) {
                    String s = Integer.toString(t);
                    Rectangle2D labelRect = tickFont.getStringBounds(s, g2.getFontRenderContext());
                    double baseline = y + labelRect.getHeight() * 0.5 - 2.0;
                    g2.drawString(s, 4.0F, (float) baseline);
                    clipArea.subtract(new Area(new Rectangle2D.Double(3.0, baseline - labelRect.getHeight() - 1.0,
                            labelRect.getWidth() + 2.0, labelRect.getHeight() + 2.0)));
                }
            }
            g2.setClip(clipArea);
            for (int t2 : yTicks) {
                double y = transformYPos(t2);
                g2.draw(new Line2D.Double(0.0, y, w, y));
            }
        }
        if (xGridOn) {
            Range r = LocationController.getInstance().getRange();
            int[] xTicks = MiscUtils.getTickPositions(r);

            g2.setColor(gridColor);
            for (int t : xTicks) {
                double x = transformXPos(t);
                g2.draw(new Line2D.Double(x, 0, x, h));
            }
        }
        g2.setClip(null);
    }

    public Range getXRange() {
        return new Range(xMin, xMax);
    }

    /**
     * Set the range for the horizontal axis, adjusting the width of graph
     * units.
     *
     * @param r an X range
     */
    @Override
    public void setXRange(Range r) {
        if (r != null) {
            xMin = r.getFrom();
            xMax = r.getTo();
            setUnitWidth();
        }
    }

    /**
     * Set the interpretation of the pane's horizontal coordinate system.
     */
    public void setXAxisType(AxisType type) {
        xAxisType = type;
    }

    @Override
    public Range getYRange() {
        return new Range(yMin, yMax);
    }

    /**
     * Set the vertical range, adjusting the height of graph units.
     *
     * @param r a Y range
     */
    @Override
    public void setYRange(Range r) {

        /*
         *         if (yAxisLocked) {
         if (lastYRange != null) {
         consolidatedYRange = lastYRange;
         System.out.println("Got previous y range for " + this.getTracks()[0].getName() + " at " + consolidatedYRange);
         } else {
         System.out.println("No y range stored for " + this.getTracks()[0].getName());
         }
         }
         */

        if (yAxisLocked) {
            return;
        }

        if (r != null && yAxisType != AxisType.NONE) {
            int oldYMin = yMin;
            yMin = r.getFrom();
            yMax = r.getTo();

            if (scaledToFit) {
                setUnitHeight();
                LOG.debug("setYRange set unit height to " + unitHeight);
            } else {
                // Adjust ymin to keep the x-axis from dropping down as we scroll along.
                if ((yMin < 0.0 || oldYMin < 0.0) && oldYMin < yMin) {
                    yMin = oldYMin;
                }
            }
        }

        //System.out.println("Saving range for " + this.getTracks()[0].getName() + " at " + new Range(yMin, yMax));

    }

    /**
     * Set the interpretation of the pane's vertical coordinate system.
     */
    public void setYAxisType(AxisType type) {
        yAxisType = type;
        parentFrame.setYMaxVisible(type != AxisType.NONE);
    }

    /**
     * Calculate the number of pixels equal to one graph unit of height.
     *
     * @return the height of a graph unit in pixels
     */
    @Override
    public double getUnitHeight() {
        return unitHeight;
    }

    /**
     * Set the number of pixels equal to one graph unit of height.
     */
    public void setUnitHeight() {
        unitHeight = (double) getHeight() / (yMax - yMin);
    }

    /**
     * Set the number of pixels equal to one graph unit of height.
     */
    @Override
    public void setUnitHeight(double height) {
        unitHeight = height;
    }

    /**
     *
     * @return the number of pixels equal to one graph unit of width.
     */
    @Override
    public double getUnitWidth() {
        return unitWidth;
    }

    /**
     * Set the number of pixels equal to one graph unit of width.
     */
    public void setUnitWidth() {
        unitWidth = (double) getWidth() / (xMax - xMin + 1);
    }

    /**
     * Transform a horizontal position in terms of drawing coordinates into
     * graph units.
     *
     * @param pix drawing position in pixels
     * @return corresponding logical position
     */
    @Override
    public int transformXPixel(double pix) {
        return (int) Math.floor(pix / unitWidth + xMin);
    }

    /**
     * Transform a horizontal position in terms of graph units into a drawing
     * coordinate.
     *
     * @param pos position in graph coordinates
     * @return corresponding drawing coordinate
     */
    @Override
    public double transformXPos(int pos) {
        return (pos - xMin) * unitWidth;
    }

    /**
     * Transform a vertical position in terms of pixels into graph units.
     *
     * @param pix position in pixel coordinates
     * @return corresponding graph coordinate
     */
    @Override
    public double transformYPixel(double pix) {
        return (getHeight() - pix) / unitHeight + yMin;
    }

    /**
     * Transform a vertical position in terms of graph units into a pixel
     * position.
     *
     * @param pos position in graph coordinates
     * @return a corresponding drawing coordinate
     */
    @Override
    public double transformYPos(double pos) {
        return getHeight() - ((pos - yMin) * unitHeight);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void mouseWheelMoved(MouseWheelEvent e) {

        int notches = e.getWheelRotation();
        LocationController lc = LocationController.getInstance();

        if (MiscUtils.MAC && e.isMetaDown() || e.isControlDown()) {
            if (notches < 0) {
                lc.shiftRangeLeft();
            } else {
                lc.shiftRangeRight();
            }
        } else {
            if (InterfaceSettings.doesWheelZoom()) {
                if (notches < 0) {
                    lc.zoomInOnMouse();
                } else {
                    lc.zoomOutFromMouse();
                }
            } else {
                JScrollBar sb = getVerticalScrollBar();
                if (sb.isVisible()) {
                    sb.setValue(sb.getValue() + notches * 15);
                }
            }
        }
    }

    private void setMouseModifier(MouseEvent e) {
        GraphPaneController gpc = GraphPaneController.getInstance();
        boolean zooming = MiscUtils.MAC ? e.isMetaDown() : e.isControlDown();
        boolean selecting = e.isShiftDown() && !zooming;
        gpc.setZooming(isDragging && zooming);
        gpc.setPanning(isDragging && !zooming && !selecting);
        gpc.setSelecting(isDragging && selecting);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void mouseClicked(MouseEvent event) {

        if (event.getClickCount() == 2) {
            LocationController.getInstance().zoomInOnMouse();
            return;
        }

        trySelect(event.getPoint());

        setMouseModifier(event);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void mousePressed(MouseEvent event) {

        setMouseModifier(event);

        requestFocus();

        int x1 = getConstrainedX(event);

        baseX = transformXPixel(x1);
        initialScroll = getVerticalScrollBar().getValue();

        Point l = event.getLocationOnScreen();
        startX = l.x;
        startY = l.y;

        GraphPaneController gpc = GraphPaneController.getInstance();
        gpc.setMouseClickPosition(transformXPixel(x1));
    }

    public void resetCursor() {
        setCursor(new Cursor(Cursor.HAND_CURSOR));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void mouseReleased(MouseEvent event) {

        GraphPaneController gpc = GraphPaneController.getInstance();
        LocationController lc = LocationController.getInstance();
        int x2 = getConstrainedX(event);

        resetCursor();

        double x1 = transformXPos(gpc.getMouseClickPosition());

        if (gpc.isPanning()) {

            if (!panVert) {
                Range r = lc.getRange();
                int shiftVal = gpc.getMouseClickPosition() - transformXPixel(x2);
                Range newr = new Range(r.getFrom() + shiftVal, r.getTo() + shiftVal);
                lc.setLocation(newr);
                AnalyticsAgent.log(new NameValuePair[] { new NameValuePair("navigation-event", "panned"),
                        new NameValuePair("navigation-modality", "mousedrag") });
            }
        } else if (gpc.isZooming()) {
            Range r;
            if (isLocked()) {
                r = lockedRange;
            } else {
                r = lc.getRange();
            }
            int newMin = (int) Math.round(Math.min(x1, x2) / getUnitWidth());
            // some weirdness here, but it's to get around an off by one
            int newMax = (int) Math.max(Math.round(Math.max(x1, x2) / getUnitWidth()) - 1, newMin);
            Range newr = new Range(r.getFrom() + newMin, r.getFrom() + newMax);

            AnalyticsAgent.log(new NameValuePair[] { new NameValuePair("navigation-event", "zoomed"),
                    new NameValuePair("navigation-modality", "mousedrag") });

            lc.setLocation(newr);
        } else if (gpc.isSelecting()) {
            for (Track t : tracks) {
                if (t.getRenderer().hasMappedValues()) {
                    if (t.getRenderer().rectangleSelect(selectionRect)) {
                        repaint();
                    }
                    break;
                }
            }
            AnalyticsAgent.log(new NameValuePair[] { new NameValuePair("selection-event", "selected"),
                    new NameValuePair("selection-modality", "mousedrag") });
        }

        isDragging = false;
        setMouseModifier(event);

        gpc.setMouseReleasePosition(transformXPixel(x2));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void mouseEntered(final MouseEvent event) {
        resetCursor();
        mouseInside = true;
        setMouseModifier(event);
        PopupPanel.hidePopup();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void mouseExited(final MouseEvent event) {
        this.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
        setMouseModifier(event);

        mouseInside = false;
        GraphPaneController.getInstance().setMouseXPosition(-1);
        GraphPaneController.getInstance().setMouseYPosition(Double.NaN, false);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void mouseDragged(MouseEvent event) {

        setMouseModifier(event);

        GraphPaneController gpc = GraphPaneController.getInstance();
        int x2 = getConstrainedX(event);

        isDragging = true;

        if (gpc.isPanning()) {
            setCursor(new Cursor(Cursor.HAND_CURSOR));
        } else if (gpc.isZooming() || gpc.isSelecting()) {
            setCursor(new Cursor(Cursor.CROSSHAIR_CURSOR));
        }

        // Check if scrollbar is present (only vertical pan if present)
        JScrollBar scroller = getVerticalScrollBar();
        boolean scroll = scroller.isVisible();

        if (scroll) {

            //get new points
            Point l = event.getLocationOnScreen();
            int currX = l.x;
            int currY = l.y;

            //magnitude
            int magX = Math.abs(currX - startX);
            int magY = Math.abs(currY - startY);

            if (magX >= magY) {
                //pan horizontally, reset vertical pan
                panVert = false;
                gpc.setMouseReleasePosition(transformXPixel(x2));
                scroller.setValue(initialScroll);
            } else {
                //pan vertically, reset horizontal pan
                panVert = true;
                gpc.setMouseReleasePosition(baseX);
                scroller.setValue(initialScroll - (currY - startY));
            }
        } else {
            //pan horizontally
            panVert = false;
            gpc.setMouseReleasePosition(transformXPixel(x2));
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void mouseMoved(MouseEvent event) {

        mouseX = event.getX();
        mouseY = event.getY();

        if (!Double.isNaN(unitWidth)) {
            GraphPaneController gpc = GraphPaneController.getInstance();

            // Update the GraphPaneController's record of the mouse position
            gpc.setMouseXPosition(transformXPixel(mouseX));
            switch (yAxisType) {
            case NONE:
                gpc.setMouseYPosition(Double.NaN, false);
                break;
            case INTEGER:
            case INTEGER_GRIDLESS:
                gpc.setMouseYPosition(Math.floor(transformYPixel(mouseY)), true);
                break;
            case REAL:
                gpc.setMouseYPosition(transformYPixel(mouseY), false);
                break;
            }
            gpc.setSpotlightSize(getXRange().getLength());
        }
    }

    /**
     * Given a MouseEvent, return the x value constrained by the dimensions of
     * the GraphPane.
     */
    private int getConstrainedX(MouseEvent event) {
        int x = event.getX();
        if (x < 0) {
            return 0;
        }
        return Math.min(x, getWidth());
    }

    /**
     * A locked track maintains its horizontal location while other tracks
     * scroll.
     *
     * @return true if the track is locked, false otherwise
     */
    public boolean isLocked() {
        return lockedRange != null;
    }

    /**
     * Set a track so that it doesn't scroll horizontally.
     *
     * @param b true to prevent horizontal scrolling
     */
    public void setLocked(boolean b) {
        if (b) {
            lockedRange = LocationController.getInstance().getRange();
        } else {
            lockedRange = null;
            renderRequired = true;
        }
        repaint();
    }

    @Override
    public FrameAdapter getParentFrame() {
        return parentFrame;
    }

    //POPUP
    public void tryPopup(Point p) {

        if (tracks == null) {
            return;
        }

        Point pt = new Point(p.x, p.y - getOffset());

        for (Track t : tracks) {
            Map<Record, Shape> map = t.getRenderer().searchPoint(pt);
            if (map != null) {
                /**
                 * XXX: This line is here to get around what looks like a bug in
                 * the 1.6.0_20 JVM for Snow Leopard which causes the
                 * mouseExited events not to be triggered sometimes. We hide the
                 * popup before showing another. This needs to be done exactly
                 * here: after we know we have a new popup to show and before we
                 * set currentOverRecord. Otherwise, it won't work.
                 */
                PopupPanel.hidePopup();

                // Arbitrarily pick the first record in the map.  Most of the time, there will be only one.
                Record overRecord = map.keySet().iterator().next();

                Point p1 = (Point) p.clone();
                SwingUtilities.convertPointToScreen(p1, this);
                PopupPanel.showPopup(this, p1, t, overRecord);

                currentOverRecord = overRecord;
                currentOverShape = map.get(currentOverRecord);
                if (currentOverRecord instanceof ContinuousRecord) {
                    currentOverShape = ContinuousTrackRenderer.continuousRecordToEllipse(this, currentOverRecord);
                }

                repaint();
                return;
            }
        }
        // Didn't get a hit on any track.
        currentOverShape = null;
        currentOverRecord = null;
    }

    /**
     * Invoked when user selects something in the popup menu.
     *
     * @param rec
     */
    @Override
    public void recordSelected(Record rec) {
        for (Track t : tracks) {
            t.getRenderer().addToSelected(rec);
        }
        repaint();
    }

    @Override
    public void popupHidden() {
        if (currentOverShape != null) {
            currentOverShape = null;
            currentOverRecord = null;
            repaint();
        }
    }

    public void trySelect(Point p) {

        Point p_offset = new Point(p.x, p.y - getOffset());

        for (Track t : tracks) {
            Map<Record, Shape> map = t.getRenderer().searchPoint(p_offset);
            if (map != null) {
                Object[] recs = map.keySet().toArray();
                if (recs.length == 1) {
                    t.getRenderer().addToSelected((Record) recs[0]);
                } else {
                    ArrayList<Record> array = new ArrayList<Record>();
                    for (Object rec : recs) {
                        array.add((Record) rec);
                    }
                    t.getRenderer().toggleGroup(array);
                }
                repaint();
                break;
            }
        }
    }

    /**
     * Draw an informational message on top of this GraphPane.
     *
     * @param g2 the graphics to be rendered
     * @param message text of the message to be displayed
     */
    private void drawMessage(Graphics2D g2, String message) {

        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        Font font = g2.getFont();
        Font subFont = font;

        int h = getSize().height / 3;
        int w = getWidth();

        if (w > 500) {
            font = font.deriveFont(Font.PLAIN, 36);
            subFont = subFont.deriveFont(Font.PLAIN, 18);
        } else if (w > 150) {
            font = font.deriveFont(Font.PLAIN, 24);
            subFont = subFont.deriveFont(Font.PLAIN, 12);
        } else {
            font = font.deriveFont(Font.PLAIN, 12);
            subFont = subFont.deriveFont(Font.PLAIN, 8);
        }

        int returnPos = message.indexOf('\n');
        g2.setColor(ColourSettings.getColor(ColourKey.GRAPH_PANE_MESSAGE));
        if (returnPos > 0) {
            drawMessageHelper(g2, message.substring(0, returnPos), font, w, h, -(subFont.getSize() / 2));
            drawMessageHelper(g2, message.substring(returnPos + 1), subFont, w, h,
                    font.getSize() - (subFont.getSize() / 2));
        } else {
            drawMessageHelper(g2, message, font, w, h, 0);
        }
    }

    private void drawMessageHelper(Graphics2D g2, String message, Font font, int w, int h, int offset) {
        g2.setFont(font);
        FontMetrics metrics = g2.getFontMetrics();

        Rectangle2D stringBounds = font.getStringBounds(message, g2.getFontRenderContext());

        int preferredWidth = (int) stringBounds.getWidth() + metrics.getHeight();
        int preferredHeight = (int) stringBounds.getHeight() + metrics.getHeight();

        w = Math.min(preferredWidth, w);
        h = Math.min(preferredHeight, h);

        int x = (getWidth() - (int) stringBounds.getWidth()) / 2;
        int y = (getHeight() / 2) + ((metrics.getAscent() - metrics.getDescent()) / 2) + offset;

        g2.drawString(message, x, y);
    }

    /**
     * One of tracks is still loading. Instead of rendering, put up a
     * progress-bar.
     *
     * @param msg progress message to be displayed
     */
    void showProgress(String msg, double fraction) {
        if (progressPanel == null) {
            progressPanel = new ProgressPanel(new TrackCancellationListener(parentFrame));
            add(progressPanel);
        }
        progressPanel.setMessage(msg);
        progressPanel.setFraction(fraction);
        validate();
    }

    public void addExportEventListener(Listener<ExportEvent> eel) {
        synchronized (exportListeners) {
            exportListeners.add(eel);
        }
    }

    public void removeExportListener(Listener<ExportEvent> eel) {
        synchronized (exportListeners) {
            exportListeners.remove(eel);
        }
    }

    public void removeExportListeners() {
        synchronized (exportListeners) {
            exportListeners.clear();
        }
    }

    public void fireExportEvent(Range range, BufferedImage image) {
        int size = exportListeners.size();
        for (int i = 0; i < size; i++) {
            exportListeners.get(i).handleEvent(new ExportEvent(range, image));
            size = exportListeners.size(); //a listener may get removed
        }
    }

    @Override
    public final void addPopupListener(Listener<PopupEvent> pel) {
        synchronized (popupListeners) {
            popupListeners.add(pel);
        }
    }

    @Override
    public void removePopupListener(Listener<PopupEvent> eel) {
        synchronized (popupListeners) {
            popupListeners.remove(eel);
        }
    }

    @Override
    public void firePopupEvent(PopupPanel popup) {
        int size = popupListeners.size();
        for (int i = 0; i < size; i++) {
            popupListeners.get(i).handleEvent(new PopupEvent(popup));
            size = popupListeners.size(); //a listener may get removed
        }
    }

    @Override
    public void setScaledToFit(boolean value) {
        if (scaledToFit != value) {
            scaledToFit = value;
            if (value) {
                // If we have just switched to scaled-to-fit mode, we will have to reevaluate our scroll-bars.
                requestHeight(getViewportHeight());
            }
            renderRequired = true;
            repaint();
        }
    }

    @Override
    public boolean isScaledToFit() {
        return scaledToFit;
    }

    /**
     * Check to see if the frame needs to be resized. Only required if we're not
     * using scaledToFit mode, and thus have a pre-existing notion of what the
     * unit-height (or interval height) should be.
     *
     * @return true if pane needs to be resized
     */
    @Override
    public boolean needsToResize() {
        if (!scaledToFit) {
            int currentHeight = getHeight();
            int expectedHeight = (int) ((yMax - yMin) * unitHeight);

            expectedHeight = Math.max(expectedHeight, parentFrame.getFrameLandscape().getHeight());
            if (expectedHeight != currentHeight) {
                requestHeight(expectedHeight);
                return true;
            }
        }
        return false;
    }

    /**
     * Update the display of ymax. Overridden by VariantGraphPanes to show the
     */
    protected void updateYMax() {
        parentFrame.updateYMax(yMax);
    }
}