/* * 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.track; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import htsjdk.tribble.Feature; import org.apache.commons.math.stat.StatUtils; import org.apache.log4j.Logger; import org.broad.igv.Globals; import org.broad.igv.data.AbstractDataSource; import org.broad.igv.data.CombinedDataSource; import org.broad.igv.feature.*; import org.broad.igv.feature.Range; import org.broad.igv.feature.basepair.BasePairTrack; import org.broad.igv.feature.genome.Genome; import org.broad.igv.feature.genome.GenomeManager; import org.broad.igv.feature.tribble.IGVBEDCodec; import org.broad.igv.prefs.Constants; import org.broad.igv.prefs.PreferencesManager; import org.broad.igv.renderer.*; import org.broad.igv.sam.AlignmentDataManager; import org.broad.igv.sam.AlignmentTrack; import org.broad.igv.sam.CoverageTrack; import org.broad.igv.sam.SAMWriter; import org.broad.igv.ui.*; import org.broad.igv.ui.color.ColorUtilities; import org.broad.igv.ui.panel.FrameManager; import org.broad.igv.ui.panel.IGVPopupMenu; import org.broad.igv.ui.panel.ReferenceFrame; import org.broad.igv.ui.panel.TrackPanel; import org.broad.igv.ui.util.FileDialogUtils; import org.broad.igv.ui.util.MessageUtils; import org.broad.igv.ui.util.UIUtilities; import org.broad.igv.util.LongRunningTask; import org.broad.igv.util.ResourceLocator; import org.broad.igv.util.StringUtils; import org.broad.igv.util.blat.BlatClient; import org.broad.igv.util.collections.CollUtils; import org.broad.igv.util.extview.ExtendViewClient; import org.broad.igv.util.stats.KMPlotFrame; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.*; import java.util.*; import java.util.List; /** * @author jrobinso */ public class TrackMenuUtils { static Logger log = Logger.getLogger(TrackMenuUtils.class); final static String LEADING_HEADING_SPACER = " "; private static List<TrackMenuItemBuilder> trackMenuItems = new ArrayList<TrackMenuItemBuilder>(); /** * Called by plugins to add a listener, which is then called when TrackMenus are created * to generate menu entries. * * @param builder * @api */ public static void addTrackMenuItemBuilder(TrackMenuItemBuilder builder) { trackMenuItems.add(builder); } /** * Return a popup menu with items applicable to the collection of tracks. * * @param tracks * @return */ public static IGVPopupMenu getPopupMenu(final Collection<Track> tracks, String title, TrackClickEvent te) { if (log.isDebugEnabled()) { log.debug("enter getPopupMenu"); } IGVPopupMenu menu = new IGVPopupMenu(); JLabel popupTitle = new JLabel(LEADING_HEADING_SPACER + title, JLabel.CENTER); popupTitle.setFont(UIConstants.boldFont); if (popupTitle != null) { menu.add(popupTitle); menu.addSeparator(); } addStandardItems(menu, tracks, te); return menu; } /** * Add menu items which have been added through the api, not known until runtime * * @param menu * @param tracks * @param te */ public static void addPluginItems(JPopupMenu menu, Collection<Track> tracks, TrackClickEvent te) { List<JMenuItem> items = new ArrayList<JMenuItem>(0); for (TrackMenuItemBuilder builder : trackMenuItems) { JMenuItem item = builder.build(tracks, te); if (item != null) { items.add(item); } } if (items.size() > 0) { menu.addSeparator(); for (JMenuItem item : items) { menu.add(item); } } } public static void addStandardItems(JPopupMenu menu, Collection<Track> tracks, TrackClickEvent te) { boolean hasDataTracks = false; boolean hasFeatureTracks = false; boolean hasOtherTracks = false; boolean hasCoverageTracks = false; for (Track track : tracks) { if (track instanceof DataTrack) { hasDataTracks = true; } else if (track instanceof CoverageTrack) { hasDataTracks = true; hasCoverageTracks = true; } else if (track instanceof FeatureTrack) { hasFeatureTracks = true; } else { hasOtherTracks = true; } if (hasDataTracks && hasFeatureTracks && hasOtherTracks) { break; } } boolean hasBasePairTracks = false; for (Track track : tracks) { if (track instanceof BasePairTrack) { hasBasePairTracks = true; break; } } if (hasBasePairTracks) { addBasePairItems(menu, tracks); } boolean featureTracksOnly = hasFeatureTracks && !hasDataTracks && !hasOtherTracks; boolean dataTracksOnly = !hasFeatureTracks && hasDataTracks && !hasOtherTracks; addSharedItems(menu, tracks, hasFeatureTracks, hasCoverageTracks); menu.addSeparator(); if (dataTracksOnly) { addDataItems(menu, tracks, hasCoverageTracks); } else if (featureTracksOnly) { addFeatureItems(menu, tracks, te); } } public static void addZoomItems(JPopupMenu menu, final ReferenceFrame frame) { if (FrameManager.isGeneListMode()) { JMenuItem item = new JMenuItem("Reset panel to '" + frame.getName() + "'"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { frame.reset(); // TODO -- paint only panels for this frame } }); menu.add(item); } JMenuItem zoomOutItem = new JMenuItem("Zoom out"); zoomOutItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { frame.doZoomIncrement(-1); } }); menu.add(zoomOutItem); JMenuItem zoomInItem = new JMenuItem("Zoom in"); zoomInItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { frame.doZoomIncrement(1); } }); menu.add(zoomInItem); } /** * Return popup menu with items applicable to data tracks * * @return */ public static void addDataItems(JPopupMenu menu, final Collection<Track> tracks, boolean hasCoverageTracks) { if (log.isTraceEnabled()) { log.trace("enter getDataPopupMenu"); } if (!hasCoverageTracks) { // The "Points" renderer cannot be used with final String[] labels = {"Heatmap", "Bar Chart", "Points", "Line Plot"}; final Class[] renderers = {HeatmapRenderer.class, BarChartRenderer.class, PointsRenderer.class, LineplotRenderer.class }; JLabel rendererHeading = new JLabel(LEADING_HEADING_SPACER + "Type of Graph", JLabel.LEFT); rendererHeading.setFont(UIConstants.boldFont); menu.add(rendererHeading); // Get existing selections Set<Class> currentRenderers = new HashSet<Class>(); for (Track track : tracks) { if (track.getRenderer() != null) { currentRenderers.add(track.getRenderer().getClass()); } } // Create renderer menu items for (int i = 0; i < labels.length; i++) { JCheckBoxMenuItem item = new JCheckBoxMenuItem(labels[i]); final Class rendererClass = renderers[i]; if (currentRenderers.contains(rendererClass)) { item.setSelected(true); } item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { changeRenderer(tracks, rendererClass); } }); menu.add(item); } menu.addSeparator(); // Get intersection of all valid window functions for selected tracks Set<WindowFunction> avaibleWindowFunctions = new LinkedHashSet<>(); avaibleWindowFunctions.addAll(AbstractDataSource.ORDERED_WINDOW_FUNCTIONS); for (Track track : tracks) { avaibleWindowFunctions.retainAll(track.getAvailableWindowFunctions()); } // dataPopupMenu.addSeparator(); // Collection all window functions for selected tracks WindowFunction currentWindowFunction = null; for (Track track : tracks) { final WindowFunction twf = track.getWindowFunction(); if (currentWindowFunction == null) { currentWindowFunction = twf; } else { if (twf != currentWindowFunction) { currentWindowFunction = null; // Multiple window functions break; } } } if (avaibleWindowFunctions.size() > 0) { JLabel statisticsHeading = new JLabel(LEADING_HEADING_SPACER + "Windowing Function", JLabel.LEFT); statisticsHeading.setFont(UIConstants.boldFont); menu.add(statisticsHeading); for (final WindowFunction wf : avaibleWindowFunctions) { JCheckBoxMenuItem item = new JCheckBoxMenuItem(wf.getValue()); item.setSelected(currentWindowFunction == wf); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { changeStatType(wf.toString(), tracks); } }); menu.add(item); } menu.addSeparator(); } } menu.add(getDataRangeItem(tracks)); if (!hasCoverageTracks) menu.add(getHeatmapScaleItem(tracks)); if (tracks.size() > 0) { menu.add(getLogScaleItem(tracks)); } menu.add(getAutoscaleItem(tracks)); if (tracks.size() > 1 || (tracks.size() == 1 && tracks.iterator().next() instanceof MergedTracks)) { menu.add(getGroupAutoscaleItem(tracks)); } menu.add(getShowDataRangeItem(tracks)); //Optionally add overlay track options Track firstTrack = tracks.iterator().next(); boolean merged = (tracks.size() == 1 && firstTrack instanceof MergedTracks); if (tracks.size() > 1 || merged) { menu.addSeparator(); final List<DataTrack> dataTrackList = Lists.newArrayList(Iterables.filter(tracks, DataTrack.class)); final JMenuItem overlayGroups = new JMenuItem("Overlay Tracks"); overlayGroups.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { MergedTracks mergedTracks = new MergedTracks(UUID.randomUUID().toString(), "Overlay", dataTrackList); Track firstTrack = tracks.iterator().next(); TrackPanel panel = TrackPanel.getParentPanel(firstTrack); panel.addTrack(mergedTracks); panel.moveSelectedTracksTo(Arrays.asList(mergedTracks), firstTrack, false); panel.removeTracks(tracks); } }); int numDataTracks = dataTrackList.size(); overlayGroups.setEnabled(numDataTracks >= 2 && numDataTracks == tracks.size()); menu.add(overlayGroups); // Enable "separateTracks" menu if selection is a single track, and that track is merged. JMenuItem unmergeItem = new JMenuItem("Separate Tracks"); menu.add(unmergeItem); if (merged) { unmergeItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { Track firstTrack = tracks.iterator().next(); TrackPanel panel = TrackPanel.getParentPanel(firstTrack); final MergedTracks mergedTracks = (MergedTracks) firstTrack; mergedTracks.setTrackAlphas(255); panel.addTracks(mergedTracks.getMemberTracks()); panel.moveSelectedTracksTo(mergedTracks.getMemberTracks(), mergedTracks, true); IGV.getInstance().removeTracks(Arrays.asList(mergedTracks)); } }); } else { unmergeItem.setEnabled(false); } } //menu.addSeparator(); //menu.add(getChangeKMPlotItem(tracks)); if (Globals.isDevelopment() && FrameManager.isGeneListMode() && tracks.size() == 1) { menu.addSeparator(); menu.add(getShowSortFramesItem(tracks.iterator().next())); } } private static List<JMenuItem> getCombinedDataSourceItems(final Collection<Track> tracks) { Iterable<DataTrack> dataTracksIter = Iterables.filter(tracks, DataTrack.class); final List<DataTrack> dataTracks = Lists.newArrayList(dataTracksIter); JMenuItem addItem = new JMenuItem("Sum Tracks"); JMenuItem subItem = new JMenuItem("Subtract Tracks"); boolean enableComb = dataTracks.size() == 2; addItem.setEnabled(enableComb); addItem.setEnabled(enableComb); addItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { addCombinedDataTrack(dataTracks, CombinedDataSource.Operation.ADD); } }); subItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { addCombinedDataTrack(dataTracks, CombinedDataSource.Operation.SUBTRACT); } }); return Arrays.asList(addItem, subItem); } private static void addCombinedDataTrack(List<DataTrack> dataTracks, CombinedDataSource.Operation op) { String text = ""; switch (op) { case ADD: text = "Sum"; break; case SUBTRACT: text = "Difference"; break; } DataTrack track0 = dataTracks.get(0); DataTrack track1 = dataTracks.get(1); CombinedDataSource source = new CombinedDataSource(track0, track1, op); DataSourceTrack newTrack = new DataSourceTrack(null, track0.getId() + track1.getId() + text, text, source); changeRenderer(Arrays.<Track>asList(newTrack), track0.getRenderer().getClass()); newTrack.setDataRange(track0.getDataRange()); newTrack.setColorScale(track0.getColorScale()); IGV.getInstance().addTracks(Arrays.<Track>asList(newTrack), PanelName.DATA_PANEL); } /** * Return popup menu with items applicable to feature tracks * * @return */ private static void addFeatureItems(JPopupMenu featurePopupMenu, final Collection<Track> tracks, TrackClickEvent te) { addDisplayModeItems(tracks, featurePopupMenu); if (tracks.size() == 1) { Track t = tracks.iterator().next(); Feature f = t.getFeatureAtMousePosition(te); ReferenceFrame frame = te.getFrame(); if (frame == null && !FrameManager.isGeneListMode()) { frame = FrameManager.getDefaultFrame(); } String featureName = ""; if (f != null) { featurePopupMenu.addSeparator(); featurePopupMenu.add(getCopyDetailsItem(f, te)); // If we are over an exon, copy its sequence instead of the entire feature. Feature sequenceFeature = f; if (sequenceFeature instanceof IGVFeature) { featureName = ((IGVFeature) sequenceFeature).getName(); double position = te.getChromosomePosition(); Collection<Exon> exons = ((IGVFeature) sequenceFeature).getExons(); if (exons != null) { for (Exon exon : exons) { if (position > exon.getStart() && position < exon.getEnd()) { sequenceFeature = exon; break; } } } } featurePopupMenu.add(getCopySequenceItem(sequenceFeature)); if (frame != null) { Range r = frame.getCurrentRange(); featurePopupMenu.add(getExtendViewItem(featureName, sequenceFeature, r)); } featurePopupMenu.add(getBlatItem(sequenceFeature)); } if (Globals.isDevelopment()) { featurePopupMenu.addSeparator(); featurePopupMenu.add(getFeatureToGeneListItem(t)); } if (Globals.isDevelopment() && FrameManager.isGeneListMode() && tracks.size() == 1) { featurePopupMenu.addSeparator(); featurePopupMenu.add(getShowSortFramesItem(tracks.iterator().next())); } } featurePopupMenu.addSeparator(); featurePopupMenu.add(getChangeFeatureWindow(tracks)); } /** * Return popup menu with items applicable to arc tracks * * @return */ // stevenbusan public static void addBasePairItems(JPopupMenu menu, final Collection<Track> tracks) { JLabel arcDirectionHeading = new JLabel(LEADING_HEADING_SPACER + "Arc direction", JLabel.LEFT); arcDirectionHeading.setFont(UIConstants.boldFont); menu.add(arcDirectionHeading); final String[] arcDirectionLabels = {"Up", "Down"}; for (int i = 0; i < arcDirectionLabels.length; i++) { JCheckBoxMenuItem item = new JCheckBoxMenuItem(arcDirectionLabels[i]); final int n = (i == 0) ? 1 : -1; for (Track track : tracks) { if (track instanceof BasePairTrack) { if (((BasePairTrack) track).getDirection() == n) { item.setSelected(true); } } } item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { for (Track track : tracks) { if (track instanceof BasePairTrack) { ((BasePairTrack) track).setDirection(n); } } IGV.getInstance().repaint(); } }); menu.add(item); } menu.addSeparator(); } private static JMenuItem getFeatureToGeneListItem(final Track t) { JMenuItem mi = new JMenuItem("Use as loci list"); mi.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // Current chromosome only for now } }); return mi; } /** * Return a menu item which will export visible features * If {@code tracks} is not a single {@code FeatureTrack}, {@code null} * is returned (there should be no menu entry) * * @param tracks * @return */ public static JMenuItem getExportFeatures(final Collection<Track> tracks, final ReferenceFrame frame) { Track ft = tracks.iterator().next(); if (tracks.size() != 1) { return null; } JMenuItem exportData = null; if (ft instanceof FeatureTrack) { exportData = new JMenuItem("Export Features..."); exportData.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { File outFile = FileDialogUtils.chooseFile("Save Visible Data", PreferencesManager.getPreferences().getLastTrackDirectory(), new File("visibleData.bed"), FileDialogUtils.SAVE); exportVisibleFeatures(outFile.getAbsolutePath(), tracks, frame); } }); } else if (ft instanceof AlignmentTrack) { exportData = new JMenuItem("Export Alignments..."); exportData.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { File outFile = FileDialogUtils.chooseFile("Save Visible Data", PreferencesManager.getPreferences().getLastTrackDirectory(), new File("visibleData.sam"), FileDialogUtils.SAVE); int countExp = exportVisibleAlignments(outFile.getAbsolutePath(), tracks, frame); String msg = String.format("%d reads written", countExp); MessageUtils.setStatusBarMessage(msg); } }); } return exportData; } static int exportVisibleAlignments(String outPath, Collection<Track> tracks, ReferenceFrame frame) { AlignmentTrack alignmentTrack = null; for (Track track : tracks) { if (track instanceof AlignmentTrack) { alignmentTrack = (AlignmentTrack) track; break; } } if (alignmentTrack == null) return -1; File outFile = new File(outPath); try { AlignmentDataManager dataManager = alignmentTrack.getDataManager(); ResourceLocator inlocator = dataManager.getLocator(); Range range = frame.getCurrentRange(); //Read directly from file //return SAMWriter.writeAlignmentFilePicard(inlocator, outPath, range.getChr(), range.getStart(), range.getEnd()); //Export those in memory, overlapping current view return SAMWriter.writeAlignmentFilePicard(dataManager, outFile, frame, range.getChr(), range.getStart(), range.getEnd()); } catch (IOException e) { log.error(e.getMessage(), e); throw new RuntimeException(e); } } /** * Write features in {@code track} found in {@code range} to {@code outPath}, * BED format * TODO Move somewhere else? run on separate thread? Probably shouldn't be here * * @param outPath * @param tracks * @param frame */ static void exportVisibleFeatures(String outPath, Collection<Track> tracks, ReferenceFrame frame) { PrintWriter writer; try { writer = new PrintWriter(outPath); } catch (FileNotFoundException e) { throw new RuntimeException(e); } for (Track track : tracks) { if (track instanceof FeatureTrack) { FeatureTrack fTrack = (FeatureTrack) track; String trackLine = fTrack.getExportTrackLine(); if (trackLine != null) { writer.println(trackLine); } //Can't trust FeatureTrack.getFeatures to limit itself, so we filter List<Feature> features = fTrack.getVisibleFeatures(frame); Range range = frame.getCurrentRange(); Predicate<Feature> pred = FeatureUtils.getOverlapPredicate(range.getChr(), range.getStart(), range.getEnd()); features = CollUtils.filter(features, pred); IGVBEDCodec codec = new IGVBEDCodec(); for (Feature feat : features) { String featString = codec.encode(feat); writer.println(featString); } } } writer.flush(); writer.close(); } /** * Popup menu with items applicable to both feature and data tracks * * @return */ public static void addSharedItems(JPopupMenu menu, final Collection<Track> tracks, boolean hasFeatureTracks, boolean hasCoverageTracks) { //JLabel trackSettingsHeading = new JLabel(LEADING_HEADING_SPACER + "Track Settings", JLabel.LEFT); //trackSettingsHeading.setFont(boldFont); //menu.add(trackSettingsHeading); menu.add(getTrackRenameItem(tracks)); String colorLabel = (hasFeatureTracks || hasCoverageTracks) ? "Change Track Color..." : "Change Track Color (Positive Values)..."; JMenuItem item = new JMenuItem(colorLabel); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { changeTrackColor(tracks); } }); menu.add(item); if (!(hasFeatureTracks || hasCoverageTracks)) { // Change track color by attribute item = new JMenuItem("Change Track Color (Negative Values)..."); item.setToolTipText( "Change the alternate track color. This color is used when graphing negative values"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { changeAltTrackColor(tracks); } }); menu.add(item); } menu.add(getChangeTrackHeightItem(tracks)); menu.add(getChangeFontSizeItem(tracks)); } private static void changeStatType(String statType, Collection<Track> selectedTracks) { for (Track track : selectedTracks) { track.setWindowFunction(WindowFunction.valueOf(statType)); } refresh(); } public static JMenuItem getTrackRenameItem(final Collection<Track> selectedTracks) { // Change track height by attribute JMenuItem item = new JMenuItem("Rename Track..."); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { UIUtilities.invokeOnEventThread(new Runnable() { public void run() { renameTrack(selectedTracks); } }); } }); if (selectedTracks.size() > 1) { item.setEnabled(false); } return item; } private static JMenuItem getHeatmapScaleItem(final Collection<Track> selectedTracks) { JMenuItem item = new JMenuItem("Set Heatmap Scale..."); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { if (selectedTracks.size() > 0) { ContinuousColorScale colorScale = selectedTracks.iterator().next().getColorScale(); HeatmapScaleDialog dlg = new HeatmapScaleDialog(IGV.getMainFrame(), colorScale); dlg.setVisible(true); if (!dlg.isCanceled()) { colorScale = dlg.getColorScale(); // dlg.isFlipAxis()); for (Track track : selectedTracks) { track.setColorScale(colorScale); } IGV.getInstance().repaint(); } } } }); return item; } public static JMenuItem getDataRangeItem(final Collection<Track> selectedTracks) { JMenuItem item = new JMenuItem("Set Data Range..."); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { if (selectedTracks.size() > 0) { // Create a datarange that spans the extent of prev tracks range DataRange prevAxisDefinition = DataRange.getFromTracks(selectedTracks); DataRangeDialog dlg = new DataRangeDialog(IGV.getMainFrame(), prevAxisDefinition); dlg.setVisible(true); if (!dlg.isCanceled()) { float min = Math.min(dlg.getMax(), dlg.getMin()); float max = Math.max(dlg.getMin(), dlg.getMax()); float mid = dlg.getBase(); mid = Math.max(min, Math.min(mid, max)); DataRange axisDefinition = new DataRange(dlg.getMin(), mid, dlg.getMax(), prevAxisDefinition.isDrawBaseline(), dlg.isLog()); for (Track track : selectedTracks) { track.setDataRange(axisDefinition); track.setAutoScale(false); track.removeAttribute(AttributeManager.GROUP_AUTOSCALE); } IGV.getInstance().repaint(); } } } }); return item; } private static JMenuItem getDrawBorderItem() { // Change track height by attribute final JCheckBoxMenuItem drawBorderItem = new JCheckBoxMenuItem("Draw borders"); drawBorderItem.setSelected(FeatureTrack.isDrawBorder()); drawBorderItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { FeatureTrack.setDrawBorder(drawBorderItem.isSelected()); IGV.getInstance().revalidateTrackPanels(); } }); return drawBorderItem; } public static JMenuItem getLogScaleItem(final Collection<Track> selectedTracks) { // Change track height by attribute final JCheckBoxMenuItem logScaleItem = new JCheckBoxMenuItem("Log scale"); final boolean logScale = selectedTracks.iterator().next().getDataRange().isLog(); logScaleItem.setSelected(logScale); logScaleItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { DataRange.Type scaleType = logScaleItem.isSelected() ? DataRange.Type.LOG : DataRange.Type.LINEAR; for (Track t : selectedTracks) { t.getDataRange().setType(scaleType); } IGV.getInstance().revalidateTrackPanels(); } }); return logScaleItem; } private static JMenuItem getAutoscaleItem(final Collection<Track> selectedTracks) { final JCheckBoxMenuItem autoscaleItem = new JCheckBoxMenuItem("Autoscale"); if (selectedTracks.size() == 0) { autoscaleItem.setEnabled(false); } else { boolean autoScale = checkAutoscale(selectedTracks); autoscaleItem.setSelected(autoScale); autoscaleItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { boolean autoScale = autoscaleItem.isSelected(); for (Track t : selectedTracks) { t.setAutoScale(autoScale); if (autoScale) { t.removeAttribute(AttributeManager.GROUP_AUTOSCALE); } } IGV.getInstance().repaint(); } }); } return autoscaleItem; } private static JMenuItem getGroupAutoscaleItem(final Collection<Track> selectedTracks) { final JMenuItem autoscaleItem = new JMenuItem("Group Autoscale"); autoscaleItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { int nextAutoscaleGroup = IGV.getInstance().getSession().getNextAutoscaleGroup(); for (Track t : selectedTracks) { t.setAttributeValue(AttributeManager.GROUP_AUTOSCALE, "Group " + nextAutoscaleGroup); t.setAutoScale(false); } PreferencesManager.getPreferences().setShowAttributeView(true); IGV.getInstance().getMainPanel().invalidate(); IGV.getInstance().doRefresh(); } }); return autoscaleItem; } private static boolean checkAutoscale(Collection<Track> selectedTracks) { boolean autoScale = false; for (Track t : selectedTracks) { if (t.getAutoScale()) { autoScale = true; break; } } return autoScale; } public static JMenuItem getShowDataRangeItem(final Collection<Track> selectedTracks) { final JCheckBoxMenuItem item = new JCheckBoxMenuItem("Show Data Range"); if (selectedTracks.size() == 0) { item.setEnabled(false); } else { boolean showDataRange = true; for (Track t : selectedTracks) { if (!t.isShowDataRange()) { showDataRange = false; break; } } item.setSelected(showDataRange); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { boolean showDataRange = item.isSelected(); for (Track t : selectedTracks) { if (t instanceof DataTrack) { ((DataTrack) t).setShowDataRange(showDataRange); } } IGV.getInstance().revalidateTrackPanels(); } }); } return item; } public static void addDisplayModeItems(final Collection<Track> tracks, JPopupMenu menu) { // Find "most representative" state from track collection Map<Track.DisplayMode, Integer> counts = new HashMap<Track.DisplayMode, Integer>(Track.DisplayMode.values().length); Track.DisplayMode currentMode = null; for (Track t : tracks) { Track.DisplayMode mode = t.getDisplayMode(); if (counts.containsKey(mode)) { counts.put(mode, counts.get(mode) + 1); } else { counts.put(mode, 1); } } int maxCount = -1; for (Map.Entry<Track.DisplayMode, Integer> count : counts.entrySet()) { if (count.getValue() > maxCount) { currentMode = count.getKey(); maxCount = count.getValue(); } } ButtonGroup group = new ButtonGroup(); Map<String, Track.DisplayMode> modes = new LinkedHashMap<String, Track.DisplayMode>(4); modes.put("Collapsed", Track.DisplayMode.COLLAPSED); modes.put("Expanded", Track.DisplayMode.EXPANDED); modes.put("Squished", Track.DisplayMode.SQUISHED); for (final Map.Entry<String, Track.DisplayMode> entry : modes.entrySet()) { JRadioButtonMenuItem mm = new JRadioButtonMenuItem(entry.getKey()); mm.setSelected(currentMode == entry.getValue()); mm.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { setTrackDisplayMode(tracks, entry.getValue()); refresh(); } }); group.add(mm); menu.add(mm); } } private static void setTrackDisplayMode(Collection<Track> tracks, Track.DisplayMode mode) { for (Track t : tracks) { t.setDisplayMode(mode); } } public static JMenuItem getRemoveMenuItem(final Collection<Track> selectedTracks) { boolean multiple = selectedTracks.size() > 1; JMenuItem item = new JMenuItem("Remove Track" + (multiple ? "s" : "")); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { removeTracksAction(selectedTracks); } }); return item; } /** * Display a dialog to the user asking to confirm if they want to remove the * selected tracks * * @param selectedTracks */ public static void removeTracksAction(final Collection<Track> selectedTracks) { if (selectedTracks.isEmpty()) { return; } StringBuffer buffer = new StringBuffer(); for (Track track : selectedTracks) { buffer.append("\n\t"); buffer.append(track.getName()); } String deleteItems = buffer.toString(); JTextArea textArea = new JTextArea(); textArea.setEditable(false); JScrollPane scrollPane = new JScrollPane(textArea); textArea.setText(deleteItems); JOptionPane optionPane = new JOptionPane(scrollPane, JOptionPane.PLAIN_MESSAGE, JOptionPane.YES_NO_OPTION); optionPane.setPreferredSize(new Dimension(550, 500)); JDialog dialog = optionPane.createDialog(IGV.getMainFrame(), "Remove The Following Tracks"); dialog.setVisible(true); Object choice = optionPane.getValue(); if ((choice == null) || (JOptionPane.YES_OPTION != ((Integer) choice).intValue())) { return; } IGV.getInstance().removeTracks(selectedTracks); IGV.getInstance().doRefresh(); } public static void changeRenderer(final Collection<Track> selectedTracks, Class rendererClass) { for (Track track : selectedTracks) { // TODO -- a temporary hack to facilitate RNAi development if (track.getTrackType() == TrackType.RNAI) { if (rendererClass == BarChartRenderer.class) { rendererClass = RNAiBarChartRenderer.class; } } track.setRendererClass(rendererClass); } refresh(); } public static void renameTrack(final Collection<Track> selectedTracks) { if (selectedTracks.isEmpty()) { return; } Track t = selectedTracks.iterator().next(); String newName = JOptionPane.showInputDialog(IGV.getMainFrame(), "Enter new name: ", t.getName()); if (newName == null || newName.trim() == "") { return; } t.setName(newName); refresh(); } public static void changeTrackHeight(final Collection<Track> selectedTracks) { if (selectedTracks.isEmpty()) { return; } final String parameter = "Track height"; Integer value = getIntegerInput(parameter, getRepresentativeTrackHeight(selectedTracks)); if (value == null) { return; } value = Math.max(0, value); for (Track track : selectedTracks) { track.setHeight(value, true); } refresh(); } public static void changeFeatureVisibilityWindow(final Collection<Track> selectedTracks) { Collection<Track> featureTracks = new ArrayList(selectedTracks.size()); for (Track t : selectedTracks) { if (t instanceof FeatureTrack) { featureTracks.add(t); } } if (featureTracks.isEmpty()) { return; } int origValue = featureTracks.iterator().next().getVisibilityWindow(); double origValueKB = (origValue / 1000.0); Double value = getDoubleInput("Enter visibility window in kilo-bases. To load all data enter zero.", origValueKB); if (value == null) { return; } for (Track track : featureTracks) { track.setVisibilityWindow((int) (value * 1000)); } refresh(); } public static void changeFontSize(final Collection<Track> selectedTracks) { if (selectedTracks.isEmpty()) { return; } final String parameter = "Font size"; int defaultValue = selectedTracks.iterator().next().getFontSize(); Integer value = getIntegerInput(parameter, defaultValue); if (value == null) { return; } for (Track track : selectedTracks) { track.setFontSize(value); } refresh(); } public static Integer getIntegerInput(String parameter, int value) { while (true) { String strValue = JOptionPane.showInputDialog( IGV.getMainFrame(), parameter + ": ", String.valueOf(value)); //strValue will be null if dialog cancelled if ((strValue == null) || strValue.trim().equals("")) { return null; } try { value = Integer.parseInt(strValue); return value; } catch (NumberFormatException numberFormatException) { JOptionPane.showMessageDialog(IGV.getMainFrame(), parameter + " must be an integer number."); } } } public static Double getDoubleInput(String parameter, double value) { while (true) { String strValue = JOptionPane.showInputDialog( IGV.getMainFrame(), parameter + ": ", String.valueOf(value)); //strValue will be null if dialog cancelled if ((strValue == null) || strValue.trim().equals("")) { return null; } try { value = Double.parseDouble(strValue); return value; } catch (NumberFormatException numberFormatException) { MessageUtils.showMessage(parameter + " must be a number."); } } } public static void changeTrackColor(final Collection<Track> selectedTracks) { if (selectedTracks.isEmpty()) { return; } Color currentSelection = selectedTracks.iterator().next().getColor(); Color color = UIUtilities.showColorChooserDialog( "Select Track Color (Positive Values)", currentSelection); if (color == null) { return; } for (Track track : selectedTracks) { //We preserve the alpha value. This is motivated by MergedTracks track.setColor(ColorUtilities.modifyAlpha(color, currentSelection.getAlpha())); } refresh(); } public static void changeAltTrackColor(final Collection<Track> selectedTracks) { if (selectedTracks.isEmpty()) { return; } Color currentSelection = selectedTracks.iterator().next().getColor(); Color color = UIUtilities.showColorChooserDialog( "Select Track Color (Negative Values)", currentSelection); if (color == null) { return; } for (Track track : selectedTracks) { track.setAltColor(ColorUtilities.modifyAlpha(color, currentSelection.getAlpha())); } refresh(); } public static void exportTrackNames(final Collection<Track> selectedTracks) { if (selectedTracks.isEmpty()) { return; } File file = FileDialogUtils.chooseFile("Export track names", PreferencesManager.getPreferences().getLastTrackDirectory(), new File("trackNames.tab"), FileDialogUtils.SAVE); if (file == null) { return; } PrintWriter pw = null; try { pw = new PrintWriter(new BufferedWriter(new FileWriter(file))); List<String> attributes = AttributeManager.getInstance().getVisibleAttributes(); pw.print("Name"); for (String att : attributes) { pw.print("\t" + att); } pw.println(); for (Track track : selectedTracks) { //We preserve the alpha value. This is motivated by MergedTracks pw.print(track.getName()); for (String att : attributes) { String val = track.getAttributeValue(att); pw.print("\t" + (val == null ? "" : val)); } pw.println(); } } catch (IOException e) { MessageUtils.showErrorMessage("Error writing to file", e); log.error(e); } finally { if (pw != null) pw.close(); } } public static JMenuItem getCopyDetailsItem(final Feature f, final TrackClickEvent evt) { JMenuItem item = new JMenuItem("Copy Details to Clipboard"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { ReferenceFrame frame = evt.getFrame(); int mouseX = evt.getMouseEvent().getX(); double location = frame.getChromosomePosition(mouseX); if (f instanceof IGVFeature) { String details = f.getChr() + ":" + (f.getStart() + 1) + "-" + f.getEnd() + System.getProperty("line.separator") + System.getProperty("line.separator"); String valueString = ((IGVFeature) f).getValueString(location, mouseX, null); if (details != null) { details += valueString; details = details.replace("<br>", System.getProperty("line.separator")); details = details.replace("<br/>", System.getProperty("line.separator")); details = details.replace("<b>", ""); details = details.replace("</b>", ""); details = details.replace(" ", " "); details = details.replace("<hr>", System.getProperty("line.separator") + "--------------------------" + System.getProperty("line.separator")); StringUtils.copyTextToClipboard(details); } } } }); return item; } public static JMenuItem getCopySequenceItem(final Feature f) { final Strand strand; if(f instanceof IGVFeature) { strand = ((IGVFeature) f).getStrand(); } else { strand = Strand.NONE; } JMenuItem item = new JMenuItem("Copy Sequence"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { Genome genome = GenomeManager.getInstance().getCurrentGenome(); IGV.copySequenceToClipboard(genome, f.getChr(), f.getStart(), f.getEnd(), strand); } }); return item; } public static JMenuItem getExtendViewItem(final String featureName, final Feature f, final Range r) { JMenuItem item = new JMenuItem("ExtView"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { ExtendViewClient.postExtendView(featureName, f.getStart(), f.getEnd(), r.getChr(), r.getStart(), r.getEnd()); } }); return item; } public static JMenuItem getBlatItem(final Feature f) { JMenuItem item = new JMenuItem("Blat Sequence"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { final Strand strand; if(f instanceof IGVFeature) { strand = ((IGVFeature) f).getStrand(); } else { strand = Strand.NONE; } BlatClient.doBlatQuery(f.getChr(), f.getStart(), f.getEnd(), strand); } }); return item; } /** * Return a representative track height to use as the default. For now * using the median track height. * * @return */ public static int getRepresentativeTrackHeight(Collection<Track> tracks) { double[] heights = new double[tracks.size()]; int i = 0; for (Track track : tracks) { heights[i] = track.getHeight(); i++; } int medianTrackHeight = (int) Math.round(StatUtils.percentile(heights, 50)); if (medianTrackHeight > 0) { return medianTrackHeight; } return PreferencesManager.getPreferences().getAsInt(Constants.INITIAL_TRACK_HEIGHT); } public static void refresh() { if (IGV.hasInstance()) { IGV.getInstance().showLoadedTrackCount(); IGV.getInstance().doRefresh(); } } public static JMenuItem getChangeTrackHeightItem(final Collection<Track> selectedTracks) { // Change track height by attribute JMenuItem item = new JMenuItem("Change Track Height..."); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { changeTrackHeight(selectedTracks); } }); return item; } public static JMenuItem getChangeKMPlotItem(final Collection<Track> selectedTracks) { // Change track height by attribute JMenuItem item = new JMenuItem("Kaplan-Meier Plot..."); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { // If one or fewer tracks are selected assume the intent is to use all tracks. A right-click // will always result in one selected track. Collection<Track> tracks = selectedTracks.size() > 1 ? selectedTracks : IGV.getInstance().getAllTracks(); KMPlotFrame frame = new KMPlotFrame(tracks); frame.setVisible(true); } }); // The Kaplan-Meier plot requires sample information, specifically survival, sample, and censure. We // can't know if these columns exist, but we can at least know if sample-info has been loaded. // 3-4 columns always exist by default, more indicate at least some sample attributes are defined. boolean sampleInfoLoaded = AttributeManager.getInstance().getAttributeNames().size() > 4; item.setEnabled(sampleInfoLoaded); return item; } public static JMenuItem getChangeFeatureWindow(final Collection<Track> selectedTracks) { // Change track height by attribute JMenuItem item = new JMenuItem("Set Feature Visibility Window..."); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { changeFeatureVisibilityWindow(selectedTracks); } }); return item; } public static JMenuItem getChangeFontSizeItem(final Collection<Track> selectedTracks) { // Change track height by attribute JMenuItem item = new JMenuItem("Change Font Size..."); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { changeFontSize(selectedTracks); } }); return item; } // Experimental methods follow public static JMenuItem getShowSortFramesItem(final Track track) { final JCheckBoxMenuItem item = new JCheckBoxMenuItem("Sort frames"); item.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { Runnable runnable = new Runnable() { public void run() { FrameManager.sortFrames(track); IGV.getInstance().resetFrames(); } }; LongRunningTask.submit(runnable); } }); return item; } }