savant.view.tracks.TrackRenderer.java Source code

Java tutorial

Introduction

Here is the source code for savant.view.tracks.TrackRenderer.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.*;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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

import savant.api.adapter.GraphPaneAdapter;
import savant.api.data.Record;
import savant.api.event.DataRetrievalEvent;
import savant.api.util.Listener;
import savant.api.util.Resolution;
import savant.controller.LocationController;
import savant.exception.RenderingException;
import savant.selection.SelectionController;
import savant.util.AxisRange;
import savant.util.ColourKey;
import savant.util.ColourScheme;
import savant.util.DrawingInstruction;
import savant.util.DrawingMode;
import savant.util.MiscUtils;

/**
 *
 * @author mfiume, AndrewBrook, tarkvara
 */
public abstract class TrackRenderer implements Listener<DataRetrievalEvent> {
    private static final Log LOG = LogFactory.getLog(TrackRenderer.class);

    private static final int MIN_TRANSPARENCY = 20;
    private static final int MAX_TRANSPARENCY = 255;

    protected static final Font LEGEND_FONT = new Font("Sans-Serif", Font.PLAIN, 10);
    protected static final Stroke ONE_STROKE = new BasicStroke(1.0f);
    protected static final Stroke TWO_STROKE = new BasicStroke(2.0f);

    /** Size of colour swatch used in legend for bases. */
    protected static final Dimension SWATCH_SIZE = new Dimension(6, 13);

    /** Notional line-height in legends. */
    protected static final int LEGEND_LINE_HEIGHT = 18;

    protected List<Record> data;
    protected final EnumMap<DrawingInstruction, Object> instructions = new EnumMap<DrawingInstruction, Object>(
            DrawingInstruction.class);
    protected String trackName;

    protected Map<Record, Shape> recordToShapeMap = new HashMap<Record, Shape>();
    protected Map<Record, Shape> artifactMap = new HashMap<Record, Shape>(); //meta info pointing to reads (ie. lines in read pair mode)

    protected TrackRenderer() {
    }

    /**
     * Set the track's name.  This is used only to provide a key when dealing with the SelectionController.
     *
     * @param name
     */
    public void setTrackName(String name) {
        trackName = name;
    }

    /**
     * Sets the data to null so we know that there's nothing to render.
     *
     * @param evt describes the data being received
     */
    @Override
    public void handleEvent(DataRetrievalEvent evt) {
        switch (evt.getType()) {
        case STARTED:
            // Sets the data to null so we know that there's nothing to render.
            data = null;
            break;
        case COMPLETED:
            // Default handler just sets the renderer to have the newly-received data.
            LOG.debug("TrackRenderer received dataRetrievalCompleted, removing PROGRESS.");
            instructions.remove(DrawingInstruction.PROGRESS);
            data = evt.getData();
            break;
        case FAILED:
            // Data retrieval has failed for some reason.
            instructions.remove(DrawingInstruction.PROGRESS);
            instructions.put(DrawingInstruction.ERROR, new RenderingException(MiscUtils.getMessage(evt.getError()),
                    RenderingException.ERROR_PRIORITY));
            break;
        }
    }

    public boolean isWaitingForData() {
        return data == null && instructions.containsKey(DrawingInstruction.PROGRESS);
    }

    public void addInstruction(DrawingInstruction key, Object value) {
        if (key == DrawingInstruction.AXIS_RANGE) {
            //System.out.println("Setting y axis range to " + ((AxisRange)value).getYRange());
        }
        instructions.put(key, value);
    }

    /**
     * Retrieve the value of a specific drawing instruction for this renderer.
     *
     * @param key key identifying the drawing instruction to be retrieved
     * @return the value of that drawing instruction
     */
    public Object getInstruction(DrawingInstruction key) {
        return instructions.get(key);
    }

    /**
     * Remove all instructions to allow for a fresh render.
     */
    public void clearInstructions() {
        instructions.clear();
    }

    public abstract void render(Graphics2D g2, GraphPaneAdapter gp) throws RenderingException;

    /**
     * Before doing any actual rendering, derived classes should call this in their
     * render() methods in case we want to display a message instead.
     *
     * If graphPane is given and error is thrown, make sure graphPane is resized if necessary.
     *
     * @throws RenderingException
     */
    protected void renderPreCheck() throws RenderingException {

        // Clear away any shapes.
        recordToShapeMap.clear();
        artifactMap.clear();

        Boolean refExists = (Boolean) instructions.get(DrawingInstruction.REFERENCE_EXISTS);
        if (refExists == null || !refExists) {
            throw new RenderingException("No data for " + LocationController.getInstance().getReferenceName(),
                    RenderingException.INFO_PRIORITY);
        }
        RenderingException error = (RenderingException) instructions.get(DrawingInstruction.ERROR);
        if (error != null) {
            throw error;
        }
        if (data == null || data.isEmpty()) {
            throw new RenderingException("No data in range", RenderingException.INFO_PRIORITY);
        }
    }

    /**
     * Check whether to perform selection for this track.
     *
     * @param checkRes if true, return true only if resolution is HIGH
     * @return whether or not to allow selection at this time
     */
    public boolean selectionAllowed(boolean checkRes) {
        Object instr_select = instructions.get(DrawingInstruction.SELECTION_ALLOWED);
        if (instr_select == null || instr_select.equals(false))
            return false;
        if (checkRes) {
            Object instr_res = instructions.get(DrawingInstruction.RESOLUTION);
            if (instr_res == null || !instr_res.equals(Resolution.HIGH))
                return false;
        }
        return true;
    }

    public boolean hasMappedValues() {
        return !recordToShapeMap.isEmpty();
    }

    public Map<Record, Shape> searchPoint(Point p) {

        if (!hasMappedValues() || data == null)
            return null;

        DrawingMode mode = (DrawingMode) instructions.get(DrawingInstruction.MODE);

        Map<Record, Shape> map = new HashMap<Record, Shape>();
        boolean allowFuzzySNPs = true;

        Rectangle2D testIntersection = new Rectangle2D.Double(p.x - 3, p.y - 3, 7, 7);
        for (Record rec : recordToShapeMap.keySet()) {
            Shape s = recordToShapeMap.get(rec);

            if (s != null) {
                //if (contains AND (notArc OR (isEdge...))
                boolean hit = false;
                if (mode == DrawingMode.ARC || mode == DrawingMode.ARC_PAIRED) {
                    hit = s.intersects(testIntersection)
                            && (!s.contains(p.x - 3, p.y - 3) || !s.contains(p.x + 3, p.y - 3));
                } else {
                    hit = s.contains(p);
                }
                // At low resolutions, SNPs can be hard to hit with the mouse, so give a second chance with a fuzzier check.
                if (mode == DrawingMode.SNP || mode == DrawingMode.STRAND_SNP || mode == DrawingMode.MATRIX) {
                    if (hit) {
                        if (allowFuzzySNPs) {
                            // We may have accumulated some fuzzy SNP hits.  We now have an exact one, so dump the fuzzies.
                            map.clear();
                            allowFuzzySNPs = false;
                        }
                    } else {
                        if (allowFuzzySNPs) {
                            hit = s.intersects(testIntersection);
                        }
                    }
                }
                if (hit) {
                    map.put(rec, s);
                    continue;
                }
            } else {
                LOG.info("Why is shape null for " + rec);
            }

            //check other artifacts
            Shape artifact = artifactMap.get(rec);
            if (artifact != null && artifact.contains(p.x, p.y)) {
                map.put(rec, s);
            }
        }
        return map.isEmpty() ? null : map;
    }

    public boolean rectangleSelect(Rectangle2D rect) {

        if (!selectionAllowed(false) || !hasMappedValues())
            return false;

        boolean repaint = false;
        List<Record> toAdd = new ArrayList<Record>();

        for (Record o : recordToShapeMap.keySet()) {
            Shape s = recordToShapeMap.get(o);
            if (s == null)
                continue;
            if (s.intersects(rect)) {
                toAdd.add(o);
                repaint = true;
            }
        }

        if (repaint) {
            SelectionController.getInstance().addMultipleSelections(trackName, toAdd);
        }

        return repaint;
    }

    public void addToSelected(Record rec) {
        if (selectionAllowed(false)) {
            SelectionController.getInstance().toggleSelection(trackName, rec);
        }
    }

    /**
     * Invoked when the user chooses "Select Pair" from the Bam popup menu.
     * @param i record to be selected
     */
    public void forceAddToSelected(Record i) {
        SelectionController.getInstance().addSelection(trackName, i);
    }

    /**
     * Current selected shapes.
     */
    public List<Shape> getCurrentSelectedShapes(GraphPaneAdapter gp) {
        List<Shape> shapes = new ArrayList<Shape>();
        List<Record> currentSelected = SelectionController.getInstance().getSelectedFromList(trackName,
                LocationController.getInstance().getRange(), data);
        for (int i = 0; i < currentSelected.size(); i++) {
            Shape s = recordToShapeMap.get(currentSelected.get(i));
            if (s != null) {
                shapes.add(s);
            }
        }
        return shapes;
    }

    /**
     * For practical reasons, we never want alpha to be less than 20 or more than 255.
     */
    public static int getConstrainedAlpha(int alpha) {
        return alpha < MIN_TRANSPARENCY ? MIN_TRANSPARENCY : (alpha > MAX_TRANSPARENCY ? MAX_TRANSPARENCY : alpha);
    }

    public void toggleGroup(ArrayList<Record> recs) {
        if (selectionAllowed(false)) {
            SelectionController.getInstance().toggleGroup(trackName, recs);
        }
    }

    public void drawFeatureLabel(Graphics2D g2, String geneName, double startXPos, double y) {
        FontMetrics fm = g2.getFontMetrics();
        double stringstartx = startXPos - fm.stringWidth(geneName) - 5;

        if (stringstartx <= 0) {
            Rectangle2D r = fm.getStringBounds(geneName, g2);

            int b = 2;
            Color textColor = g2.getColor();
            g2.setColor(new Color(255, 255, 255, 200));
            g2.fill(new RoundRectangle2D.Double(3.0, y - (fm.getHeight() - fm.getDescent()) - b,
                    r.getWidth() + 2 * b, r.getHeight() + 2 * b, 8.0, 8.0));
            g2.setColor(textColor);
            g2.drawString(geneName, 5.0F, (float) y);
        } else {
            g2.drawString(geneName, (float) stringstartx, (float) y);
        }
    }

    /**
     * Shared by BAMTrackRenderer and RichIntervalTrackRenderer to draw the white diamond
     * which indicates an insertion.
     */
    public Shape drawInsertion(Graphics2D g2, double x, double y, double unitWidth, double unitHeight) {

        ColourScheme cs = (ColourScheme) instructions.get(DrawingInstruction.COLOUR_SCHEME);
        g2.setColor(cs.getColor(ColourKey.INSERTED_BASE));
        double w = unitWidth * 0.5;

        Path2D.Double rhombus = new Path2D.Double();
        rhombus.moveTo(x, y);
        rhombus.lineTo(x + w, y + unitHeight * 0.5);
        rhombus.lineTo(x, y + unitHeight);
        rhombus.lineTo(x - w, y + unitHeight * 0.5);
        rhombus.closePath();
        g2.fill(rhombus);

        return rhombus;
    }

    /**
     * Return the dimensions of the legend which this renderer currently requires.
     * If the derived renderer doesn't have a legend, it should return null.
     */
    public Dimension getLegendSize(DrawingMode mode) {
        return null;
    }

    /**
     * Draw the actual legend.
     */
    public void drawLegend(Graphics2D g2, DrawingMode mode) {
    }

    /**
     * Simplest kind of legend is just a list of coloured lines with names next to them.
     */
    protected void drawSimpleLegend(Graphics2D g2, int x, int y, ColourKey... keys) {
        ColourScheme cs = (ColourScheme) instructions.get(DrawingInstruction.COLOUR_SCHEME);

        g2.setFont(LEGEND_FONT);

        for (ColourKey k : keys) {
            String legendString = k.getName();
            g2.setColor(cs.getColor(k));
            g2.setStroke(TWO_STROKE);
            Rectangle2D stringRect = LEGEND_FONT.getStringBounds(legendString, g2.getFontRenderContext());
            g2.drawLine(x - 25, y - (int) stringRect.getHeight() / 2, x - 5, y - (int) stringRect.getHeight() / 2);
            g2.setColor(cs.getColor(ColourKey.INTERVAL_LINE));
            g2.setStroke(ONE_STROKE);
            g2.drawString(legendString, x, y);

            y += LEGEND_LINE_HEIGHT;
        }
    }

    /**
     * Draw a legend which consists of the given bases arranged horizontally
     */
    protected void drawBaseLegend(Graphics2D g2, int x, int y, ColourKey... keys) {
        ColourScheme cs = (ColourScheme) instructions.get(DrawingInstruction.COLOUR_SCHEME);
        g2.setFont(LEGEND_FONT);

        for (ColourKey k : keys) {
            g2.setColor(cs.getColor(k));
            g2.fillRect(x, y - SWATCH_SIZE.height + 2, SWATCH_SIZE.width, SWATCH_SIZE.height);
            g2.setColor(Color.BLACK);
            g2.drawString(k.getName(), x + SWATCH_SIZE.width + 3, y);
            x += 27;
        }
    }
}