savant.view.tracks.Track.java Source code

Java tutorial

Introduction

Here is the source code for savant.view.tracks.Track.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.tracks;

import java.awt.Color;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.Point2D;
import java.text.NumberFormat;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JPanel;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import savant.api.adapter.DataSourceAdapter;
import savant.api.adapter.FrameAdapter;
import savant.api.adapter.RecordFilterAdapter;
import savant.api.adapter.TrackAdapter;
import savant.api.data.DataFormat;
import savant.api.data.Record;
import savant.api.event.DataRetrievalEvent;
import savant.api.util.DialogUtils;
import savant.api.util.Resolution;
import savant.controller.TrackController;
import savant.exception.RenderingException;
import savant.exception.SavantTrackCreationCancelledException;
import savant.plugin.SavantPanelPlugin;
import savant.selection.SelectionController;
import savant.util.*;

/**
 * Class to handle the preparation for rendering of a track. Handles colour
 * schemes and drawing instructions, getting and filtering of data, setting of
 * vertical axis, etc. The ranges associated with various resolutions are also
 * handled here, and the drawing modes are defined.
 *
 * @author mfiume
 */
public abstract class Track extends Controller<DataRetrievalEvent> implements TrackAdapter {

    private static final Log LOG = LogFactory.getLog(Track.class);
    protected static final RenderingException ZOOM_MESSAGE = new RenderingException(MiscUtils.MAC
            ? "Zoom in to see data\nTo view data at this range, change Preferences > Track Resolutions"
            : "Zoom in to see data\nTo view data at this range, change Edit > Preferences > Track Resolutions",
            RenderingException.LOWEST_PRIORITY);
    private final String name;
    private ColourScheme colourScheme;
    private List<Record> dataInRange;
    protected DrawingMode drawingMode = DrawingMode.STANDARD;
    protected final TrackRenderer renderer;
    private final DataSourceAdapter dataSource;
    private DataRetriever retriever;
    protected RecordFilterAdapter filter;
    /**
     * In practice this will be a JIDE DockableFrame, but we could conceivably
     * have a stub implementation for headless or web-based operation.
     */
    private FrameAdapter frame;

    /**
     * Constructor a new track with the given renderer.
     *
     * @param dataSource track data source; name, type, and will be derived from
     * this
     * @param renderer the <code>TrackRenderer</code> to be used for this track
     */
    protected Track(DataSourceAdapter dataSource, TrackRenderer renderer)
            throws SavantTrackCreationCancelledException {

        this.dataSource = dataSource;
        this.renderer = renderer;

        String n = getUniqueName(dataSource.getName());

        if (n == null) {
            throw new SavantTrackCreationCancelledException();
        }

        name = n;

        renderer.setTrackName(name);
        addListener(renderer);
    }

    @Override
    public String toString() {
        return name;
    }

    private String getUniqueName(String name) {
        String result = name;
        while (TrackController.getInstance().containsTrack(result)) {
            result = DialogUtils.displayInputMessage("Duplicate Track",
                    "A track with that name already exists. Please enter a new name:", result);
            if (result == null) {
                return null;
            }
        }
        return result;
    }

    /**
     * Get the current colour scheme.
     *
     * @return ColourScheme
     */
    public ColourScheme getColourScheme() {
        if (colourScheme == null) {
            colourScheme = getDefaultColourScheme();
        }
        return colourScheme;
    }

    /**
     * Set individual colour.
     *
     * @param key one of Savant's standard colour keys
     * @param color new color
     */
    public void setColor(ColourKey key, Color color) {
        getColourScheme().setColor(key, color);
    }

    public abstract ColourScheme getDefaultColourScheme();

    /**
     * Get the name of this track. Usually constructed from the file name.
     *
     * @return track name
     */
    @Override
    public String getName() {
        return name;
    }

    /**
     * Get the data currently being displayed (or ready to be displayed)
     *
     * @return List of data objects
     */
    @Override
    public List<Record> getDataInRange() {
        return dataInRange;
    }

    @Override
    public List<Record> getSelectedDataInRange() {
        return SelectionController.getInstance().getSelections(getName());
    }

    /**
     * Get current draw mode
     *
     * @return draw mode as Mode
     */
    @Override
    public DrawingMode getDrawingMode() {
        return drawingMode;
    }

    /**
     * Get all valid draw modes for this track.
     *
     * @return List of draw Modes
     */
    @Override
    public DrawingMode[] getValidDrawingModes() {
        return new DrawingMode[] { DrawingMode.STANDARD };
    }

    /**
     * Set the current draw mode.
     *
     * @param mode
     */
    @Override
    public void setDrawingMode(DrawingMode mode) {
        drawingMode = mode;
        frame.drawModeChanged(this);
    }

    /**
     * Get the record (data) track associated with this view track (if any.)
     *
     * @return Record Track or null (in the case of a genome.)
     */
    @Override
    public DataSourceAdapter getDataSource() {
        return dataSource;
    }

    /**
     * Convenience method to get a track's data format.
     *
     * @return the DataFormat of the track's DataSource
     */
    @Override
    public DataFormat getDataFormat() {
        return dataSource.getDataFormat();
    }

    /**
     * Utility method to determine whether this track has data for the given
     * reference.
     *
     * @param ref the reference to be checked
     * @return true if the track has data for ref
     */
    public boolean containsReference(String ref) {
        return dataSource.getReferenceNames().contains(ref)
                || dataSource.getReferenceNames().contains(MiscUtils.homogenizeSequence(ref));
    }

    /**
     * Retrieve a JPanel for the layer which plugins can use to draw on top of
     * the track, creating one if necessary.
     *
     * @return component to draw onto (guaranteed to be non-null if called after <code>TrackEvent.OPENED<code> notification has been received)
     * @since 1.6.0
     */
    @Override
    public JPanel getLayerCanvas(SavantPanelPlugin plugin) {
        return frame.getLayerCanvas(plugin, true);
    }

    /**
     * Create a JPanel for the layer which a plugin can use to draw on top of
     * the track.
     *
     * @return component to draw onto or null if frame not initialized yet
     * @deprecated Renamed to <code>createLayerCanvas()</code>.
     */
    @Override
    public JPanel getLayerCanvas() {
        return getLayerCanvas(null);
    }

    /**
     * For use by plugins. Scale a pixel position along the x-axis into a base
     * position.
     *
     * @since 1.6.0
     */
    @Override
    public int transformXPixel(double pix) {
        return frame.getGraphPane().transformXPixel(pix);
    }

    /**
     * For use by plugins. Scale a position in bases into a pixel position along
     * the x-axis.
     *
     * @since 1.6.0
     */
    @Override
    public double transformXPos(int pos) {
        return frame.getGraphPane().transformXPos(pos);
    }

    /**
     * For use by plugins. Scale a pixel position along the y-axis into a
     * logical vertical position.
     *
     * @since 1.6.0
     */
    @Override
    public double transformYPixel(double pix) {
        return frame.getGraphPane().transformYPixel(pix);
    }

    /**
     * For use by plugins. Scale a logical vertical position into a pixel
     * position along the y-axis.
     *
     * @since 1.6.0
     */
    @Override
    public double transformYPos(double pos) {
        return frame.getGraphPane().transformYPos(pos);
    }

    /**
     * Given a record, determine the bounds which would be used for displaying
     * that record.
     *
     * @param rec the record whose bounds we're interested in
     * @return the record's bounds in pixels, relative to the track's bounds (or
     * null
     */
    @Override
    public Rectangle getRecordBounds(Record rec) {
        Shape s = renderer.recordToShapeMap.get(rec);
        if (s != null) {
            return s.getBounds();
        }
        return null;
    }

    /**
     * Given a location within a track window, determine the record which lies
     * at that location. If multiple records overlap at the given position, only
     * the first one will be returned.
     *
     * @param pt the point we're interested in
     * @return the record at that position, or <code>null</code> if no record is
     * there
     */
    @Override
    public Record getRecordAtPos(Point pt) {
        for (Record r : renderer.recordToShapeMap.keySet()) {
            Shape s = renderer.recordToShapeMap.get(r);
            if (s.contains(new Point2D.Double(pt.x, pt.y))) {
                return r;
            }
        }
        return null;
    }

    public FrameAdapter getFrame() {
        return frame;
    }

    public void setFrame(FrameAdapter f, DrawingMode initialMode) {
        frame = f;
        if (initialMode != null) {
            drawingMode = initialMode;
        }
        addListener(frame);
    }

    /**
     * Retrieve the renderer associated with this track.
     *
     * @return the track's renderer
     */
    public TrackRenderer getRenderer() {
        return renderer;
    }

    /**
     * Prepare this track to render the given range. Since the actual
     * data-retrieval is now done on a separate thread, preparing to render
     * should not throw any exceptions.
     *
     * @param ref the reference to be rendered
     * @param r the range to be rendered
     */
    public abstract void prepareForRendering(String ref, Range r);

    /**
     * Method which plugins can use to force the Track to repaint itself.
     */
    @Override
    public void repaint() {
        frame.getGraphPane().setRenderForced();
        frame.getGraphPane().repaint();
    }

    /**
     * Like repaint(), but doesn't force a re-render. Intended for updating the
     * track's selection.
     */
    public void repaintSelection() {
        frame.getGraphPane().repaint();
    }

    @Override
    public boolean isSelectionAllowed() {
        return renderer.selectionAllowed(false);
    }

    /**
     * All ordinary tracks have integer markings along their x axes.
     *
     * @param res the resolution to be considered (ignored)
     * @return <code>AxisType.INTEGER</code>
     */
    @Override
    public AxisType getXAxisType(Resolution res) {
        return AxisType.INTEGER;
    }

    /**
     * A number of common track types (Sequence, Point, RichInterval) have no
     * y-axis, so they all share this implementation.
     *
     * @param res the resolution to be considered (ignored)
     * @return <code>AxisType.NONE</code>
     */
    @Override
    public AxisType getYAxisType(Resolution res) {
        return AxisType.NONE;
    }

    /**
     * Request data from the underlying data track at the current resolution. A
     * new thread will be started.
     *
     * @param reference The reference within which to retrieve objects
     * @param range The range within which to retrieve objects
     */
    public void requestData(String reference, Range range) {
        if (retriever != null) {
            if (retriever.reference.equals(reference) && retriever.range.equals(range)) {
                LOG.debug("Nothing to request, already busy retrieving " + reference + ":" + range);
                return;
            } else {
                LOG.debug("You're wasting your time on " + reference + ":" + range);
            }
        }
        dataInRange = null;
        fireEvent(new DataRetrievalEvent(this, range));

        retriever = new DataRetriever(reference, range, filter);
        retriever.start();

        try {
            if (retriever != null) {
                retriever.join(1000);
                if (retriever != null && retriever.isAlive()) {
                    // Join timed out, but we are still waiting for data to arrive.
                    LOG.trace("Join timed out, putting up progress-bar.");
                }
            }
        } catch (InterruptedException ix) {
            LOG.error("DataRetriever interrupted during join.", ix);
            retriever = null;
        }
    }

    /**
     * Fires a DataSource successful completion event. It will be posted to the
     * AWT event-queue thread, so that UI code can function properly.
     */
    private void fireDataRetrievalCompleted(final Range r) {
        MiscUtils.invokeLaterIfNecessary(new Runnable() {
            @Override
            public void run() {
                fireEvent(new DataRetrievalEvent(Track.this, dataInRange, r));
            }
        });
    }

    /**
     * Fires a DataSource error event. It will be posted to the AWT event-queue
     * thread, so that UI code can function properly.
     */
    private void fireDataRetrievalFailed(final Throwable x, final Range r) {
        MiscUtils.invokeLaterIfNecessary(new Runnable() {
            @Override
            public void run() {
                fireEvent(new DataRetrievalEvent(Track.this, x, r));
            }
        });
    }

    /**
     * Cancel an in-progress request to retrieve data.
     */
    public void cancelDataRequest() {
        if (retriever != null) {
            retriever.interrupt(); // Will fire fireDataRetrievalFailed when InterruptedException is caught.
        }
    }

    /**
     * Store null to dataInRange. This implicitly means that data-retrieval is
     * considered to have completed without error.
     *
     * @throws Exception
     */
    public void saveNullData(Range r) {
        dataInRange = null;
        fireDataRetrievalCompleted(r);
    }

    /**
     * Retrieve data from the underlying data source. The default behaviour is
     * just to call getRecords on the track's data source.
     *
     * @param r The range within which to retrieve objects
     * @param res The resolution at which to get data
     * @return a List of data objects from the given range and resolution
     * @throws Exception
     */
    protected synchronized List<Record> retrieveData(String ref, Range r, Resolution res,
            RecordFilterAdapter filter) throws Exception {
        return getDataSource().getRecords(ref, r, res, filter);
    }

    private static class MemoryMonitor extends Thread {

        private final Thread thread;
        private int MEMORY_LIMIT = 20; // in mb
        private boolean warned;

        public MemoryMonitor(DataRetriever r) {
            this.thread = r;
        }

        public boolean didWarn() {
            return warned;
        }

        @Override
        public void run() {

            Runtime runtime = Runtime.getRuntime();
            NumberFormat format = NumberFormat.getInstance();

            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    return;
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException ex) {
                    return;
                }

                long freeMemory = runtime.freeMemory() / 1024 * 1024; // in mb
                if (freeMemory < MEMORY_LIMIT) {
                    warned = true;
                    thread.interrupt();
                    return;
                }
            }
        }
    }

    private class DataRetriever extends Thread {

        String reference;
        Range range;
        RecordFilterAdapter filter;
        private final MemoryMonitor memoryMonitor;

        DataRetriever(String ref, Range r, RecordFilterAdapter filt) {
            super("DataRetriever-" + ref + ":" + r);
            reference = ref;
            range = r;
            filter = filt;
            memoryMonitor = new MemoryMonitor(this);
        }

        @Override
        public void run() {

            try {
                memoryMonitor.start();
                LOG.debug("Retrieving data for " + name + "(" + reference + ":" + range + ")");
                dataInRange = retrieveData(reference, range, getResolution(range), filter);
                if (isInterrupted()) {
                    LOG.info(name + " was interrupted.");
                } else {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Retrieved " + (dataInRange != null ? Integer.toString(dataInRange.size()) : "no")
                                + " records for " + name + "(" + reference + ":" + range + ")");
                    }
                    fireDataRetrievalCompleted(range);
                }
            } catch (InterruptedException x) {
                if (memoryMonitor.didWarn()) {
                    fireDataRetrievalFailed(new Exception("Retrieval stopped due to memory warning"), range);
                } else {
                    fireDataRetrievalFailed(new Exception("Data retrieval cancelled"), range);
                }
            } catch (Throwable x) {
                if (NetworkUtils.isStreamCached(dataSource.getURI())) {
                    LOG.info("Cached read failed for " + getName() + " with " + MiscUtils.getMessage(x)
                            + "; deleting cache file and retrying.");
                    try {
                        RemoteFileCache.removeCacheEntry(dataSource.getURI().toString());
                        dataInRange = retrieveData(reference, range, getResolution(range), filter);
                        fireDataRetrievalCompleted(range);
                    } catch (Throwable x2) {
                        LOG.error("Data retrieval failed twice.", x2);
                        fireDataRetrievalFailed(x2, range);
                    }
                } else {
                    LOG.error("Data retrieval failed.", x);
                    fireDataRetrievalFailed(x, range);
                }
            }
            memoryMonitor.interrupt();

            retriever = null;
        }
    }
}