/*
* The MIT License (MIT)
*
* Copyright (c) 2007-2015 Broad Institute
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.broad.igv.ui;
import org.broad.igv.feature.IExon;
import org.broad.igv.prefs.Constants;
import org.broad.igv.prefs.PreferencesManager;
import org.broad.igv.renderer.SashimiJunctionRenderer;
import org.broad.igv.sam.*;
import org.broad.igv.track.*;
import org.broad.igv.ui.color.ColorPalette;
import org.broad.igv.ui.color.ColorUtilities;
import org.broad.igv.event.IGVEventBus;
import org.broad.igv.event.IGVEventObserver;
import org.broad.igv.event.ViewChange;
import org.broad.igv.ui.panel.*;
import org.broad.igv.ui.util.UIUtilities;
import javax.swing.*;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Rectangle2D;
import java.io.File;
import java.util.*;
import java.util.List;
/**
* Window for displaying sashimi style junction plot
* See http://genes.mit.edu/burgelab/miso/docs/sashimi.html
* <p/>
* User: jacob
* Date: 2013-Jan-11
*/
public class SashimiPlot extends JFrame implements IGVEventObserver {
private List<SpliceJunctionTrack> spliceJunctionTracks;
private ReferenceFrame frame;
private IGVEventBus eventBus;
/**
* The minimum allowed origin of the frame. We set scrolling
* limits based on initialization
*/
private final double minOrigin;
/**
* The maximum allow end of the frame. We set scrolling
* limits based on initialization
*/
private final double maxEnd;
private static final List<Color> plotColors;
static {
ColorPalette palette = ColorUtilities.getDefaultPalette();
plotColors = Arrays.asList(palette.getColors());
}
public SashimiPlot(ReferenceFrame iframe, Collection<? extends AlignmentTrack> alignmentTracks, FeatureTrack geneTrack) {
getGlassPane().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
int minJunctionCoverage = PreferencesManager.getPreferences().getAsInt(Constants.SAM_JUNCTION_MIN_COVERAGE);
this.eventBus = new IGVEventBus();
this.frame = new ReferenceFrame(iframe, eventBus);
minOrigin = this.frame.getOrigin();
maxEnd = this.frame.getEnd();
initSize(frame.getWidthInPixels());
BoxLayout boxLayout = new BoxLayout(getContentPane(), BoxLayout.Y_AXIS);
getContentPane().setLayout(boxLayout);
//Add control elements to the top
getContentPane().add(generateControlPanel(this.frame));
spliceJunctionTracks = new ArrayList<>(alignmentTracks.size());
int colorInd = 0;
eventBus.subscribe(ViewChange.class, this);
for (AlignmentTrack alignmentTrack : alignmentTracks) {
//AlignmentDataManager oldDataManager = alignmentTrack.getDataManager();
AlignmentDataManager dataManager = alignmentTrack.getDataManager();
SpliceJunctionTrack spliceJunctionTrack =
new SpliceJunctionTrack(alignmentTrack.getResourceLocator(), alignmentTrack.getName(), dataManager, null, SpliceJunctionTrack.StrandOption.COMBINE);
// Override expand/collpase setting -- expanded sashimi plots make no sense
spliceJunctionTrack.setDisplayMode(Track.DisplayMode.COLLAPSED);
spliceJunctionTrack.setRendererClass(SashimiJunctionRenderer.class);
Color color = plotColors.get(colorInd);
colorInd = (colorInd + 1) % plotColors.size();
spliceJunctionTrack.setColor(color);
TrackComponent<SpliceJunctionTrack> trackComponent = new TrackComponent<SpliceJunctionTrack>(frame, spliceJunctionTrack);
trackComponent.originalFrame = iframe;
initSpliceJunctionComponent(trackComponent, dataManager,dataManager.getCoverageTrack(), minJunctionCoverage);
getContentPane().add(trackComponent);
spliceJunctionTracks.add(spliceJunctionTrack);
spliceJunctionTrack.load(iframe); // <= Must "load" tracks with frame of alignment track (actually just fetches from cache)
}
Axis axis = createAxis(frame);
getContentPane().add(axis);
SelectableFeatureTrack geneTrackClone = new SelectableFeatureTrack(geneTrack);
TrackComponent<SelectableFeatureTrack> geneComponent = new TrackComponent<SelectableFeatureTrack>(frame, geneTrackClone);
getContentPane().add(geneComponent);
initGeneComponent(frame.getWidthInPixels(), geneComponent, geneTrackClone);
validate();
}
private Component generateControlPanel(ReferenceFrame frame) {
JPanel controlPanel = new JPanel();
ZoomSliderPanel zoomSliderPanel = new ZoomSliderPanel(frame);
zoomSliderPanel.setMinZoomLevel(frame.getZoom());
zoomSliderPanel.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
SashimiPlot.this.repaint();
}
});
Dimension controlSize = new Dimension(200, 30);
controlPanel.add(zoomSliderPanel);
setFixedSize(zoomSliderPanel, controlSize);
Dimension panelSize = controlSize;
setFixedSize(controlPanel, panelSize);
BoxLayout layout = new BoxLayout(controlPanel, BoxLayout.X_AXIS);
controlPanel.setLayout(layout);
return controlPanel;
}
private static void setFixedSize(Component component, Dimension dimension) {
component.setPreferredSize(dimension);
component.setMinimumSize(dimension);
component.setMaximumSize(dimension);
}
private void initSize(int width) {
setSize(width, 500);
}
private Axis createAxis(ReferenceFrame frame) {
Axis axis = new Axis(frame);
Dimension maxDim = new Dimension(Integer.MAX_VALUE, 25);
axis.setMaximumSize(maxDim);
Dimension prefDim = new Dimension(maxDim);
prefDim.setSize(frame.getWidthInPixels(), prefDim.height);
axis.setPreferredSize(prefDim);
return axis;
}
private void initGeneComponent(int prefWidth, TrackComponent<SelectableFeatureTrack> geneComponent, FeatureTrack geneTrack) {
geneTrack.setDisplayMode(Track.DisplayMode.SQUISHED);
geneTrack.clearPackedFeatures();
RenderContext context = new RenderContext(geneComponent, null, frame, null);
geneTrack.load(context.getReferenceFrame());
Dimension maxGeneDim = new Dimension(Integer.MAX_VALUE, geneTrack.getNumberOfFeatureLevels() * geneTrack.getSquishedRowHeight() + 10);
geneComponent.setMaximumSize(maxGeneDim);
Dimension prefGeneDim = new Dimension(maxGeneDim);
prefGeneDim.setSize(prefWidth, prefGeneDim.height);
geneComponent.setPreferredSize(prefGeneDim);
GeneTrackMouseAdapter ad2 = new GeneTrackMouseAdapter(geneComponent);
geneComponent.addMouseListener(ad2);
geneComponent.addMouseMotionListener(ad2);
}
private void initSpliceJunctionComponent(TrackComponent<SpliceJunctionTrack> trackComponent, AlignmentDataManager dataManager, CoverageTrack coverageTrack, int minJunctionCoverage) {
JunctionTrackMouseAdapter ad1 = new JunctionTrackMouseAdapter(trackComponent);
trackComponent.addMouseListener(ad1);
trackComponent.addMouseMotionListener(ad1);
getRenderer(trackComponent.track).setDataManager(dataManager);
getRenderer(trackComponent.track).setCoverageTrack(coverageTrack);
getRenderer(trackComponent.track).getCoverageTrack().rescale(trackComponent.originalFrame);
dataManager.setMinJunctionCoverage(minJunctionCoverage);
getRenderer(trackComponent.track).setBackground(getBackground());
}
private SashimiJunctionRenderer getRenderer(SpliceJunctionTrack spliceJunctionTrack) {
return (SashimiJunctionRenderer) spliceJunctionTrack.getRenderer();
}
@Override
public void receiveEvent(Object event) {
repaint();
}
/**
* Should consider using this elsewhere. Single component
* which contains a single track
*/
private static class TrackComponent<T extends Track> extends JComponent {
private T track;
private ReferenceFrame frame;
private String toolTipText = null;
public ReferenceFrame originalFrame;
public TrackComponent(ReferenceFrame frame, T track) {
this.frame = frame;
this.track = track;
}
public void updateToolTipText(TrackClickEvent tce) {
toolTipText = track.getValueStringAt(tce.getFrame().getChrName(), tce.getChromosomePosition(), tce.getMouseEvent().getX(), tce.getMouseEvent().getY(), tce.getFrame());
toolTipText = "<html>" + toolTipText;
setToolTipText(toolTipText);
}
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Rectangle visibleRect = getVisibleRect();
RenderContext context = new RenderContext(this, (Graphics2D) g, frame, visibleRect);
track.render(context, visibleRect);
}
}
/**
* Set the minimum junction coverage, per trac,k and is not persistent
* <p/>
* Our "Set Max Junction Coverage Range" just changes the view scaling, it doesn't
* filter anything, which is different behavior than the minimum. This might be confusing.
*
* @param trackComponent
* @param newMinJunctionCoverage
*/
private void setMinJunctionCoverage(TrackComponent<SpliceJunctionTrack> trackComponent, int newMinJunctionCoverage) {
AlignmentDataManager dataManager = getRenderer(trackComponent.track).getDataManager();
dataManager.setMinJunctionCoverage(newMinJunctionCoverage);
trackComponent.track.clear();
trackComponent.repaint();
}
/**
* Set the max coverage depth, which is a graphical scaling parameter for determining how
* thick the junction arcs will be
*
* @param trackComponent
* @param newMaxDepth
*/
private void setMaxCoverageDepth(TrackComponent<SpliceJunctionTrack> trackComponent, int newMaxDepth) {
getRenderer(trackComponent.track).setMaxDepth(newMaxDepth);
repaint();
}
private class JunctionTrackMouseAdapter extends TrackComponentMouseAdapter<SpliceJunctionTrack> {
JunctionTrackMouseAdapter(TrackComponent<SpliceJunctionTrack> trackComponent) {
super(trackComponent);
}
@Override
protected void handleDataClick(MouseEvent e) {
//Show data of some sort?
}
@Override
protected IGVPopupMenu getPopupMenu(MouseEvent e) {
IGVPopupMenu menu = new IGVPopupMenu();
final JCheckBoxMenuItem showCoverageData = new JCheckBoxMenuItem("Show Exon Coverage Data");
showCoverageData.setSelected(PreferencesManager.getPreferences().getAsBoolean(Constants.SASHIMI_SHOW_COVERAGE));
showCoverageData.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
PreferencesManager.getPreferences().put(Constants.SASHIMI_SHOW_COVERAGE, showCoverageData.isSelected());
SashimiPlot.this.repaint();
}
});
CoverageTrack covTrack = getRenderer(this.trackComponent.track).getCoverageTrack();
JMenuItem setCoverageDataRange = CoverageTrack.addDataRangeItem(SashimiPlot.this, null, Arrays.asList(covTrack));
setCoverageDataRange.setText("Set Exon Coverage Max");
JMenuItem maxJunctionCoverageRange = new JMenuItem("Set Junction Coverage Max");
maxJunctionCoverageRange.setToolTipText("The thickness of each line will be proportional to the coverage, up until this value");
maxJunctionCoverageRange.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String input = JOptionPane.showInputDialog("Set Max Junction Coverage", getRenderer(trackComponent.track).getMaxDepth());
if (input == null || input.length() == 0) return;
try {
int newMaxDepth = Integer.parseInt(input);
setMaxCoverageDepth(JunctionTrackMouseAdapter.this.trackComponent, newMaxDepth);
} catch (NumberFormatException ex) {
JOptionPane.showMessageDialog(SashimiPlot.this, input + " is not an integer");
}
}
});
JMenuItem minJunctionCoverage = new JMenuItem("Set Junction Coverage Min");
minJunctionCoverage.setToolTipText("Junctions below this threshold will be removed from view");
minJunctionCoverage.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
AlignmentDataManager dataManager = getRenderer(trackComponent.track).getDataManager();
SpliceJunctionHelper.LoadOptions loadOptions = dataManager.getSpliceJunctionLoadOptions();
String input = JOptionPane.showInputDialog("Set Minimum Junction Coverage", loadOptions.minJunctionCoverage);
if (input == null || input.length() == 0) return;
try {
int newMinJunctionCoverage = Integer.parseInt(input);
setMinJunctionCoverage(JunctionTrackMouseAdapter.this.trackComponent, newMinJunctionCoverage);
} catch (NumberFormatException ex) {
JOptionPane.showMessageDialog(SashimiPlot.this, input + " is not an integer");
}
}
});
JMenuItem colorItem = new JMenuItem("Set Color");
colorItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
Color color = UIUtilities.showColorChooserDialog(
"Select Track Color", trackComponent.track.getColor());
SashimiPlot.this.toFront();
if (color == null) return;
trackComponent.track.setColor(color);
trackComponent.repaint();
}
});
ButtonGroup shapeGroup = new ButtonGroup();
JRadioButtonMenuItem textShape = getJunctionCoverageRadioButton("Text", SashimiJunctionRenderer.ShapeType.TEXT);
textShape.setToolTipText("Show junction coverage as text");
shapeGroup.add(textShape);
JRadioButtonMenuItem circleShape = getJunctionCoverageRadioButton("Circle", SashimiJunctionRenderer.ShapeType.CIRCLE);
circleShape.setToolTipText("Show junction coverage as a circle");
shapeGroup.add(circleShape);
JRadioButtonMenuItem ellipseShape = getJunctionCoverageRadioButton("Ellipse", SashimiJunctionRenderer.ShapeType.ELLIPSE);
ellipseShape.setToolTipText("Show junction coverage as an ellipse");
shapeGroup.add(ellipseShape);
JRadioButtonMenuItem noShape = getJunctionCoverageRadioButton("None", SashimiJunctionRenderer.ShapeType.NONE);
ellipseShape.setToolTipText("Show junction coverage as an ellipse");
shapeGroup.add(ellipseShape);
ButtonGroup strandGroup = new ButtonGroup();
JRadioButtonMenuItem combineStrands = getStrandRadioButton("Combine Strands", SpliceJunctionTrack.StrandOption.COMBINE);
combineStrands.setToolTipText("Combine junctions from both strands -- best for non-strand preserving libraries.");
strandGroup.add(combineStrands);
// JRadioButtonMenuItem bothStrands = getStrandRadioButton("Both Strands", SpliceJunctionTrack.StrandOption.BOTH);
// strandGroup.add(bothStrands);
// menu.add(bothStrands);
JRadioButtonMenuItem plusStrand = getStrandRadioButton("Forward Strand", SpliceJunctionTrack.StrandOption.FORWARD);
plusStrand.setToolTipText("Show only junctions on the forward read strand (of first-in-pair for paired reads)");
strandGroup.add(plusStrand);
JRadioButtonMenuItem minusStrand = getStrandRadioButton("Reverse Strand", SpliceJunctionTrack.StrandOption.REVERSE);
plusStrand.setToolTipText("Show only junctions on the reverse read strand (of first-in-pair for paired reads)");
strandGroup.add(minusStrand);
JMenuItem saveImageItem = new JMenuItem("Save Image...");
saveImageItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
File defaultFile = new File("Sashimi.png");
IGV.getInstance().createSnapshot(SashimiPlot.this.getContentPane(), defaultFile);
}
});
// Coverage ranges -- these apply to current plot only
menu.add(new JLabel("Junction Coverage Display"));
menu.add(setCoverageDataRange);
menu.add(minJunctionCoverage);
menu.add(maxJunctionCoverageRange);
menu.add(colorItem);
// Coverage data -- applies to all plots
menu.add(showCoverageData);
// Shape options -- all plots
menu.addSeparator();
menu.add(textShape);
menu.add(circleShape);
// menu.add(ellipseShape);
menu.add(noShape);
// Strand options -- applies to all plots
menu.addSeparator();
menu.add(combineStrands);
menu.add(plusStrand);
menu.add(minusStrand);
menu.addSeparator();
menu.add(saveImageItem);
return menu;
}
private JRadioButtonMenuItem getJunctionCoverageRadioButton(String label, final SashimiJunctionRenderer.ShapeType shapeType) {
JRadioButtonMenuItem item = new JRadioButtonMenuItem(label);
item.setSelected(SashimiJunctionRenderer.getShapeType() == shapeType);
item.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
SashimiJunctionRenderer.setShapeType(shapeType);
SashimiPlot.this.repaint();
}
});
return item;
}
private JRadioButtonMenuItem getStrandRadioButton(String label, final SpliceJunctionTrack.StrandOption option) {
JRadioButtonMenuItem item = new JRadioButtonMenuItem(label);
item.setSelected(SpliceJunctionTrack.getStrandOption() == option);
item.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
SpliceJunctionTrack.setStrandOption(option);
for (SpliceJunctionTrack t : spliceJunctionTracks) {
t.clear();
}
repaint();
}
});
return item;
}
}
private class GeneTrackMouseAdapter extends TrackComponentMouseAdapter<SelectableFeatureTrack> {
GeneTrackMouseAdapter(TrackComponent<SelectableFeatureTrack> trackComponent) {
super(trackComponent);
}
@Override
protected void handleDataClick(MouseEvent e) {
trackComponent.track.handleDataClick(createTrackClickEvent(e));
Set<IExon> selectedExon = trackComponent.track.getSelectedExons();
for (SpliceJunctionTrack spliceTrack : spliceJunctionTracks) {
getRenderer(spliceTrack).setSelectedExons(selectedExon);
}
repaint();
}
@Override
public void mouseMoved(MouseEvent e) {
trackComponent.updateToolTipText(createTrackClickEvent(e));
}
@Override
protected IGVPopupMenu getPopupMenu(MouseEvent e) {
IGVPopupMenu menu = new IGVPopupMenu();
TrackMenuUtils.addDisplayModeItems(Arrays.<Track>asList(trackComponent.track), menu);
menu.addPopupMenuListener(new RepaintPopupMenuListener(SashimiPlot.this));
return menu;
}
}
private abstract class TrackComponentMouseAdapter<T extends Track> extends MouseAdapter {
protected TrackComponent<T> trackComponent;
protected PanTool currentTool;
TrackComponentMouseAdapter(TrackComponent<T> trackComponent) {
this.trackComponent = trackComponent;
currentTool = new PanTool(null);
currentTool.setReferenceFrame(this.trackComponent.frame);
}
@Override
public void mouseDragged(MouseEvent e) {
if (currentTool.getLastMousePoint() == null) {
//This shouldn't happen, but does occasionally
return;
}
double diff = e.getX() - currentTool.getLastMousePoint().getX();
// diff > 0 means moving mouse to the right, which drags the frame towards the negative direction
boolean hitBounds = SashimiPlot.this.frame.getOrigin() <= minOrigin && diff > 0;
hitBounds |= SashimiPlot.this.frame.getEnd() >= maxEnd && diff < 0;
if (!hitBounds) {
currentTool.mouseDragged(e);
repaint();
}
}
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger()) {
doPopupMenu(e);
} else {
currentTool.mouseReleased(e);
}
}
@Override
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger()) {
doPopupMenu(e);
} else {
currentTool.mousePressed(e);
super.mousePressed(e);
}
}
protected void doPopupMenu(MouseEvent e) {
IGVPopupMenu menu = getPopupMenu(e);
if (menu != null) menu.show(trackComponent, e.getX(), e.getY());
}
protected TrackClickEvent createTrackClickEvent(MouseEvent e) {
return new TrackClickEvent(e, trackComponent.frame);
}
@Override
public void mouseClicked(MouseEvent e) {
if (e.isPopupTrigger()) {
doPopupMenu(e);
return;
}
currentTool.mouseClicked(e);
handleDataClick(e);
}
/**
* Essentially left click
*
* @param e
*/
protected abstract void handleDataClick(MouseEvent e);
/**
* Essentially right click
*
* @param e
* @return
*/
protected abstract IGVPopupMenu getPopupMenu(MouseEvent e);
}
/**
* Show SashimiPlot window, or change settings of {@code currentWindow}
*
* @param sashimiPlot
*/
public static void getSashimiPlot(SashimiPlot sashimiPlot) {
if (sashimiPlot == null) {
FeatureTrack geneTrack = null;
List<FeatureTrack> nonJunctionTracks = new ArrayList(IGV.getInstance().getFeatureTracks());
Iterator iter = nonJunctionTracks.iterator();
while(iter.hasNext()) {
if(iter.next() instanceof SpliceJunctionTrack) iter.remove();
}
if (nonJunctionTracks.size() == 1) {
geneTrack = nonJunctionTracks.get(0);
} else {
FeatureTrackSelectionDialog dlg = new FeatureTrackSelectionDialog(IGV.getMainFrame(), nonJunctionTracks);
dlg.setTitle("Select Gene Track");
dlg.setVisible(true);
if (dlg.getIsCancelled()) return;
geneTrack = dlg.getSelectedTrack();
}
Collection<AlignmentTrack> alignmentTracks = new ArrayList<AlignmentTrack>();
for (Track track : IGV.getInstance().getAllTracks()) {
if (track instanceof AlignmentTrack) {
alignmentTracks.add((AlignmentTrack) track);
}
}
if (alignmentTracks.size() > 1) {
TrackSelectionDialog<AlignmentTrack> alDlg =
new TrackSelectionDialog<AlignmentTrack>(IGV.getMainFrame(), TrackSelectionDialog.SelectionMode.MULTIPLE, alignmentTracks);
alDlg.setTitle("Select Alignment Tracks");
alDlg.setVisible(true);
if (alDlg.getIsCancelled()) return;
alignmentTracks = alDlg.getSelectedTracks();
}
sashimiPlot = new SashimiPlot(FrameManager.getDefaultFrame(), alignmentTracks, geneTrack);
//sashimiPlot.setShapeType(shapeType);
sashimiPlot.setVisible(true);
} else {
//sashimiPlot.setShapeType(shapeType);
}
}
private static class RepaintPopupMenuListener implements PopupMenuListener {
Component component;
RepaintPopupMenuListener(Component component) {
this.component = component;
}
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
component.repaint();
}
@Override
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
component.repaint();
}
@Override
public void popupMenuCanceled(PopupMenuEvent e) {
component.repaint();
}
}
private static class Axis extends JComponent {
private ReferenceFrame frame;
Axis(ReferenceFrame frame) {
this.frame = frame;
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Rectangle visibleRect = getVisibleRect();
RenderContext context = new RenderContext(this, (Graphics2D) g, frame, visibleRect);
drawGenomicAxis(context, visibleRect);
}
/**
* Draw axis displaying genomic coordinates
*
* @param context
* @param trackRectangle
*/
private void drawGenomicAxis(RenderContext context, Rectangle trackRectangle) {
int numTicks = 4;
int ticHeight = 5;
double pixelPadding = trackRectangle.getWidth() / 20;
int yLoc = ticHeight + 1;
double origin = context.getOrigin();
double locScale = context.getScale();
//Pixel start/end positions of ruler
double startPix = trackRectangle.getX() + pixelPadding;
double endPix = trackRectangle.getMaxX() - pixelPadding;
double ticIntervalPix = (endPix - startPix) / (numTicks - 1);
double ticIntervalCoord = locScale * ticIntervalPix;
int startCoord = (int) (origin + (locScale * startPix));
Graphics2D g2D = context.getGraphic2DForColor(Color.black);
g2D.drawLine((int) startPix, yLoc, (int) endPix, yLoc);
for (int tic = 0; tic < numTicks; tic++) {
int xLoc = (int) (startPix + tic * ticIntervalPix);
g2D.drawLine(xLoc, yLoc, xLoc, yLoc - ticHeight);
int ticCoord = (int) (startCoord + tic * ticIntervalCoord);
String text = "" + ticCoord;
Rectangle2D textBounds = g2D.getFontMetrics().getStringBounds(text, g2D);
g2D.drawString(text, (int) (xLoc - textBounds.getWidth() / 2), (int) (yLoc + textBounds.getHeight()));
}
}
}
}