/*
* 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.
*/
/*
* IGV.java
*
* Represents an IGV instance.
*
* Note: Currently, only one instance is allowed per JVM.
*
*/
package org.broad.igv.ui;
import apple.dts.samplecode.osxadapter.OSXAdapter;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.jidesoft.swing.JideSplitPane;
import htsjdk.samtools.seekablestream.SeekableFileStream;
import org.apache.log4j.Logger;
import org.broad.igv.DirectoryManager;
import org.broad.igv.Globals;
import org.broad.igv.annotations.ForTesting;
import org.broad.igv.batch.BatchRunner;
import org.broad.igv.batch.CommandListener;
import org.broad.igv.dev.api.IGVPlugin;
import org.broad.igv.exceptions.DataLoadException;
import org.broad.igv.feature.*;
import org.broad.igv.feature.Range;
import org.broad.igv.feature.genome.*;
import org.broad.igv.lists.GeneList;
import org.broad.igv.peaks.PeakCommandBar;
import org.broad.igv.prefs.IGVPreferences;
import org.broad.igv.prefs.PreferenceEditorFX;
import org.broad.igv.prefs.PreferencesEditor;
import org.broad.igv.prefs.PreferencesManager;
import org.broad.igv.sam.AlignmentTrack;
import org.broad.igv.sam.InsertionSelectionEvent;
import org.broad.igv.session.*;
import org.broad.igv.track.*;
import org.broad.igv.ui.dnd.GhostGlassPane;
import org.broad.igv.event.*;
import org.broad.igv.ui.panel.*;
import org.broad.igv.ui.util.*;
import org.broad.igv.ui.util.ProgressMonitor;
import org.broad.igv.util.*;
import org.broad.igv.variant.VariantTrack;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.ImageObserver;
import java.io.*;
import java.net.NoRouteToHostException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.*;
import java.util.List;
import java.util.concurrent.Future;
import java.util.prefs.Preferences;
import static org.broad.igv.prefs.Constants.*;
import static org.broad.igv.ui.WaitCursorManager.CursorToken;
/**
* Represents an IGV instance, consisting of a main window and associated model.
*
* @author jrobinso
*/
public class IGV implements IGVEventObserver {
private static Logger log = Logger.getLogger(IGV.class);
private static IGV theInstance;
// Window components
private Frame mainFrame;
private JRootPane rootPane;
private IGVContentPane contentPane;
private IGVMenuBar menuBar;
private StatusWindow statusWindow;
// Glass panes
Component glassPane;
GhostGlassPane dNdGlassPane;
// Cursors
public static Cursor fistCursor;
public static Cursor zoomInCursor;
public static Cursor zoomOutCursor;
public static Cursor dragNDropCursor;
//Session session;
Session session;
private GenomeManager genomeManager;
/**
* Attribute used to group tracks. Normally "null". Set from the "Tracks" menu.
*/
private String groupByAttribute = null;
private Map<String, List<Track>> overlayTracksMap = new HashMap();
private Set<Track> overlaidTracks = new HashSet();
public static final String DATA_PANEL_NAME = "DataPanel";
public static final String FEATURE_PANEL_NAME = "FeaturePanel";
// Misc state
private LinkedList<String> recentSessionList = new LinkedList<String>();
private boolean isExportingSnapshot = false;
private List<JComponent> otherToolMenus = new ArrayList<>();
// Vertical line that follows the mouse
private boolean rulerEnabled;
/**
* Add an entry to the "Tools" menu
*
* @param menu
* @api
*/
public void addOtherToolMenu(JComponent menu) {
otherToolMenus.add(menu);
if (menuBar != null) menuBar.refreshToolsMenu();
}
List<JComponent> getOtherToolMenus() {
return otherToolMenus;
}
public static IGV createInstance(Frame frame) {
if (theInstance != null) {
throw new RuntimeException("Only a single instance is allowed.");
}
theInstance = new IGV(frame);
return theInstance;
}
public static IGV getInstance() {
if (theInstance == null) {
throw new RuntimeException("IGV has not been initialized. Must call createInstance(Frame) first");
}
return theInstance;
}
@ForTesting
static void destroyInstance() {
IGVMenuBar.destroyInstance();
theInstance = null;
}
public static boolean hasInstance() {
return theInstance != null;
}
public static JRootPane getRootPane() {
return getInstance().rootPane;
}
/**
* The IGV GUI has one master frame containing all other elements.
* This method returns that frame.
*
* @return
* @api
*/
public static Frame getMainFrame() {
return getInstance().mainFrame;
}
/**
* Creates new IGV
*/
private IGV(Frame frame) {
theInstance = this;
final IGVPreferences preferences = PreferencesManager.getPreferences();
genomeManager = GenomeManager.getInstance();
mainFrame = frame;
mainFrame.addWindowListener(new WindowAdapter() {
@Override
public void windowLostFocus(WindowEvent windowEvent) {
// Start & stop tooltip manager to force any tooltip windows to close.
ToolTipManager.sharedInstance().setEnabled(false);
ToolTipManager.sharedInstance().setEnabled(true);
IGVPopupMenu.closeAll();
}
@Override
public void windowDeactivated(WindowEvent windowEvent) {
// Start & stop tooltip manager to force any tooltip windows to close.
ToolTipManager.sharedInstance().setEnabled(false);
ToolTipManager.sharedInstance().setEnabled(true);
IGVPopupMenu.closeAll();
}
@Override
public void windowActivated(WindowEvent windowEvent) {
}
@Override
public void windowGainedFocus(WindowEvent windowEvent) {
}
});
session = new Session(null);
// Create cursors
createHandCursor();
createZoomCursors();
createDragAndDropCursor();
// Create components
mainFrame.setTitle(UIConstants.APPLICATION_NAME);
if (mainFrame instanceof JFrame) {
JFrame jf = (JFrame) mainFrame;
rootPane = jf.getRootPane();
} else {
rootPane = new JRootPane();
mainFrame.add(rootPane);
}
contentPane = new IGVContentPane(this);
menuBar = IGVMenuBar.createInstance(this);
rootPane.setContentPane(contentPane);
rootPane.setJMenuBar(menuBar);
glassPane = rootPane.getGlassPane();
glassPane.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
consumeEvents(glassPane);
dNdGlassPane = new GhostGlassPane();
mainFrame.pack();
//Certain components MUST be visible, so we set minimum size
//{@link MainPanel#addDataPanel}
mainFrame.setMinimumSize(new Dimension(300, 300));
// Set the application's previous location and size
Dimension screenBounds = Toolkit.getDefaultToolkit().getScreenSize();
Rectangle applicationBounds = preferences.getApplicationFrameBounds();
if (applicationBounds == null || applicationBounds.getMaxX() > screenBounds.getWidth() ||
applicationBounds.getMaxY() > screenBounds.getHeight() ||
applicationBounds.width == 0 || applicationBounds.height == 0) {
int width = Math.min(1150, (int) screenBounds.getWidth());
int height = Math.min(800, (int) screenBounds.getHeight());
applicationBounds = new Rectangle(0, 0, width, height);
}
mainFrame.setBounds(applicationBounds);
subscribeToEvents();
}
private void consumeEvents(Component glassPane) {
glassPane.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
e.consume();
}
@Override
public void mousePressed(MouseEvent e) {
e.consume();
}
});
glassPane.setFocusable(true);
glassPane.addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
e.consume();
}
@Override
public void keyReleased(KeyEvent e) {
e.consume();
}
@Override
public void keyPressed(KeyEvent e) {
e.consume();
}
});
}
public GhostGlassPane getDnDGlassPane() {
return dNdGlassPane;
}
public void startDnD() {
rootPane.setGlassPane(dNdGlassPane);
dNdGlassPane.setVisible(true);
}
public void endDnD() {
rootPane.setGlassPane(glassPane);
glassPane.setVisible(false);
}
public Dimension getPreferredSize() {
return UIConstants.preferredSize;
}
public void addRegionOfInterest(RegionOfInterest roi) {
session.addRegionOfInterestWithNoListeners(roi);
RegionOfInterestPanel.setSelectedRegion(roi);
doRefresh();
}
void beginROI(JButton button) {
for (TrackPanel tp : getTrackPanels()) {
TrackPanelScrollPane tsv = tp.getScrollPane();
DataPanelContainer dpc = tsv.getDataPanel();
for (Component c : dpc.getComponents()) {
if (c instanceof DataPanel) {
DataPanel dp = (DataPanel) c;
RegionOfInterestTool regionOfInterestTool = new RegionOfInterestTool(dp, button);
dp.setCurrentTool(regionOfInterestTool);
}
}
}
}
public void endROI() {
for (TrackPanel tp : getTrackPanels()) {
DataPanelContainer dp = tp.getScrollPane().getDataPanel();
dp.setCurrentTool(null);
}
}
// Set the focus on the command bar search box
public void focusSearchBox() {
contentPane.getCommandBar().focusSearchBox();
}
public void selectGenomeFromList(String genomeId) {
contentPane.getCommandBar().selectGenome(genomeId);
}
public void defineGenome(javax.swing.ProgressMonitor monitor) {
ProgressBar.ProgressDialog progressDialog = null;
File archiveFile = null;
try {
GenomeBuilderDialog genomeBuilderDialog = new GenomeBuilderDialog(mainFrame, this);
genomeBuilderDialog.setVisible(true);
File genomeZipFile = genomeBuilderDialog.getArchiveFile();
if (genomeBuilderDialog.isCanceled() || genomeZipFile == null) {
return;
}
String cytobandFileName = genomeBuilderDialog.getCytobandFileName();
String geneAnnotFileName = genomeBuilderDialog.getGeneAnnotFileName();
String fastaFileName = genomeBuilderDialog.getFastaFileName();
String chrAliasFile = genomeBuilderDialog.getChrAliasFileName();
String genomeDisplayName = genomeBuilderDialog.getGenomeDisplayName();
String genomeId = genomeBuilderDialog.getGenomeId();
GenomeListItem genomeListItem = getGenomeManager().defineGenome(
genomeZipFile, cytobandFileName, geneAnnotFileName,
fastaFileName, chrAliasFile, genomeDisplayName,
genomeId, monitor);
if (genomeListItem != null) {
contentPane.getCommandBar().refreshGenomeListComboBox();
contentPane.getCommandBar().selectGenome(genomeListItem.getId());
}
if (monitor != null) {
monitor.setProgress(100);
}
} catch (MaximumContigGenomeException e) {
String genomePath = "";
if (archiveFile != null) {
genomePath = archiveFile.getAbsolutePath();
}
log.error("Failed to define genome: " + genomePath, e);
JOptionPane.showMessageDialog(mainFrame, "Failed to define genome " +
genomePath + "\n" + e.getMessage());
} catch (GenomeException e) {
log.error("Failed to define genome.", e);
MessageUtils.showMessage(e.getMessage());
} catch (Exception e) {
String genomePath = "";
if (archiveFile != null) {
genomePath = archiveFile.getAbsolutePath();
}
log.error("Failed to define genome: " + genomePath, e);
MessageUtils.showMessage("Unexpected error while importing a genome: " + e.getMessage());
} finally {
if (progressDialog != null) {
progressDialog.setVisible(false);
}
}
}
void loadGenomeFromServer() {
Runnable showDialog = new Runnable() {
@Override
public void run() {
Collection<GenomeListItem> inputListItems = GenomeManager.getInstance().getServerGenomeArchiveList();
if (inputListItems == null) {
//Could not reach genome server. Not necessary to display a message, getServerGenomeArchiveList does it already
return;
}
GenomeSelectionDialog dialog = new GenomeSelectionDialog(IGV.getMainFrame(), inputListItems, ListSelectionModel.SINGLE_SELECTION);
UIUtilities.invokeAndWaitOnEventThread(() -> dialog.setVisible(true));
if (dialog.isCanceled()) {
// Clear the "More..." selection in pulldown
IGVEventBus.getInstance().post(new GenomeResetEvent());
} else {
List<GenomeListItem> selectedValues = dialog.getSelectedValuesList();
if (selectedValues != null && selectedValues.size() >= 1) {
if (selectedValues.size() == 1 && dialog.downloadSequence()) {
GenomeListItem oldItem = selectedValues.get(0);
GenomeSelectionDialog.downloadGenome(getMainFrame(), oldItem);
File newLocation = new File(DirectoryManager.getGenomeCacheDirectory().getAbsolutePath(), Utilities.getFileNameFromURL(oldItem.getLocation()));
GenomeListItem newItem = new GenomeListItem(oldItem.getDisplayableName(), newLocation.getAbsolutePath(), oldItem.getId());
//Checking to see if it has a downloaded sequence might seem redundant,
//but if the user cancels a download we want to use the oldItem
if (newItem.hasDownloadedSequence()) {
selectedValues = Arrays.asList(newItem);
}
}
if (selectedValues.size() > 0) {
GenomeManager.getInstance().addGenomeItems(selectedValues, false);
GenomeManager.getInstance().loadGenome(selectedValues.get(0).getLocation(), null);
// contentPane.getCommandBar().selectGenome(selectedValues.get(0).getId());
}
}
}
}
};
if (SwingUtilities.isEventDispatchThread()) {
LongRunningTask.submit(showDialog);
} else {
showDialog.run();
}
}
public void enableExtrasMenu() {
menuBar.enableExtrasMenu();
}
/**
* Load a collection of tracks in a background thread.
* <p/>
* Note: Most of the code here is to adjust the scrollbars and split pane after loading
*
* @param locators
*/
public Future loadTracks(final Collection<ResourceLocator> locators) {
contentPane.getStatusBar().setMessage("Loading ...");
log.debug("Run loadTracks");
Future toRet = null;
if (locators != null && !locators.isEmpty()) {
NamedRunnable runnable = new NamedRunnable() {
public void run() {
//Collect size statistics before loading
List<Map<TrackPanelScrollPane, Integer>> trackPanelAttrs = getTrackPanelAttrs();
loadResources(locators);
resetPanelHeights(trackPanelAttrs.get(0), trackPanelAttrs.get(1));
showLoadedTrackCount();
}
public String getName() {
return "Load Tracks";
}
};
toRet = LongRunningTask.submit(runnable);
}
log.debug("Finish loadTracks");
return toRet;
}
/**
* Cet current track count per panel. Needed to detect which panels
* changed. Also record panel sizes
*
* @return A 2 element list: 0th element is a map from scrollpane -> number of tracks,
* 1st element is a map from scrollpane -> track height (in pixels)
*/
public List<Map<TrackPanelScrollPane, Integer>> getTrackPanelAttrs() {
Map<TrackPanelScrollPane, Integer> trackCountMap = new HashMap();
Map<TrackPanelScrollPane, Integer> panelSizeMap = new HashMap();
for (TrackPanel tp : getTrackPanels()) {
TrackPanelScrollPane sp = tp.getScrollPane();
trackCountMap.put(sp, sp.getDataPanel().getAllTracks().size());
panelSizeMap.put(sp, sp.getDataPanel().getHeight());
}
return Arrays.asList(trackCountMap, panelSizeMap);
}
/**
* Recalculate and set heights of track panels, based on newly loaded tracks
*
* @param trackCountMap scrollpane -> number of tracks
* @param panelSizeMap scrollpane -> height in pixels
*/
public void resetPanelHeights(Map<TrackPanelScrollPane, Integer> trackCountMap, Map<TrackPanelScrollPane, Integer> panelSizeMap) {
UIUtilities.invokeAndWaitOnEventThread(() -> {
double totalHeight = 0;
for (TrackPanel tp : getTrackPanels()) {
TrackPanelScrollPane sp = tp.getScrollPane();
if (trackCountMap.containsKey(sp)) {
int prevTrackCount = trackCountMap.get(sp);
if (prevTrackCount != sp.getDataPanel().getAllTracks().size()) {
int scrollPosition = panelSizeMap.get(sp);
if (prevTrackCount != 0 && sp.getVerticalScrollBar().isShowing()) {
sp.getVerticalScrollBar().setMaximum(sp.getDataPanel().getHeight());
sp.getVerticalScrollBar().setValue(scrollPosition);
}
}
}
// Give a maximum "weight" of 300 pixels to each panel. If there are no tracks, give zero
if (sp.getTrackPanel().getTracks().size() > 0)
totalHeight += Math.min(300, sp.getTrackPanel().getPreferredPanelHeight());
}
// Adjust dividers for data panel. The data panel divider can be
// zero if there are no data tracks loaded.
final JideSplitPane centerSplitPane = contentPane.getMainPanel().getCenterSplitPane();
int htotal = centerSplitPane.getHeight();
int y = 0;
int i = 0;
for (Component c : centerSplitPane.getComponents()) {
if (c instanceof TrackPanelScrollPane) {
final TrackPanel trackPanel = ((TrackPanelScrollPane) c).getTrackPanel();
if (trackPanel.getTracks().size() > 0) {
int panelWeight = Math.min(300, trackPanel.getPreferredPanelHeight());
int dh = (int) ((panelWeight / totalHeight) * htotal);
y += dh;
}
centerSplitPane.setDividerLocation(i, y);
i++;
}
}
contentPane.getMainPanel().invalidate();
});
}
public void setGeneList(GeneList geneList) {
setGeneList(geneList, true);
}
public void setGeneList(final GeneList geneList, final boolean recordHistory) {
final CursorToken token = WaitCursorManager.showWaitCursor();
SwingUtilities.invokeLater(new NamedRunnable() {
public void run() {
try {
if (geneList == null) {
session.setCurrentGeneList(null);
} else {
if (recordHistory) {
session.getHistory().push("List: " + geneList.getName(), 0);
}
session.setCurrentGeneList(geneList);
}
resetFrames();
} finally {
WaitCursorManager.removeWaitCursor(token);
}
}
public String getName() {
return "Set gene list";
}
});
}
public void setDefaultFrame(String searchString) {
FrameManager.setToDefaultFrame(searchString);
resetFrames();
}
final public void doViewPreferences() {
try {
PreferenceEditorFX.open(this.mainFrame);
} catch (IOException e) {
log.error("Error openining preference dialog", e);
}
}
final public void saveStateForExit() {
// Store recent sessions
if (!getRecentSessionList().isEmpty()) {
int size = getRecentSessionList().size();
if (size > UIConstants.NUMBER_OF_RECENT_SESSIONS_TO_LIST) {
size = UIConstants.NUMBER_OF_RECENT_SESSIONS_TO_LIST;
}
String recentSessions = "";
for (int i = 0; i <
size; i++) {
recentSessions += getRecentSessionList().get(i);
if (i < (size - 1)) {
recentSessions += ";";
}
}
PreferencesManager.getPreferences().remove(RECENT_SESSIONS);
PreferencesManager.getPreferences().setRecentSessions(recentSessions);
}
}
final public void doShowAttributeDisplay(boolean enableAttributeView) {
boolean oldState = PreferencesManager.getPreferences().getAsBoolean(SHOW_ATTRIBUTE_VIEWS_KEY);
// First store the newly requested state
if (oldState != enableAttributeView) {
PreferencesManager.getPreferences().setShowAttributeView(enableAttributeView);
doRefresh();
}
}
// TODO -- move all of this attribute stuff out of IGV, perhaps to
// some Attribute helper class.
final public void doSelectDisplayableAttribute() {
List<String> allAttributes = AttributeManager.getInstance().getAttributeNames();
Set<String> hiddenAttributes = IGV.getInstance().getSession().getHiddenAttributes();
final CheckListDialog dlg = new CheckListDialog(mainFrame, allAttributes, hiddenAttributes, false);
dlg.setVisible(true);
if (!dlg.isCanceled()) {
// If any "default" attributes are checked turn off hide default option
Set<String> selections = dlg.getSelections();
for (String att : AttributeManager.defaultTrackAttributes) {
if (selections.contains(att)) {
PreferencesManager.getPreferences().put(SHOW_DEFAULT_TRACK_ATTRIBUTES, true);
break;
}
}
IGV.getInstance().getSession().setHiddenAttributes(dlg.getNonSelections());
doRefresh();
}
}
final public void saveImage(Component target) {
saveImage(target, "igv_snapshot");
}
final public void saveImage(Component target, String title) {
contentPane.getStatusBar().setMessage("Creating image...");
File defaultFile = new File(title + ".png");
createSnapshot(target, defaultFile);
}
public boolean isExportingSnapshot() {
return isExportingSnapshot;
}
final public void createSnapshot(final Component target, final File defaultFile) {
File file = selectSnapshotFile(defaultFile);
if (file == null) {
return;
}
CursorToken token = null;
try {
token = WaitCursorManager.showWaitCursor();
contentPane.getStatusBar().setMessage("Exporting image: " + defaultFile.getAbsolutePath());
String msg = createSnapshotNonInteractive(target, file, false);
if (msg != null && msg.toLowerCase().startsWith("error")) {
MessageUtils.showMessage(msg);
}
} catch (IOException e) {
log.error("Error creating exporting image ", e);
MessageUtils.showMessage(("Error creating the image file: " + defaultFile + "<br> "
+ e.getMessage()));
} finally {
if (token != null) WaitCursorManager.removeWaitCursor(token);
resetStatusMessage();
}
}
/**
* Create a snapshot image of {@code target} and save it to {@code file}. The file type of the exported
* snapshot will be chosen by the extension of {@code file}, which must be a supported type.
*
* @param target
* @param file
* @param paintOffscreen Whether to include offScreen data in the snapshot. Components must implement
* the {@link Paintable} interface for this to work
* @throws IOException
* @api
* @see SnapshotFileChooser.SnapshotFileType
*/
public String createSnapshotNonInteractive(Component target, File file, boolean paintOffscreen) throws IOException {
log.debug("Creating snapshot: " + file.getName());
String extension = FileUtils.getFileExtension(file.getAbsolutePath());
SnapshotFileChooser.SnapshotFileType type = SnapshotFileChooser.getSnapshotFileType(extension);
String message;
IOException exc = null;
if (type == SnapshotFileChooser.SnapshotFileType.NULL) {
message = "ERROR: Unknown file extension " + extension;
log.error(message);
return message;
} else if (type == SnapshotFileChooser.SnapshotFileType.EPS && !SnapshotUtilities.canExportScreenshotEps()) {
message = "ERROR: EPS output requires EPSGraphics library. See https://www.broadinstitute.org/software/igv/third_party_tools#epsgraphics";
log.error(message);
return message;
}
//boolean doubleBuffered = RepaintManager.currentManager(contentPane).isDoubleBufferingEnabled();
try {
setExportingSnapshot(true);
message = SnapshotUtilities.doComponentSnapshot(target, file, type, paintOffscreen);
} catch (IOException e) {
exc = e;
message = e.getMessage();
} finally {
setExportingSnapshot(false);
}
log.debug("Finished creating snapshot: " + file.getName());
if (exc != null) throw exc;
return message;
}
public File selectSnapshotFile(File defaultFile) {
File snapshotDirectory = PreferencesManager.getPreferences().getLastSnapshotDirectory();
JFileChooser fc = new SnapshotFileChooser(snapshotDirectory, defaultFile);
fc.showSaveDialog(mainFrame);
File file = fc.getSelectedFile();
// If a file selection was made
if (file != null) {
File directory = file.getParentFile();
if (directory != null) {
PreferencesManager.getPreferences().setLastSnapshotDirectory(directory);
}
}
return file;
}
private void createZoomCursors() throws HeadlessException, IndexOutOfBoundsException {
if (zoomInCursor == null || zoomOutCursor == null) {
final Image zoomInImage = IconFactory.getInstance().getIcon(IconFactory.IconID.ZOOM_IN).getImage();
final Image zoomOutImage = IconFactory.getInstance().getIcon(IconFactory.IconID.ZOOM_OUT).getImage();
final Point hotspot = new Point(10, 10);
zoomInCursor = mainFrame.getToolkit().createCustomCursor(zoomInImage, hotspot, "Zoom in");
zoomOutCursor = mainFrame.getToolkit().createCustomCursor(zoomOutImage, hotspot, "Zoom out");
}
}
private void createHandCursor() throws HeadlessException, IndexOutOfBoundsException {
/*if (handCursor == null) {
BufferedImage handImage = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
// Make backgroun transparent
Graphics2D g = handImage.createGraphics();
g.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f));
Rectangle2D.Double rect = new Rectangle2D.Double(0, 0, 32, 32);
g.fill(rect);
// Draw hand image in middle
g = handImage.createGraphics();
g.drawImage(IconFactory.getInstance().getIcon(IconFactory.IconID.OPEN_HAND).getImage(), 0, 0, null);
handCursor = getToolkit().createCustomCursor(handImage, new Point(8, 6), "Move");
}*/
if (fistCursor == null) {
final BufferedImage handImage = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
// Make backgroun transparent
Graphics2D g = handImage.createGraphics();
g.setComposite(AlphaComposite.getInstance(
AlphaComposite.CLEAR, 0.0f));
Rectangle2D.Double rect = new Rectangle2D.Double(0, 0, 32, 32);
g.fill(rect);
// Draw hand image in middle
g = handImage.createGraphics();
boolean ready = g.drawImage(IconFactory.getInstance().getIcon(IconFactory.IconID.FIST).getImage(), 0, 0, new ImageObserver() {
@Override
public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
if ((infoflags & ImageObserver.ALLBITS) != 0) {
// Image is ready
try {
fistCursor = mainFrame.getToolkit().createCustomCursor(handImage, new Point(8, 6), "Move");
} catch (Exception e) {
log.error("Could not create fistCursor", e);
fistCursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
}
return false;
} else {
return true;
}
}
});
if (ready) {
try {
fistCursor = mainFrame.getToolkit().createCustomCursor(handImage, new Point(8, 6), "Move");
} catch (Exception e) {
log.info("Warning: could not create fistCursor");
fistCursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
}
}
}
}
private void createDragAndDropCursor()
throws HeadlessException, IndexOutOfBoundsException {
if (dragNDropCursor == null) {
ImageIcon icon = IconFactory.getInstance().getIcon(IconFactory.IconID.DRAG_AND_DROP);
int width = icon.getIconWidth();
int height = icon.getIconHeight();
final BufferedImage dragNDropImage =
new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
// Make background transparent
Graphics2D g = dragNDropImage.createGraphics();
g.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f));
Rectangle2D.Double rect = new Rectangle2D.Double(0, 0, width, height);
g.fill(rect);
// Draw DND image
g = dragNDropImage.createGraphics();
Image image = icon.getImage();
boolean ready = g.drawImage(image, 0, 0, new ImageObserver() {
@Override
public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
if ((infoflags & ImageObserver.ALLBITS) != 0) {
// Image is ready
try {
dragNDropCursor = mainFrame.getToolkit().createCustomCursor(
dragNDropImage, new Point(0, 0), "Drag and Drop");
} catch (Exception e) {
log.info("Warning: could not create dragNDropCursor");
dragNDropCursor = Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR);
}
return false;
} else {
return true;
}
}
});
if (ready) {
try {
dragNDropCursor = mainFrame.getToolkit().createCustomCursor(
dragNDropImage, new Point(0, 0), "Drag and Drop");
} catch (Exception e) {
log.info("Warning: could not create dragNDropCursor");
dragNDropCursor = Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR);
}
}
}
}
/**
* Set the session to the file specified by {@code sessionPath}
* If you want to create a new session, consider {@link #newSession()}
* as that preserves the gene track.
*
* @param sessionPath
*/
public void resetSession(String sessionPath) {
System.gc();
List<Track> oldTracks = getAllTracks();
AttributeManager.getInstance().clearAllAttributes();
String tile = sessionPath == null ? UIConstants.APPLICATION_NAME : sessionPath;
mainFrame.setTitle(tile);
menuBar.resetSessionActions();
AttributeManager.getInstance().clearAllAttributes();
if (session == null) {
session = new Session(sessionPath);
} else {
session.reset(sessionPath);
}
contentPane.getMainPanel().resetPanels();
//TODO -- this is a very blunt and dangerous way to clean up -- change to close files associated with this session
SeekableFileStream.closeAllInstances();
Set<Track> newTracks = new HashSet<>(getAllTracks());
for(Track t : oldTracks) {
}
doRefresh();
System.gc();
}
private void subscribeToEvents() {
IGVEventBus.getInstance().subscribe(ViewChange.class, this);
IGVEventBus.getInstance().subscribe(ShiftEvent.class, this);
IGVEventBus.getInstance().subscribe(InsertionSelectionEvent.class, this);
IGVEventBus.getInstance().subscribe(GenomeChangeEvent.class, this);
}
/**
* Creates a new IGV session, and restores the gene track afterwards.
* For that reason, if one wishes to keep the default gene track, this method
* should be used, rather than resetSession
*/
public void newSession() {
resetSession(null);
setGenomeTracks(GenomeManager.getInstance().getCurrentGenome().getGeneTrack());
}
/**
* Set the status bar message. If the message equals "Done." intercept
* and reset to the default "quite" message, currently the number of tracks
* loaded.
*
* @param message
*/
public void setStatusBarMessage(String message) {
if (message.equals("Done.")) {
resetStatusMessage();
}
contentPane.getStatusBar().setMessage(message);
}
/**
* Set the status bar message. If the message equals "Done." intercept
* and reset to the default "quite" message, currently the number of tracks
* loaded.
*
* @param message
*/
public void setStatusBarPosition(String message) {
contentPane.getStatusBar().setMessage2(message);
}
/**
* Resets factory settings. this is not the same as reset user defaults
* DO NOT DELETE used when debugging
*/
public void resetToFactorySettings() {
try {
PreferencesManager.getPreferences().clear();
boolean isShow = PreferencesManager.getPreferences().getAsBoolean(SHOW_ATTRIBUTE_VIEWS_KEY);
doShowAttributeDisplay(isShow);
Preferences prefs = Preferences.userNodeForPackage(Globals.class);
prefs.remove(DirectoryManager.IGV_DIR_USERPREF);
doRefresh();
} catch (Exception e) {
String message = "Failure while resetting preferences!";
log.error(message, e);
MessageUtils.showMessage(message + ": " + e.getMessage());
}
}
public void setFilterMatchAll(boolean value) {
menuBar.setFilterMatchAll(value);
}
public boolean isFilterMatchAll() {
return menuBar.isFilterMatchAll();
}
public void setFilterShowAllTracks(boolean value) {
menuBar.setFilterShowAllTracks(value);
}
public boolean isFilterShowAllTracks() {
return menuBar.isFilterShowAllTracks();
}
/**
* Add a new data panel set
*/
public TrackPanelScrollPane addDataPanel(String name) {
return contentPane.getMainPanel().addDataPanel(name);
}
/**
* Return the panel with the given name. This is called infrequently, and doesn't need to be fast (linear
* search is fine).
*
* @param name
* @return
*/
public TrackPanel getTrackPanel(String name) {
for (TrackPanel sp : getTrackPanels()) {
if (name.equals(sp.getName())) {
return sp;
}
}
// If we get this far this is a new panel
TrackPanelScrollPane sp = addDataPanel(name);
return sp.getTrackPanel();
}
/**
* Return an ordered list of track panels. This method is provided primarily for storing sessions, where
* the track panels need to be stored in order.
*/
public List<TrackPanel> getTrackPanels() {
return contentPane.getMainPanel().getTrackPanels();
}
public boolean scrollToTrack(String trackName) {
for (TrackPanel tp : getTrackPanels()) {
if (tp.getScrollPane().getNamePanel().scrollTo(trackName)) {
return true;
}
}
return false;
}
public Session getSession() {
return session;
}
/**
* Restore a session file, and optionally go to a locus. Called upon startup and from user action.
*
* @param sessionFile
* @param locus
*/
final public void doRestoreSession(final File sessionFile, final String locus) {
if (sessionFile.exists()) {
doRestoreSession(sessionFile.getAbsolutePath(), locus, false);
} else {
String message = "Session file does not exist! : " + sessionFile.getAbsolutePath();
log.error(message);
MessageUtils.showMessage(message);
}
}
/**
* Load a session file, possibly asynchronously (if on the event dispatch thread).
*
* @param sessionPath
* @param locus
* @param merge
*/
public void doRestoreSession(final String sessionPath,
final String locus,
final boolean merge) {
Runnable runnable = new Runnable() {
public void run() {
restoreSessionSynchronous(sessionPath, locus, merge);
}
};
LongRunningTask.submit(runnable);
}
/**
* Load a session file in the current thread. This should not be called from the event dispatch thread.
*
* @param merge
* @param sessionPath
* @param locus
* @return true if successful
*/
public boolean restoreSessionSynchronous(String sessionPath, String locus, boolean merge) {
InputStream inputStream = null;
try {
if (!merge) {
// Do this first, it closes all open SeekableFileStreams.
resetSession(sessionPath);
}
setStatusBarMessage("Opening session...");
inputStream = new BufferedInputStream(ParsingUtils.openInputStreamGZ(new ResourceLocator(sessionPath)));
boolean isUCSC = sessionPath.endsWith(".session") || sessionPath.endsWith(".session.txt");
boolean isIndexAware = sessionPath.endsWith(".idxsession") || sessionPath.endsWith(".idxsession.txt");
final SessionReader sessionReader = isUCSC ?
new UCSCSessionReader(this) :
(isIndexAware ? new IndexAwareSessionReader(this) : new IGVSessionReader(this));
sessionReader.loadSession(inputStream, session, sessionPath);
String searchText = locus == null ? session.getLocus() : locus;
// NOTE: Nothing to do if chr == all
if (!FrameManager.isGeneListMode() && searchText != null &&
!searchText.equals(Globals.CHR_ALL) && searchText.trim().length() > 0) {
goToLocus(searchText);
}
mainFrame.setTitle(UIConstants.APPLICATION_NAME + " - Session: " + sessionPath);
System.gc();
double[] dividerFractions = session.getDividerFractions();
if (dividerFractions != null) {
contentPane.getMainPanel().setDividerFractions(dividerFractions);
}
session.clearDividerLocations();
//If there's a RegionNavigatorDialog, kill it.
//this could be done through the Observer that RND uses, I suppose. Not sure that's cleaner
RegionNavigatorDialog.destroyInstance();
if (!getRecentSessionList().contains(sessionPath)) {
getRecentSessionList().addFirst(sessionPath);
}
doRefresh();
return true;
} catch (Exception e) {
String message = "Error loading session session : <br> " + sessionPath + "<br>" +
e.getMessage();
log.error(message, e);
throw new RuntimeException(e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException iOException) {
log.error("Error closing session stream", iOException);
}
resetStatusMessage();
}
}
}
/**
* Uses either current session.getPersistent, or preferences, depending
* on if IGV has an instance or not. Generally intended for testing
*
* @param key
* @param def
* @return
* @see Session#getPersistent(String, String)
* @see IGVPreferences#getPersistent(String, String)
*/
public static String getPersistent(String key, String def) {
if (IGV.hasInstance()) {
return IGV.getInstance().getSession().getPersistent(key, def);
} else {
return PreferencesManager.getPreferences().getPersistent(key, def);
}
}
/**
* Reset the default status message, which is the number of tracks loaded.
*/
public void resetStatusMessage() {
contentPane.getStatusBar().setMessage("" +
getVisibleTrackCount() + " tracks loaded");
}
public void rebuildGenomeDropdownList() {
GenomeManager.getInstance().buildGenomeItemList();
contentPane.getCommandBar().refreshGenomeListComboBox();
}
public void showLoadedTrackCount() {
final int visibleTrackCount = getVisibleTrackCount();
contentPane.getStatusBar().setMessage("" +
visibleTrackCount + (visibleTrackCount == 1 ? " track" : " tracks"));
}
private void closeWindow(final ProgressBar.ProgressDialog progressDialog) {
UIUtilities.invokeOnEventThread(new Runnable() {
public void run() {
progressDialog.setVisible(false);
}
});
}
/**
* Jump to a locus synchronously. {@code locus} can be any valid search term,
* including gene names. Genomic coordinates (e.g. "chr5:500-1000") are recommended
* Used for port command options
*
* @param locus
* @api
*/
public void goToLocus(String locus) {
contentPane.getCommandBar().searchByLocus(locus);
}
/**
* To to multiple loci, creating a new gene list if required. This method is provided to support control of
* multiple panels from a command or external program.
*
* @param loci
*/
public void goToLociList(List<String> loci) {
List<ReferenceFrame> frames = FrameManager.getFrames();
if (frames.size() == loci.size()) {
for (int i = 0; i < loci.size(); i++) {
frames.get(i).jumpTo(new Locus(loci.get(i)));
}
repaint();
} else {
GeneList geneList = new GeneList("", loci, false);
getSession().setCurrentGeneList(geneList);
resetFrames();
}
}
public void tweakPanelDivider() {
contentPane.getMainPanel().tweakPanelDivider();
}
public void removeDataPanel(String name) {
contentPane.getMainPanel().removeDataPanel(name);
}
public void layoutMainPanel() {
contentPane.getMainPanel().doLayout();
}
public MainPanel getMainPanel() {
return contentPane.getMainPanel();
}
public void setExportingSnapshot(boolean exportingSnapshot) {
isExportingSnapshot = exportingSnapshot;
if (isExportingSnapshot) {
RepaintManager.currentManager(contentPane).setDoubleBufferingEnabled(false);
} else {
RepaintManager.currentManager(contentPane).setDoubleBufferingEnabled(true);
}
}
public LinkedList<String> getRecentSessionList() {
return recentSessionList;
}
public void setRecentSessionList(LinkedList<String> recentSessionList) {
this.recentSessionList = recentSessionList;
}
public IGVContentPane getContentPane() {
return contentPane;
}
public GenomeManager getGenomeManager() {
return genomeManager;
}
JCheckBoxMenuItem showPeakMenuItem;
PeakCommandBar peakCommandBar;
public void addCommandBar(PeakCommandBar cb) {
this.peakCommandBar = cb;
contentPane.add(peakCommandBar);
contentPane.invalidate();
showPeakMenuItem = new JCheckBoxMenuItem("Show peaks toolbar");
showPeakMenuItem.setSelected(true);
showPeakMenuItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent actionEvent) {
if (showPeakMenuItem.isSelected()) {
contentPane.add(peakCommandBar);
} else {
contentPane.remove(peakCommandBar);
}
}
});
menuBar.getViewMenu().addSeparator();
menuBar.getViewMenu().add(showPeakMenuItem);
}
public boolean isShowDetailsOnClick() {
return contentPane != null && contentPane.getCommandBar().getDetailsBehavior() == IGVCommandBar.SHOW_DETAILS_BEHAVIOR.CLICK;
}
public boolean isShowDetailsOnHover() {
return contentPane != null && contentPane.getCommandBar().getDetailsBehavior() == IGVCommandBar.SHOW_DETAILS_BEHAVIOR.HOVER;
}
public void openStatusWindow() {
if (statusWindow == null) {
statusWindow = new StatusWindow();
}
statusWindow.setVisible(true);
}
public void setStatusWindowText(String text) {
if (statusWindow != null && statusWindow.isVisible()) {
statusWindow.updateText(text);
}
}
/**
* Load resources into IGV. Tracks are added to the appropriate panel
* <p>
* NOTE: this must be run on the event tread as UI components are added here.
*
* @param locators
*/
public void loadResources(Collection<ResourceLocator> locators) {
log.info("Loading " + locators.size() + " resources.");
final MessageCollection messages = new MessageCollection();
for (final ResourceLocator locator : locators) {
// If its a local file, check explicitly for existence (rather than rely on exception)
if (locator.isLocal()) {
File trackSetFile = new File(locator.getPath());
if (!trackSetFile.exists()) {
messages.append("File not found: " + locator.getPath() + "\n");
continue;
}
}
try {
List<Track> tracks = load(locator);
addTracks(tracks, locator);
} catch (Exception e) {
log.error("Error loading track", e);
messages.append("Error loading " + locator + ": " + e.getMessage());
}
}
if (!messages.isEmpty()) {
for (String message : messages.getMessages()) {
MessageUtils.showMessage(message);
}
}
}
/**
* Add tracks to the specified panel
*
* @param tracks
* @param panelName
* @api
*/
public void addTracks(List<Track> tracks, PanelName panelName) {
TrackPanel panel = getTrackPanel(panelName.getName());
panel.addTracks(tracks);
doRefresh();
}
/**
* Add the specified tracks to the appropriate panel. Panel
* is chosen based on characteristics of the {@code locator}.
*
* @param tracks
* @param locator
*/
void addTracks(List<Track> tracks, ResourceLocator locator) {
if (tracks.size() > 0) {
String path = locator.getPath();
// Get an appropriate panel. If its a VCF file create a new panel if the number of genotypes
// is greater than 10
TrackPanel panel = getPanelFor(locator);
if (path.endsWith(".vcf") || path.endsWith(".vcf.gz") ||
path.endsWith(".vcf4") || path.endsWith(".vcf4.gz")) {
Track t = tracks.get(0);
if (t instanceof VariantTrack && ((VariantTrack) t).getAllSamples().size() > 10) {
String newPanelName = "Panel" + System.currentTimeMillis();
panel = addDataPanel(newPanelName).getTrackPanel();
}
}
panel.addTracks(tracks);
}
}
/**
* Load a resource and return the tracks.
* Does not automatically add anything
*
* @param locator
* @return A list of loaded tracks
*/
public List<Track> load(ResourceLocator locator) throws DataLoadException {
TrackLoader loader = new TrackLoader();
Genome genome = GenomeManager.getInstance().getCurrentGenome();
List<Track> newTracks = loader.load(locator, genome);
if (newTracks.size() > 0) {
for (Track track : newTracks) {
String fn = locator.getPath();
int lastSlashIdx = fn.lastIndexOf("/");
if (lastSlashIdx < 0) {
lastSlashIdx = fn.lastIndexOf("\\");
}
if (lastSlashIdx > 0) {
fn = fn.substring(lastSlashIdx + 1);
}
track.setAttributeValue(Globals.TRACK_NAME_ATTRIBUTE, track.getName());
track.setAttributeValue(Globals.TRACK_DATA_FILE_ATTRIBUTE, fn);
track.setAttributeValue(Globals.TRACK_DATA_TYPE_ATTRIBUTE, track.getTrackType().toString());
}
}
return newTracks;
}
/**
* Load the data file into the specified panel. Triggered via drag and drop.
*/
public void load(ResourceLocator locator, TrackPanel panel) throws DataLoadException {
// If this is a session TODO -- need better "is a session?" test
if (locator.getPath().endsWith(".xml") || locator.getPath().endsWith(("session"))) {
boolean merge = false; // TODO -- ask user?
this.doRestoreSession(locator.getPath(), null, merge);
} else {
// Not a session, load into target panel
List<Track> tracks = load(locator);
panel.addTracks(tracks);
doRefresh();
}
}
/**
* Return a DataPanel appropriate for the resource type
*
* @param locator
* @return
*/
public TrackPanel getPanelFor(ResourceLocator locator) {
String path = locator.getPath().toLowerCase();
if ("alist".equals(locator.getType())) {
return getVcfBamPanel();
} else if (PreferencesManager.getPreferences().getAsBoolean(SHOW_SINGLE_TRACK_PANE_KEY)) {
return getTrackPanel(DATA_PANEL_NAME);
} else if (TrackLoader.isAlignmentTrack(locator.getTypeString())) {
String newPanelName = "Panel" + System.currentTimeMillis();
return addDataPanel(newPanelName).getTrackPanel();
} else {
return getDefaultPanel(locator);
}
}
public Set<TrackType> getLoadedTypes() {
Set<TrackType> types = new HashSet();
for (Track t : getAllTracks()) {
TrackType type = t.getTrackType();
if (t != null) {
types.add(type);
}
}
return types;
}
/**
* Experimental method to support VCF -> BAM coupling
*
* @return
*/
public TrackPanel getVcfBamPanel() {
String panelName = "VCF_BAM";
TrackPanel panel = getTrackPanel(panelName);
if (panel != null) {
return panel;
} else {
return addDataPanel(panelName).getTrackPanel();
}
}
private TrackPanel getDefaultPanel(ResourceLocator locator) {
if (locator.getType() != null && locator.getType().equalsIgnoreCase("das")) {
return getTrackPanel(FEATURE_PANEL_NAME);
}
String filename = locator.getPath().toLowerCase();
if (filename.endsWith(".txt") || filename.endsWith(".tab") || filename.endsWith(
".xls") || filename.endsWith(".gz")) {
filename = filename.substring(0, filename.lastIndexOf("."));
}
if (isAnnotationFile(filename)) {
return getTrackPanel(FEATURE_PANEL_NAME);
} else {
return getTrackPanel(DATA_PANEL_NAME);
}
}
private boolean isAnnotationFile(String filename) {
return filename.contains("refflat") || filename.contains("ucscgene") ||
filename.contains("genepred") || filename.contains("ensgene") ||
filename.contains("refgene") ||
filename.endsWith("gff") || filename.endsWith("gtf") ||
filename.endsWith("gff3") || filename.endsWith("embl") ||
filename.endsWith("bed") || filename.endsWith("gistic") ||
filename.endsWith("bedz") || filename.endsWith("repmask") ||
filename.contains("dranger") || filename.endsWith("ucscsnp");
}
public void reset() {
groupByAttribute = null;
for (TrackPanel sp : getTrackPanels()) {
if (DATA_PANEL_NAME.equals(sp.getName())) {
sp.reset();
break;
}
}
}
public boolean sortAlignmentTracks(AlignmentTrack.SortOption option, String tag) {
return sortAlignmentTracks(option, null, tag);
}
public boolean sortAlignmentTracks(AlignmentTrack.SortOption option, Double location, String tag) {
double actloc;
boolean toRet = true;
for (Track t : getAllTracks()) {
if (t instanceof AlignmentTrack) {
for (ReferenceFrame frame : FrameManager.getFrames()) {
actloc = location != null ? location : frame.getCenter();
toRet &= ((AlignmentTrack) t).sortRows(option, frame, actloc, tag);
}
}
}
return toRet;
}
/**
* Group all alignment tracks by the specified option.
*
* @param option
* @api
*/
public void groupAlignmentTracks(AlignmentTrack.GroupOption option, String tag, Range pos) {
final IGVPreferences prefMgr = PreferencesManager.getPreferences();
prefMgr.put(SAM_GROUP_OPTION, option.toString());
if (option == AlignmentTrack.GroupOption.TAG && tag != null) {
prefMgr.put(SAM_GROUP_BY_TAG, tag);
}
if (option == AlignmentTrack.GroupOption.BASE_AT_POS && pos != null) {
prefMgr.put(SAM_GROUP_BY_POS, pos.getChr() + " " + String.valueOf(pos.getStart()));
}
for (Track t : getAllTracks()) {
if (t instanceof AlignmentTrack) {
((AlignmentTrack) t).groupAlignments(option, tag, pos);
}
}
}
public void packAlignmentTracks() {
for (Track t : getAllTracks()) {
if (t instanceof AlignmentTrack) {
((AlignmentTrack) t).packAlignments();
}
}
}
public void setTrackDisplayMode(Track.DisplayMode mode, String trackName) {
for (Track t : getAllTracks()) {
if (trackName == null || t.getName().equals(trackName)) {
t.setDisplayMode(mode);
}
}
}
/**
* Reset the overlay tracks collection. Currently the only overlayable track
* type is Mutation. This method finds all mutation tracks and builds a map
* of key -> mutation track, where the key is the specified attribute value
* for linking tracks for overlay.
*/
public void resetOverlayTracks() {
log.debug("Resetting Overlay Tracks");
overlayTracksMap.clear();
overlaidTracks.clear();
// Old option to allow overlaying based on an arbitrary attribute.
// String overlayAttribute = igv.getSession().getOverlayAttribute();
for (Track track : getAllTracks()) {
if (track != null && track.getTrackType() == TrackType.MUTATION) {
String sample = track.getSample();
if (sample != null) {
List<Track> trackList = overlayTracksMap.get(sample);
if (trackList == null) {
trackList = new ArrayList();
overlayTracksMap.put(sample, trackList);
}
trackList.add(track);
}
}
}
for (Track track : getAllTracks()) {
if (track != null) { // <= this should not be neccessary
if (track.getTrackType() != TrackType.MUTATION) {
String sample = track.getSample();
if (sample != null) {
List<Track> trackList = overlayTracksMap.get(sample);
if (trackList != null) overlaidTracks.addAll(trackList);
}
}
}
}
boolean displayOverlays = getSession().getOverlayMutationTracks();
for (Track track : getAllTracks()) {
if (track != null) {
if (track.getTrackType() == TrackType.MUTATION) {
track.setOverlayed(displayOverlays && overlaidTracks.contains(track));
}
}
}
}
/**
* Return tracks overlaid on "track"
* // TODO -- why aren't overlaid tracks stored in a track member? This seems unnecessarily complex
*
* @param track
* @return
*/
public List<Track> getOverlayTracks(Track track) {
String sample = track.getSample();
if (sample != null) {
return overlayTracksMap.get(sample);
}
return null;
}
public int getVisibleTrackCount() {
int count = 0;
for (TrackPanel tsv : getTrackPanels()) {
count += tsv.getVisibleTrackCount();
}
return count;
}
/**
* Return the list of all tracks in the order they appear on the screen
*
* @return
*/
public List<Track> getAllTracks() {
List<Track> allTracks = new ArrayList<Track>();
for (TrackPanel tp : getTrackPanels()) {
allTracks.addAll(tp.getTracks());
}
return allTracks;
}
public List<FeatureTrack> getFeatureTracks() {
Iterable<FeatureTrack> featureTracksIter = Iterables.filter(getAllTracks(), FeatureTrack.class);
List<FeatureTrack> featureTracks = Lists.newArrayList(featureTracksIter);
return featureTracks;
}
public List<DataTrack> getDataTracks() {
Iterable<DataTrack> dataTracksIter = Iterables.filter(getAllTracks(), DataTrack.class);
List<DataTrack> dataTracks = Lists.newArrayList(dataTracksIter);
return dataTracks;
}
public void clearSelections() {
for (Track t : getAllTracks()) {
if (t != null)
t.setSelected(false);
}
}
public void setTrackSelections(Iterable<Track> selectedTracks) {
for (Track t : selectedTracks) {
t.setSelected(true);
}
}
public void shiftSelectTracks(Track track) {
List<Track> allTracks = getAllTracks();
int clickedTrackIndex = allTracks.indexOf(track);
// Find another track that is already selected. The semantics of this
// are not well defined, so any track will do
int otherIndex = clickedTrackIndex;
for (int i = 0; i < allTracks.size(); i++) {
if (allTracks.get(i).isSelected() && i != clickedTrackIndex) {
otherIndex = i;
break;
}
}
int left = Math.min(otherIndex, clickedTrackIndex);
int right = Math.max(otherIndex, clickedTrackIndex);
for (int i = left; i <= right; i++) {
Track t = allTracks.get(i);
if (t.isVisible()) {
t.setSelected(true);
}
}
}
public void toggleTrackSelections(Iterable<Track> selectedTracks) {
for (Track t : selectedTracks) {
t.setSelected(!t.isSelected());
}
}
public List<Track> getSelectedTracks() {
ArrayList<Track> selectedTracks = new ArrayList();
for (Track t : getAllTracks()) {
if (t != null && t.isSelected()) {
selectedTracks.add(t);
}
}
return selectedTracks;
}
/**
* Return the complete set of unique DataResourceLocators currently loaded
*
* @return
*/
public Set<ResourceLocator> getDataResourceLocators() {
HashSet<ResourceLocator> locators = new HashSet();
for (Track track : getAllTracks()) {
Collection<ResourceLocator> tlocators = track.getResourceLocators();
if (tlocators != null) {
locators.addAll(tlocators);
}
}
locators.remove(null);
return locators;
}
public void setAllTrackHeights(int newHeight) {
for (Track track : getAllTracks()) {
track.setHeight(newHeight, true);
}
}
public void removeTracks(Collection<? extends Track> tracksToRemove) {
// Make copy of list as we will be modifying the original in the loop
List<TrackPanel> panels = getTrackPanels();
for (TrackPanel trackPanel : panels) {
trackPanel.removeTracks(tracksToRemove);
if (!trackPanel.hasTracks()) {
removeDataPanel(trackPanel.getName());
}
}
for (Track t : tracksToRemove) {
if (t instanceof IGVEventObserver) {
IGVEventBus.getInstance().unsubscribe((IGVEventObserver) t);
}
}
for (Track t : tracksToRemove) {
t.dispose();
}
}
/**
* Add gene and sequence tracks. This is called upon switching genomes.
*
* @param newGeneTrack
* @param
*/
public void setGenomeTracks(FeatureTrack newGeneTrack) {
TrackPanel panel = PreferencesManager.getPreferences().getAsBoolean(SHOW_SINGLE_TRACK_PANE_KEY) ?
getTrackPanel(DATA_PANEL_NAME) : getTrackPanel(FEATURE_PANEL_NAME);
SequenceTrack newSeqTrack = new SequenceTrack("Reference sequence");
panel.addTrack(newSeqTrack);
if (newGeneTrack != null) {
panel.addTrack(newGeneTrack);
}
}
public boolean hasGeneTrack() {
FeatureTrack geneTrack = GenomeManager.getInstance().getCurrentGenome().getGeneTrack();
if (geneTrack == null) return false;
for (Track t : getFeatureTracks()) {
if (geneTrack == t) return true;
}
return false;
}
public boolean hasSequenceTrack() {
return getSequenceTrack() != null;
}
/**
* @return First SequenceTrack found, or null if none
*/
public SequenceTrack getSequenceTrack() {
for (Track t : getAllTracks()) {
if (t instanceof SequenceTrack) return (SequenceTrack) t;
}
return null;
}
/////////////////////////////////////////////////////////////////////////////////////////
// Sorting
/**
* Sort all groups (data and feature) by attribute value(s). Tracks are
* sorted within groups.
*
* @param attributeNames
* @param ascending
*/
public void sortAllTracksByAttributes(final String attributeNames[], final boolean[] ascending) {
assert attributeNames.length == ascending.length;
for (TrackPanel trackPanel : getTrackPanels()) {
trackPanel.sortTracksByAttributes(attributeNames, ascending);
}
}
/**
* Sort all groups (data and feature) by a computed score over a region. The
* sort is done twice (1) groups are sorted with the featureGroup, and (2) the
* groups themselves are sorted.
*
* @param region
* @param type
* @param frame
*/
public void sortByRegionScore(RegionOfInterest region,
final RegionScoreType type,
final ReferenceFrame frame) {
final RegionOfInterest r = region == null ? new RegionOfInterest(frame.getChrName(), (int) frame.getOrigin(),
(int) frame.getEnd() + 1, frame.getName()) : region;
// Create a rank order of samples. This is done globally so sorting is consistent across groups and panels.
final List<String> sortedSamples = sortSamplesByRegionScore(r, type, frame);
for (TrackPanel trackPanel : getTrackPanels()) {
trackPanel.sortByRegionsScore(r, type, frame, sortedSamples);
}
revalidateTrackPanels();
}
/**
* Sort a collection of tracks by a score over a region.
*
* @param region
* @param type
* @param frame
*/
private List<String> sortSamplesByRegionScore(final RegionOfInterest region,
final RegionScoreType type,
final ReferenceFrame frame) {
// Get the sortable tracks for this score (data) type
final List<Track> allTracks = getAllTracks();
final List<Track> tracksWithScore = new ArrayList(allTracks.size());
for (Track t : allTracks) {
if (t.isRegionScoreType(type)) {
tracksWithScore.add(t);
}
}
// Sort the "sortable" tracks
sortByRegionScore(tracksWithScore, region, type, frame);
// Now get sample order from sorted tracks, use to sort (tracks which do not implement the selected "sort by" score)
List<String> sortedSamples = new ArrayList(tracksWithScore.size());
for (Track t : tracksWithScore) {
String att = t.getSample(); //t.getAttributeValue(linkingAtt);
if (att != null) {
sortedSamples.add(att);
}
}
return sortedSamples;
}
static void sortByRegionScore(List<Track> tracks,
final RegionOfInterest region,
final RegionScoreType type,
ReferenceFrame frame) {
if ((tracks != null) && (region != null) && !tracks.isEmpty()) {
final String frameName = frame != null ? frame.getName() : null;
int tmpzoom = frame != null ? frame.getZoom() : 0;
final int zoom = Math.max(0, tmpzoom);
final String chr = region.getChr();
final int start = region.getStart();
final int end = region.getEnd();
Comparator<Track> c = new Comparator<Track>() {
public int compare(Track t1, Track t2) {
try {
if (t1 == null && t2 == null) return 0;
if (t1 == null) return 1;
if (t2 == null) return -1;
float s1 = t1.getRegionScore(chr, start, end, zoom, type, frameName);
float s2 = t2.getRegionScore(chr, start, end, zoom, type, frameName);
return Float.compare(s2, s1);
} catch (Exception e) {
log.error("Error sorting tracks. Sort might not be accurate.", e);
return 0;
}
}
};
Collections.sort(tracks, c);
}
}
////////////////////////////////////////////////////////////////////////////////////////
// Groups
public String getGroupByAttribute() {
return groupByAttribute;
}
public void setGroupByAttribute(String attributeName) {
groupByAttribute = attributeName;
resetGroups();
// Some tracks need to respond to changes in grouping, fire notification event
IGVEventBus.getInstance().post(new TrackGroupEvent(this));
}
private void resetGroups() {
log.debug("Resetting Groups");
for (TrackPanel trackPanel : getTrackPanels()) {
trackPanel.groupTracksByAttribute(groupByAttribute);
}
}
//////////////////////////////////////////////////////////////////////////////////////////
// Startup
public Future startUp(Main.IGVArgs igvArgs) {
if (log.isDebugEnabled()) {
log.debug("startUp");
}
return LongRunningTask.submit(new StartupRunnable(igvArgs));
}
public void setRulerEnabled(boolean rulerEnabled) {
this.rulerEnabled = rulerEnabled;
}
public boolean isRulerEnabled() {
return rulerEnabled;
}
/**
* Swing worker class to startup IGV
*/
public class StartupRunnable implements Runnable {
Main.IGVArgs igvArgs;
ProgressBar.ProgressDialog progressDialog;
StartupRunnable(Main.IGVArgs args) {
this.igvArgs = args;
}
@Override
public void run() {
final boolean runningBatch = igvArgs.getBatchFile() != null;
BatchRunner.setIsBatchMode(runningBatch);
final ProgressMonitor monitor = new ProgressMonitor();
UIUtilities.invokeAndWaitOnEventThread(() -> {
progressDialog = ProgressBar.showProgressDialog(mainFrame, "Initializing...", monitor, false);
progressDialog.getProgressBar().setIndeterminate(true);
monitor.fireProgressChange(20);
});
UIUtilities.invokeOnEventThread(() -> mainFrame.setIconImage(getIconImage()));
if (Globals.IS_MAC) {
setAppleDockIcon();
}
final IGVPreferences preferenceManager = PreferencesManager.getPreferences();
try {
contentPane.getCommandBar().initializeGenomeList(monitor);
} catch (FileNotFoundException ex) {
JOptionPane.showMessageDialog(mainFrame, "Error initializing genome list: " + ex.getMessage());
log.error("Error initializing genome list: ", ex);
} catch (NoRouteToHostException ex) {
JOptionPane.showMessageDialog(mainFrame, "Network error initializing genome list: " + ex.getMessage());
log.error("Network error initializing genome list: ", ex);
} finally {
UIUtilities.invokeAndWaitOnEventThread(() -> monitor.fireProgressChange(50));
closeWindow(progressDialog);
}
if (igvArgs.getGenomeId() != null) {
GenomeManager.getInstance().loadGenomeById(igvArgs.getGenomeId());
} else if (igvArgs.getSessionFile() == null) {
String genomeId = preferenceManager.getDefaultGenome();
contentPane.getCommandBar().selectGenome(genomeId);
}
//Load plugins
//Do this before loading files, hooks might need to be inserted
initIGVPlugins();
//If there is an argument assume it is a session file or url
if (igvArgs.getSessionFile() != null || igvArgs.getDataFileString() != null) {
if (log.isDebugEnabled()) {
log.debug("Loading session data");
}
final IndefiniteProgressMonitor indefMonitor = new IndefiniteProgressMonitor();
final ProgressBar.ProgressDialog progressDialog2 = ProgressBar.showProgressDialog(mainFrame, "Loading session data", indefMonitor, false);
indefMonitor.start();
if (log.isDebugEnabled()) {
log.debug("Calling restore session");
}
if (igvArgs.getSessionFile() != null) {
boolean success = false;
if (HttpUtils.isRemoteURL(igvArgs.getSessionFile())) {
boolean merge = false;
success = restoreSessionSynchronous(igvArgs.getSessionFile(), igvArgs.getLocusString(), merge);
} else {
File sf = new File(igvArgs.getSessionFile());
if (sf.exists()) {
success = restoreSessionSynchronous(sf.getAbsolutePath(), igvArgs.getLocusString(), false);
}
}
if (!success) {
String genomeId = preferenceManager.getDefaultGenome();
contentPane.getCommandBar().selectGenome(genomeId);
}
} else if (igvArgs.getDataFileString() != null) {
// Not an xml file, assume its a list of data files
String decodedString = URLDecoder.decode(igvArgs.getDataFileString());
String[] dataFiles = decodedString.split(",");
String[] names = null;
if (igvArgs.getName() != null) {
names = igvArgs.getName().split(",");
}
String[] indexFiles = null;
if (igvArgs.getIndexFile() != null) {
indexFiles = igvArgs.getIndexFile().split(",");
}
String[] coverageFiles = null;
if (igvArgs.getCoverageFile() != null) {
coverageFiles = igvArgs.getCoverageFile().split(",");
}
List<ResourceLocator> locators = new ArrayList();
for (int i = 0; i < dataFiles.length; i++) {
String p = dataFiles[i].trim();
// Decode local file paths
if (HttpUtils.isURL(p) && !FileUtils.isRemote(p)) {
p = StringUtils.decodeURL(p);
}
ResourceLocator rl = new ResourceLocator(p);
if (names != null && i < names.length) {
String name = names[i];
rl.setName(name);
}
//Set index file, iff one was passed
if (indexFiles != null && i < indexFiles.length) {
String idxP = indexFiles[i];
if (HttpUtils.isURL(idxP) && !FileUtils.isRemote(idxP)) {
idxP = StringUtils.decodeURL(idxP);
}
if (idxP.length() > 0) {
rl.setIndexPath(idxP);
}
}
//Set coverage file, iff one was passed
if (coverageFiles != null && i < coverageFiles.length) {
String covP = coverageFiles[i];
if (HttpUtils.isURL(covP) && !FileUtils.isRemote(covP)) {
covP = StringUtils.decodeURL(covP);
}
if (covP.length() > 0) {
rl.setCoverage(covP);
}
}
locators.add(rl);
}
loadTracks(locators);
}
indefMonitor.stop();
closeWindow(progressDialog2);
}
session.recordHistory();
// Start up a port listener. Port # can be overriden with "-p" command line switch
boolean portEnabled = preferenceManager.getAsBoolean(PORT_ENABLED);
String portString = igvArgs.getPort();
if (portEnabled || portString != null) {
// Command listener thread
int port = preferenceManager.getAsInt(PORT_NUMBER);
if (portString != null) {
port = Integer.parseInt(portString);
}
CommandListener.start(port);
}
UIUtilities.invokeOnEventThread(new Runnable() {
public void run() {
mainFrame.setVisible(true);
if (igvArgs.getLocusString() != null) {
goToLocus(igvArgs.getLocusString());
}
if (runningBatch) {
Globals.setBatch(false); // Set to false for startup execution -- otherwise we block the event thread
LongRunningTask.submit(new BatchRunner(igvArgs.getBatchFile()));
}
}
});
synchronized (IGV.getInstance()) {
IGV.getInstance().notifyAll();
}
}
private void setAppleDockIcon() {
try {
Image image = getIconImage();
OSXAdapter.setDockIconImage(image);
} catch (Exception e) {
log.error("Error setting apple dock icon", e);
}
}
private Image getIconImage() {
String path = "resources/IGV_64.png";
URL url = IGV.class.getResource(path);
Image image = new ImageIcon(url).getImage();
return image;
}
private void initIGVPlugins() {
List<String> pluginClassNames = new ArrayList<String>(2);
InputStream is = IGV.class.getResourceAsStream("resources/builtin_plugin_list.txt");
if (is != null) {
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String line = null;
try {
while ((line = br.readLine()) != null) {
if (line.startsWith("##")) continue;
pluginClassNames.add(line);
}
} catch (IOException e) {
log.error("Error reading builtin plugin list", e);
}
}
pluginClassNames.addAll(Arrays.asList(PreferencesManager.getPreferences().getIGVPluginList()));
for (String classname : pluginClassNames) {
if (classname.startsWith("#")) continue;
try {
Class clazz = Class.forName(classname);
IGVPlugin plugin = (IGVPlugin) clazz.newInstance();
plugin.init();
} catch (Exception e) {
log.error("Error loading " + classname, e);
}
}
}
}
public static void copySequenceToClipboard(Genome genome, String chr, int start, int end, Strand strand) {
try {
IGV.getMainFrame().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
byte[] seqBytes = genome.getSequence(chr, start, end);
if (seqBytes == null) {
MessageUtils.showMessage("Sequence not available");
} else {
String sequence = new String(seqBytes);
SequenceTrack sequenceTrack = IGV.getInstance().getSequenceTrack();
if (strand == Strand.NEGATIVE || (sequenceTrack != null && sequenceTrack.getStrand() == Strand.NEGATIVE)) {
sequence = SequenceTrack.getReverseComplement(sequence);
}
StringUtils.copyTextToClipboard(sequence);
}
} finally {
IGV.getMainFrame().setCursor(Cursor.getDefaultCursor());
}
}
/**
* Wrapper for igv.wait(timeout). Used during unit tests.
*
* @param timeout
* @return True if method completed before interruption (not necessarily before timeout), otherwise false
*/
public boolean waitForNotify(long timeout) {
boolean completed = false;
synchronized (this) {
while (!completed) {
try {
this.wait(timeout);
completed = true;
} catch (InterruptedException e) {
}
break;
}
}
return completed;
}
public void receiveEvent(Object event) {
if (event instanceof ViewChange || event instanceof InsertionSelectionEvent) {
revalidateTrackPanels(); // TODO -- this seems extreme
} else if (event instanceof ShiftEvent) {
revalidateTrackPanels();
} else if (event instanceof GenomeChangeEvent) {
doRefresh();
} else {
log.info("Unknown event type: " + event.getClass());
}
}
public void repaint() {
mainFrame.repaint();
}
public void resetFrames() {
contentPane.getMainPanel().headerPanelContainer.createHeaderPanels();
for (TrackPanel tp : getTrackPanels()) {
tp.createDataPanels();
}
contentPane.getCommandBar().setGeneListMode(FrameManager.isGeneListMode());
contentPane.getMainPanel().applicationHeaderPanel.revalidate();
contentPane.getMainPanel().validate();
contentPane.getMainPanel().repaint();
}
final public void doRefresh() {
contentPane.getMainPanel().revalidate();
mainFrame.repaint();
getContentPane().repaint();
}
/**
* Repaint the header and data panels.
* <p/>
* Note: If running in Batch mode we force synchronous painting. This is necessary as the
* paint() command triggers loading of data. If allowed to proceed asynchronously the "snapshot" batch command
* might execute before the data from a previous command has loaded.
*/
public void revalidateTrackPanels() {
UIUtilities.invokeOnEventThread(() -> {
if (Globals.isBatch()) {
contentPane.revalidateTrackPanels();
rootPane.paintImmediately(rootPane.getBounds());
} else {
contentPane.revalidateTrackPanels();
rootPane.repaint();
}
});
}
public void repaintNamePanels() {
for (TrackPanel tp : getTrackPanels()) {
tp.getScrollPane().getNamePanel().repaint();
}
}
//
// NOTE: MAC ONLY, WILL NOT COMPILE ON OTHER PLATFORMS
// private void getRealDPI() {
// // find the display device of interest
// final GraphicsDevice defaultScreenDevice = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
//
// // on OS X, it would be CGraphicsDevice
// if (defaultScreenDevice instanceof CGraphicsDevice) {
// final CGraphicsDevice device = (CGraphicsDevice) defaultScreenDevice;
//
// // this is the missing correction factor, it's equal to 2 on HiDPI a.k.a. Retina displays
// final int scaleFactor = device.getScaleFactor();
//
// // now we can compute the real DPI of the screen
// final double realDPI = scaleFactor * (device.getXResolution() + device.getYResolution()) / 2;
//
// System.out.println("scale factor = " + scaleFactor + " realDPI = " + realDPI);
// }
// }
}