From 48f06fe24b9e105b147855cc832550c9a675728b Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Wed, 24 Jul 2024 15:31:47 -0700 Subject: [PATCH 001/130] base mod popup text bug --- src/main/java/org/broad/igv/sam/SAMAlignment.java | 2 +- .../java/org/broad/igv/sam/mods/BaseModificationCounts.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/broad/igv/sam/SAMAlignment.java b/src/main/java/org/broad/igv/sam/SAMAlignment.java index ca2f1ced10..9aeb138dd6 100644 --- a/src/main/java/org/broad/igv/sam/SAMAlignment.java +++ b/src/main/java/org/broad/igv/sam/SAMAlignment.java @@ -939,7 +939,7 @@ private String getAttributeString(boolean truncate) { } buf.append("
" + tag.tag + " = "); - if (tag.tag.equals("Ml")) { + if (tag.tag.equals("ML") || tag.tag.equals("Ml")) { buf.append(this.getMlTagString(tag)); buf.append("
"); continue; diff --git a/src/main/java/org/broad/igv/sam/mods/BaseModificationCounts.java b/src/main/java/org/broad/igv/sam/mods/BaseModificationCounts.java index 2c04c869a8..232dde5cf4 100644 --- a/src/main/java/org/broad/igv/sam/mods/BaseModificationCounts.java +++ b/src/main/java/org/broad/igv/sam/mods/BaseModificationCounts.java @@ -82,7 +82,7 @@ public void incrementCounts(Alignment alignment) { } } - // Count the modification with highest likelihood, which might be the likelihood of no-modification + // Take the modification with highest likelihood, which might be the likelihood of no-modification if (canonicalBase != 0) { BaseModificationKey noModKey = BaseModificationKey.getKey(canonicalBase, '+', "NONE_" + canonicalBase); allModifications.add(noModKey); From 5e8861152facecdeb4af4ff88b217b91bd2b2f15 Mon Sep 17 00:00:00 2001 From: Jim Robinson <933148+jrobinso@users.noreply.github.com> Date: Wed, 24 Jul 2024 16:27:03 -0700 Subject: [PATCH 002/130] Create gradle.yml --- .github/workflows/gradle.yml | 67 ++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 .github/workflows/gradle.yml diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 0000000000..51e9f38df7 --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,67 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle + +name: Java CI with Gradle + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + # Configure Gradle for optimal use in GitHub Actions, including caching of downloaded dependencies. + # See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md + - name: Setup Gradle + uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0 + + - name: Test with Gradle Wrapper + run: ./gradlew test + + # NOTE: The Gradle Wrapper is the default and recommended way to run Gradle (https://docs.gradle.org/current/userguide/gradle_wrapper.html). + # If your project does not have the Gradle Wrapper configured, you can use the following configuration to run Gradle with a specified version. + # + # - name: Setup Gradle + # uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0 + # with: + # gradle-version: '8.5' + # + # - name: Build with Gradle 8.5 + # run: gradle build + + dependency-submission: + + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + # Generates and submits a dependency graph, enabling Dependabot Alerts for all project dependencies. + # See: https://github.com/gradle/actions/blob/main/dependency-submission/README.md + - name: Generate and submit dependency graph + uses: gradle/actions/dependency-submission@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0 From 1648696f4b1d83637d03e4cf0e76ab1775941246 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Wed, 24 Jul 2024 16:35:02 -0700 Subject: [PATCH 003/130] update badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ac7e3e8466..7352385162 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # igv -![Build Status](https://github.com/igvteam/igv/actions/workflows/gradle_test.yml/badge.svg) +![Build Status](https://github.com/igvteam/igv/actions/workflows/gradle.yml/badge.svg) ![GitHub issues](https://img.shields.io/github/issues/igvteam/igv) ![GitHub closed issues](https://img.shields.io/github/issues-closed/igvteam/igv) ![](https://img.shields.io/npm/l/igv.svg) From e5e62540cfeaf1552abf653c5a4f9d663e2b1f50 Mon Sep 17 00:00:00 2001 From: Jim Robinson <933148+jrobinso@users.noreply.github.com> Date: Wed, 24 Jul 2024 16:36:17 -0700 Subject: [PATCH 004/130] Update gradle.yml --- .github/workflows/gradle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 51e9f38df7..be85ad7178 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -5,7 +5,7 @@ # This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle -name: Java CI with Gradle +name: Test on: push: From d4e05b799ff8019740728896fcc8760e953bd0dd Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Wed, 24 Jul 2024 20:14:02 -0700 Subject: [PATCH 005/130] Add right-click menu option to remove empty track panels --- src/main/java/org/broad/igv/ui/IGV.java | 8 ++ .../org/broad/igv/ui/panel/MainPanel.java | 25 ++++-- .../igv/ui/panel/TrackPanelComponent.java | 85 +++++++++++-------- 3 files changed, 73 insertions(+), 45 deletions(-) diff --git a/src/main/java/org/broad/igv/ui/IGV.java b/src/main/java/org/broad/igv/ui/IGV.java index 9fdd00a58a..be9bd010b7 100644 --- a/src/main/java/org/broad/igv/ui/IGV.java +++ b/src/main/java/org/broad/igv/ui/IGV.java @@ -1093,6 +1093,14 @@ public void removeDataPanel(String name) { contentPane.getMainPanel().removeDataPanel(name); } + public void removeTrackPanel(TrackPanel trackPanel) { + contentPane.getMainPanel().removeTrackPanel(trackPanel); + } + + public boolean panelIsRemovable(TrackPanel trackPanel) { + return contentPane.getMainPanel().panelIsRemovable(trackPanel); + } + public MainPanel getMainPanel() { return contentPane.getMainPanel(); } diff --git a/src/main/java/org/broad/igv/ui/panel/MainPanel.java b/src/main/java/org/broad/igv/ui/panel/MainPanel.java index 0c9c4d3944..5e181ae012 100644 --- a/src/main/java/org/broad/igv/ui/panel/MainPanel.java +++ b/src/main/java/org/broad/igv/ui/panel/MainPanel.java @@ -413,23 +413,30 @@ public void removeEmptyDataPanels() { public void removeDataPanel(String name) { - TrackPanelScrollPane sp = null; for (TrackPanel tp : getTrackPanels()) { if (name.equals(tp.getName())) { - sp = tp.getScrollPane(); - break; + removeTrackPanel(tp); + return; } } + } + + public void removeTrackPanel(TrackPanel trackPanel) { // Don't remove the "special" panes - if (sp == dataTrackScrollPane || sp == featureTrackScrollPane) { - return; - } - if (sp != null) { - centerSplitPane.remove(sp); - TrackNamePanel.removeDropListenerFor(sp.getNamePanel()); + if (panelIsRemovable(trackPanel)) { + TrackPanelScrollPane sp = trackPanel.getScrollPane(); + if (sp != null) { + centerSplitPane.remove(sp); + TrackNamePanel.removeDropListenerFor(sp.getNamePanel()); + centerSplitPane.revalidate(); + } } } + public boolean panelIsRemovable(TrackPanel trackPanel) { + return trackPanel.getScrollPane() != dataTrackScrollPane && trackPanel.getScrollPane() != featureTrackScrollPane; + } + public void updatePanelDimensions() { Insets insets = applicationHeaderPanel.getInsets(); namePanelX = insets.left; diff --git a/src/main/java/org/broad/igv/ui/panel/TrackPanelComponent.java b/src/main/java/org/broad/igv/ui/panel/TrackPanelComponent.java index c92496b7f5..2e0717c2cc 100644 --- a/src/main/java/org/broad/igv/ui/panel/TrackPanelComponent.java +++ b/src/main/java/org/broad/igv/ui/panel/TrackPanelComponent.java @@ -36,11 +36,13 @@ import org.broad.igv.track.Track; import org.broad.igv.track.TrackClickEvent; import org.broad.igv.track.TrackMenuUtils; +import org.broad.igv.ui.FontManager; import org.broad.igv.ui.IGV; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.util.ArrayList; @@ -176,57 +178,68 @@ protected void openPopupMenu(TrackClickEvent te) { protected void openPopupMenu(TrackClickEvent te, List extraItems) { MouseEvent e = te.getMouseEvent(); + IGVPopupMenu menu = null; final Collection selectedTracks = getSelectedTracks(); if (selectedTracks.isEmpty()) { - return; - } + // If this panel is empty (no tracks), and is removable, present option to remove it + if (getAllTracks().size() == 0 && IGV.getInstance().panelIsRemovable(getTrackPanel())) { + menu = new IGVPopupMenu(); + JMenuItem item = new JMenuItem("Remove panel"); + item.addActionListener(e12 -> { + IGV.getInstance().removeTrackPanel(getTrackPanel()); + }); + menu.add(item); + } + } else { - IGVPopupMenu menu = null; - // If a single track is selected, give it an opportunity to provide the popup menu - if (selectedTracks.size() == 1) { - Track track = selectedTracks.iterator().next(); - menu = track.getPopupMenu(te); - } + // If a single track is selected, give it an opportunity to provide the popup menu + if (selectedTracks.size() == 1) { + Track track = selectedTracks.iterator().next(); + menu = track.getPopupMenu(te); + } - // If still no menu, create a generic one with common items - if (menu == null) { - String title = getPopupMenuTitle(e.getX(), e.getY()); - menu = TrackMenuUtils.getPopupMenu(selectedTracks, title, te); - } + // If still no menu, create a generic one with common items + if (menu == null) { + String title = getPopupMenuTitle(e.getX(), e.getY()); + menu = TrackMenuUtils.getPopupMenu(selectedTracks, title, te); + } - // Add additional items, if any - if (extraItems != null) { - menu.addSeparator(); - for (Component item : extraItems) { - menu.add(item); + // Add additional items, if any + if (extraItems != null) { + menu.addSeparator(); + for (Component item : extraItems) { + menu.add(item); + } } - } - if (menu.includeStandardItems()) { + if (menu.includeStandardItems()) { - TrackMenuUtils.addPluginItems(menu, selectedTracks, te); + TrackMenuUtils.addPluginItems(menu, selectedTracks, te); - // Add saveImage items - menu.addSeparator(); - JMenuItem savePng = new JMenuItem("Save PNG image..."); - savePng.addActionListener(e1 -> saveImage("png")); - menu.add(savePng); - JMenuItem saveSvg = new JMenuItem("Save SVG image..."); - saveSvg.addActionListener(e1 -> saveImage("svg")); - menu.add(saveSvg); + // Add saveImage items + menu.addSeparator(); + JMenuItem savePng = new JMenuItem("Save PNG image..."); + savePng.addActionListener(e1 -> saveImage("png")); + menu.add(savePng); + JMenuItem saveSvg = new JMenuItem("Save SVG image..."); + saveSvg.addActionListener(e1 -> saveImage("svg")); + menu.add(saveSvg); - // Add export features - ReferenceFrame frame = FrameManager.getDefaultFrame(); - JMenuItem exportFeats = TrackMenuUtils.getExportFeatures(selectedTracks, frame); - if (exportFeats != null) menu.add(exportFeats); + // Add export features + ReferenceFrame frame = FrameManager.getDefaultFrame(); + JMenuItem exportFeats = TrackMenuUtils.getExportFeatures(selectedTracks, frame); + if (exportFeats != null) menu.add(exportFeats); - menu.addSeparator(); - menu.add(TrackMenuUtils.getRemoveMenuItem(selectedTracks)); + menu.addSeparator(); + menu.add(TrackMenuUtils.getRemoveMenuItem(selectedTracks)); + } } - menu.show(e.getComponent(), e.getX(), e.getY()); + if(menu != null) { + menu.show(e.getComponent(), e.getX(), e.getY()); + } } From 9c78e802fcd988140c0cc589983af1f6423af350 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Wed, 24 Jul 2024 22:10:37 -0700 Subject: [PATCH 006/130] Restore cluster menu option for 3rd gen alignments, renamed to "Cluster alignments". Fixes #1509 --- .../java/org/broad/igv/sam/Alignment.java | 6 ++--- .../org/broad/igv/sam/AlignmentPacker.java | 4 ++-- .../org/broad/igv/sam/AlignmentTrack.java | 8 +++++-- .../org/broad/igv/sam/AlignmentTrackMenu.java | 24 +++++++++---------- ...{HaplotypeUtils.java => ClusterUtils.java} | 11 ++++----- .../java/org/broad/igv/sam/SAMAlignment.java | 12 +++++----- .../java/org/broad/igv/sam/SortOption.java | 5 +--- 7 files changed, 34 insertions(+), 36 deletions(-) rename src/main/java/org/broad/igv/sam/{HaplotypeUtils.java => ClusterUtils.java} (97%) diff --git a/src/main/java/org/broad/igv/sam/Alignment.java b/src/main/java/org/broad/igv/sam/Alignment.java index 9442bbe5df..835973795c 100644 --- a/src/main/java/org/broad/igv/sam/Alignment.java +++ b/src/main/java/org/broad/igv/sam/Alignment.java @@ -175,13 +175,13 @@ default ClippingCounts getClippingCounts(){ return ClippingCounts.fromCigar(getCigar()); } - default void setHaplotypeName(String hap) {} + default void setClusterName(String hap) {} - default String getHaplotypeName() {return null;} + default String getClusterName() {return null;} default void setHapDistance(int dist) {}; - default int getHapDistance() {return 0;} + default int getClusterDistance() {return 0;} default Map getBaseModificationMap() { return null;} diff --git a/src/main/java/org/broad/igv/sam/AlignmentPacker.java b/src/main/java/org/broad/igv/sam/AlignmentPacker.java index 8b96b8fc46..6c269e786f 100644 --- a/src/main/java/org/broad/igv/sam/AlignmentPacker.java +++ b/src/main/java/org/broad/igv/sam/AlignmentPacker.java @@ -439,8 +439,8 @@ private Object getGroupValue(Alignment al, AlignmentTrack.RenderOptions renderOp String readNameParts[], movieName, zmw; switch (groupBy) { - case HAPLOTYPE: - return al.getHaplotypeName(); + case CLUSTER: + return al.getClusterName(); case STRAND: return al.isNegativeStrand() ? "-" : "+"; case SAMPLE: diff --git a/src/main/java/org/broad/igv/sam/AlignmentTrack.java b/src/main/java/org/broad/igv/sam/AlignmentTrack.java index 1a8f47735c..e79af83c6d 100644 --- a/src/main/java/org/broad/igv/sam/AlignmentTrack.java +++ b/src/main/java/org/broad/igv/sam/AlignmentTrack.java @@ -164,7 +164,7 @@ public enum GroupOption { INSERTION_AT_POS("insertion at position", true), MOVIE("movie"), ZMW("ZMW"), - HAPLOTYPE("haplotype"), + CLUSTER("cluster"), READ_ORDER("read order"), LINKED("linked"), PHASE("phase"), @@ -1711,7 +1711,11 @@ public void unmarshalXML(Element element, Integer version) { sortOption = SortOption.valueOf((element.getAttribute("sortOption"))); } if (element.hasAttribute("groupByOption")) { - groupByOption = GroupOption.valueOf(element.getAttribute("groupByOption")); + String value = element.getAttribute("groupByOption"); + if(value.equals("HAPLOTYPE")) { + value = "CLUSTER"; // Backward compatibility + } + groupByOption = GroupOption.valueOf(value); } if (element.hasAttribute("shadeAlignmentsByOption")) { shadeAlignmentsOption = ShadeAlignmentsOption.valueOf(element.getAttribute("shadeAlignmentsByOption")); diff --git a/src/main/java/org/broad/igv/sam/AlignmentTrackMenu.java b/src/main/java/org/broad/igv/sam/AlignmentTrackMenu.java index efd1916e90..2bfa447000 100644 --- a/src/main/java/org/broad/igv/sam/AlignmentTrackMenu.java +++ b/src/main/java/org/broad/igv/sam/AlignmentTrackMenu.java @@ -2,7 +2,6 @@ import htsjdk.samtools.SAMTag; import org.broad.igv.Globals; -import org.broad.igv.feature.Locus; import org.broad.igv.feature.Range; import org.broad.igv.feature.Strand; import org.broad.igv.jbrowse.CircularViewUtilities; @@ -95,10 +94,6 @@ class AlignmentTrackMenu extends IGVPopupMenu { // Experiment type (RNA, THIRD GEN, OTHER) addSeparator(); addExperimentTypeMenuItem(); -// if (alignmentTrack.getExperimentType() == AlignmentTrack.ExperimentType.THIRD_GEN) { -// addHaplotype(e); -// } - // Group, sort, color, shade, and pack addSeparator(); @@ -186,6 +181,12 @@ class AlignmentTrackMenu extends IGVPopupMenu { add(sashimi); } + // Experimental items + if (alignmentTrack.getExperimentType() == AlignmentTrack.ExperimentType.THIRD_GEN) { + addSeparator(); + addClusterItem(e); + } + // Show alignments, coverage, splice junctions addSeparator(); addShowItems(); @@ -233,9 +234,9 @@ private void addShowDiagram(final TrackClickEvent e, final Alignment clickedAlig } - private void addHaplotype(TrackClickEvent e) { + private void addClusterItem(TrackClickEvent e) { - JMenuItem item = new JMenuItem("Cluster (phase) alignments"); + JMenuItem item = new JMenuItem("Cluster alignments *EXPERIMENTAL*"); final ReferenceFrame frame; if (e.getFrame() == null && FrameManager.getFrames().size() == 1) { @@ -269,17 +270,14 @@ private void addHaplotype(TrackClickEvent e) { final int end = (int) frame.getEnd(); AlignmentInterval interval = dataManager.getLoadedInterval(frame); - HaplotypeUtils haplotypeUtils = new HaplotypeUtils(interval); - boolean success = haplotypeUtils.clusterAlignments(frame.getChrName(), start, end, nClusters); + ClusterUtils clusterUtils = new ClusterUtils(interval); + boolean success = clusterUtils.clusterAlignments(frame.getChrName(), start, end, nClusters); if (success) { - groupAlignments(AlignmentTrack.GroupOption.HAPLOTYPE, null, null); + groupAlignments(AlignmentTrack.GroupOption.CLUSTER, null, null); alignmentTrack.repaint(); } - //dataManager.sortRows(SortOption.HAPLOTYPE, frame, (end + start) / 2, null); - //AlignmentTrack.repaint(); - }); diff --git a/src/main/java/org/broad/igv/sam/HaplotypeUtils.java b/src/main/java/org/broad/igv/sam/ClusterUtils.java similarity index 97% rename from src/main/java/org/broad/igv/sam/HaplotypeUtils.java rename to src/main/java/org/broad/igv/sam/ClusterUtils.java index 51802c1631..798c9cada8 100644 --- a/src/main/java/org/broad/igv/sam/HaplotypeUtils.java +++ b/src/main/java/org/broad/igv/sam/ClusterUtils.java @@ -3,7 +3,6 @@ import org.broad.igv.feature.genome.GenomeManager; import org.broad.igv.logging.*; -import org.broad.igv.feature.genome.Genome; import org.broad.igv.ui.util.MessageUtils; import java.util.*; @@ -15,14 +14,14 @@ * @author Jim Robinson */ -public class HaplotypeUtils { +public class ClusterUtils { - private static Logger log = LogManager.getLogger(HaplotypeUtils.class); + private static Logger log = LogManager.getLogger(ClusterUtils.class); private final AlignmentInterval alignmentInterval; - public HaplotypeUtils(AlignmentInterval alignmentInterval) { + public ClusterUtils(AlignmentInterval alignmentInterval) { this.alignmentInterval = alignmentInterval; } @@ -54,7 +53,7 @@ public boolean clusterAlignments(String chr, int start, int end, int nClasses) { // Clear any existing names for(Alignment a : this.alignmentInterval.getAlignments()) { - a.setHaplotypeName("NONE"); + a.setClusterName("?"); } // Label alignments @@ -126,7 +125,7 @@ public boolean clusterAlignments(String chr, int start, int end, int nClasses) { List alignments = labelAlignmentMap.get(l); for (Alignment a : alignments) { - a.setHaplotypeName(label); + a.setClusterName(label); } } } diff --git a/src/main/java/org/broad/igv/sam/SAMAlignment.java b/src/main/java/org/broad/igv/sam/SAMAlignment.java index 9aeb138dd6..889da5d393 100644 --- a/src/main/java/org/broad/igv/sam/SAMAlignment.java +++ b/src/main/java/org/broad/igv/sam/SAMAlignment.java @@ -685,9 +685,9 @@ public String getAlignmentValueString(double position, int mouseX, AlignmentTrac int basePosition = (int) position; StringBuffer buf = new StringBuffer(); - if (getHaplotypeName() != null) { - buf.append("Hap name: " + getHaplotypeName() + "
"); - buf.append("Dist: " + getHapDistance() + "
"); + if (getClusterName() != null) { + buf.append("Cluster name: " + getClusterName() + "
"); + buf.append("Dist: " + getClusterDistance() + "
"); } boolean atInsertion = false; @@ -1215,12 +1215,12 @@ private static boolean operatorIsMatch(boolean showSoftClipped, char operator) { String haplotypeName; @Override - public void setHaplotypeName(String hap) { + public void setClusterName(String hap) { haplotypeName = hap; } @Override - public String getHaplotypeName() { + public String getClusterName() { return haplotypeName; } @@ -1232,7 +1232,7 @@ public void setHapDistance(int dist) { } @Override - public int getHapDistance() { + public int getClusterDistance() { return hapDistance; } diff --git a/src/main/java/org/broad/igv/sam/SortOption.java b/src/main/java/org/broad/igv/sam/SortOption.java index 93e26787c9..c4393fe128 100644 --- a/src/main/java/org/broad/igv/sam/SortOption.java +++ b/src/main/java/org/broad/igv/sam/SortOption.java @@ -1,13 +1,10 @@ package org.broad.igv.sam; -import htsjdk.samtools.SAMTag; import htsjdk.samtools.util.Locatable; import org.broad.igv.feature.genome.ChromosomeNameComparator; import java.util.Comparator; -import java.util.List; import java.util.Objects; -import java.util.Set; import java.util.function.Function; import java.util.function.ToIntFunction; @@ -134,7 +131,7 @@ Comparator getAlignmentComparator(final int center, final String tag, }, HAPLOTYPE { @Override Comparator getAlignmentComparator(final int center, final String tag, final byte referenceBase) { - return Comparator.comparingInt(Alignment::getHapDistance); + return Comparator.comparingInt(Alignment::getClusterDistance); } }, READ_ORDER { @Override From e976e70d7f63aacb2505425c717bb9b8ee075957 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Thu, 25 Jul 2024 11:41:01 -0700 Subject: [PATCH 007/130] cleanup --- .../java/org/broad/igv/sam/ClusterUtils.java | 30 +++++++------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/broad/igv/sam/ClusterUtils.java b/src/main/java/org/broad/igv/sam/ClusterUtils.java index 798c9cada8..6113c866b3 100644 --- a/src/main/java/org/broad/igv/sam/ClusterUtils.java +++ b/src/main/java/org/broad/igv/sam/ClusterUtils.java @@ -27,7 +27,6 @@ public ClusterUtils(AlignmentInterval alignmentInterval) { public boolean clusterAlignments(String chr, int start, int end, int nClasses) { - try { AlignmentCounts counts = this.alignmentInterval.getCounts(); @@ -46,14 +45,13 @@ public boolean clusterAlignments(String chr, int start, int end, int nClasses) { MessageUtils.showMessage("Not enough variants, reducing # of clusters: " + nClasses); } - // Adjust start and end to min and max snp positions, there is no information outside these bounds start = snpPos.get(0) - 1; end = snpPos.get(snpPos.size() - 1) + 1; // Clear any existing names for(Alignment a : this.alignmentInterval.getAlignments()) { - a.setClusterName("?"); + a.setClusterName(null); } // Label alignments @@ -65,9 +63,7 @@ public boolean clusterAlignments(String chr, int start, int end, int nClasses) { } // Sort labels (entries) by # of associated alignments - labels.sort((o1, o2) -> { - return labelAlignmentMap.get(o2).size() - labelAlignmentMap.get(o1).size(); - }); + labels.sort((o1, o2) -> labelAlignmentMap.get(o2).size() - labelAlignmentMap.get(o1).size()); // Create initial cluster centroids List clusters = new ArrayList<>(); @@ -78,7 +74,6 @@ public boolean clusterAlignments(String chr, int start, int end, int nClasses) { } // Now assign all labels to a cluster - int n = 0; int max = 50; while (true) { @@ -145,7 +140,7 @@ private List findVariantPositions(int start, int end, AlignmentCounts c byte ref = reference[i - start]; - float mismatchCount = getMismatchCount(counts, i, ref); + float mismatchCount = getMismatchFraction(counts, i, ref); if (mismatchCount > 0.2f) { snpPos.add(i); @@ -170,29 +165,29 @@ public Map> labelAlignments(int start, int end, List= alignment.getStart() && end <= alignment.getEnd()) { - String hapName = ""; + String clusterName = ""; for (Integer pos : positions) { boolean found = false; for (AlignmentBlock block : alignment.getAlignmentBlocks()) { if (block.isSoftClip()) continue; if (block.contains(pos)) { int blockOffset = pos - block.getStart(); - hapName += (char) block.getBase(blockOffset); + clusterName += (char) block.getBase(blockOffset); found = true; break; } } if (!found) { - hapName += "_"; + clusterName += "_"; } } - hapName = hapName.toLowerCase(); + clusterName = clusterName.toLowerCase(); - List alignments = alignmentMap.get(hapName); + List alignments = alignmentMap.get(clusterName); if (alignments == null) { alignments = new ArrayList<>(); - alignmentMap.put(hapName, alignments); + alignmentMap.put(clusterName, alignments); } alignments.add(alignment); @@ -201,13 +196,10 @@ public Map> labelAlignments(int start, int end, List combineClusters(List clusters) { From 4e790053086e6a123cb0098666ee944e2c376f40 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:10:23 -0700 Subject: [PATCH 008/130] Bug fix -- errors saving session on shutdown could hang IGV, necessitating a force quite --- src/main/java/org/broad/igv/session/SessionWriter.java | 1 - .../java/org/broad/igv/ui/action/SaveSessionMenuAction.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/broad/igv/session/SessionWriter.java b/src/main/java/org/broad/igv/session/SessionWriter.java index 1d70758651..b86f7c84a3 100644 --- a/src/main/java/org/broad/igv/session/SessionWriter.java +++ b/src/main/java/org/broad/igv/session/SessionWriter.java @@ -179,7 +179,6 @@ public String createXmlFromSession(Session session, File outputFile) throws Runt } catch (Exception e) { String message = "Error creating session."; log.error(message, e); - JOptionPane.showMessageDialog(IGV.getInstance().getMainFrame(), message); throw new RuntimeException(e); } } diff --git a/src/main/java/org/broad/igv/ui/action/SaveSessionMenuAction.java b/src/main/java/org/broad/igv/ui/action/SaveSessionMenuAction.java index 1d1a732b7d..3ef45edc7d 100644 --- a/src/main/java/org/broad/igv/ui/action/SaveSessionMenuAction.java +++ b/src/main/java/org/broad/igv/ui/action/SaveSessionMenuAction.java @@ -113,7 +113,7 @@ public void actionPerformed(ActionEvent e) { } catch (Exception e2) { JOptionPane.showMessageDialog(igv.getMainFrame(), "There was an error writing to " + sf.getName() + "(" + e2.getMessage() + ")"); - log.error("Failed to save session!", e2); + log.error("Failed to save session
" + e2.getMessage(), e2); } finally { WaitCursorManager.removeWaitCursor(token); igv.resetStatusMessage(); From 0072026cc0b7731106b58310173d7e2b8c0b60e5 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:43:22 -0700 Subject: [PATCH 009/130] Handle startup errors due to corrupt .genome files. --- .../org/broad/igv/feature/genome/GenomeManager.java | 7 +++++++ src/main/java/org/broad/igv/ui/IGV.java | 11 ++++++++--- .../broad/igv/ui/commandbar/GenomeListManager.java | 12 +++++++++++- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/broad/igv/feature/genome/GenomeManager.java b/src/main/java/org/broad/igv/feature/genome/GenomeManager.java index f4150d5f67..3783b7aa74 100644 --- a/src/main/java/org/broad/igv/feature/genome/GenomeManager.java +++ b/src/main/java/org/broad/igv/feature/genome/GenomeManager.java @@ -455,4 +455,11 @@ private static void updateSequenceMapFile() { } } + public void refreshHostedGenome(String genomeId) { + + Map itemMap = GenomeListManager.getInstance().getServerGenomeMap(); + if(itemMap.containsKey(genomeId)) { + downloadGenome(itemMap.get(genomeId), false); + } + } } diff --git a/src/main/java/org/broad/igv/ui/IGV.java b/src/main/java/org/broad/igv/ui/IGV.java index be9bd010b7..63344ddc6b 100644 --- a/src/main/java/org/broad/igv/ui/IGV.java +++ b/src/main/java/org/broad/igv/ui/IGV.java @@ -1940,11 +1940,16 @@ public void run() { GenomeManager.getInstance().loadGenomeById(genomeId); genomeLoaded = true; } catch (Exception e) { - MessageUtils.showErrorMessage("Error loading genome: " + genomeId, e); - log.error("Error loading genome: " + genomeId, e); - } + MessageUtils.showErrorMessage("Error loading genome " + genomeId + "
" + e.getMessage(), e); + genomeLoaded = false; + } if (!genomeLoaded) { + // If the error is with the default genome try refreshing it. + if(genomeId.equals(GenomeListManager.DEFAULT_GENOME.getId())) { + GenomeManager.getInstance().refreshHostedGenome(genomeId); + } + genomeId = GenomeListManager.DEFAULT_GENOME.getId(); try { GenomeManager.getInstance().loadGenomeById(genomeId); diff --git a/src/main/java/org/broad/igv/ui/commandbar/GenomeListManager.java b/src/main/java/org/broad/igv/ui/commandbar/GenomeListManager.java index 36271a73de..5b4e90741f 100644 --- a/src/main/java/org/broad/igv/ui/commandbar/GenomeListManager.java +++ b/src/main/java/org/broad/igv/ui/commandbar/GenomeListManager.java @@ -311,9 +311,19 @@ public void removeAllItems(List removedValuesList) { public void removeGenomeListItem(GenomeListItem genomeListItem) { - final String id = genomeListItem.getId(); genomeItemMap.remove(id); + + // If this is a cached genome remove it from cache + Map cachedItems = getCachedGenomeList(); + if(cachedItems.containsKey(id)){ + try { + (new File(genomeListItem.getPath())).delete(); + } catch (Exception e) { + log.error("Error deleting genome file: " + genomeListItem.getPath() + ": " + e.getMessage()); + } + } + removeUserDefinedGenome(id); } From 735d626d887ffac3555c9b080a720931c9c40836 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:43:50 -0700 Subject: [PATCH 010/130] Map legacy URLs before opening connection input stream --- src/main/java/org/broad/igv/util/HttpUtils.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/broad/igv/util/HttpUtils.java b/src/main/java/org/broad/igv/util/HttpUtils.java index c97804e18d..086d035ce0 100644 --- a/src/main/java/org/broad/igv/util/HttpUtils.java +++ b/src/main/java/org/broad/igv/util/HttpUtils.java @@ -345,6 +345,9 @@ public String doPost(URL url, Map params) throws IOException { * @throws IOException */ public InputStream openConnectionStream(URL url) throws IOException { + + url = new URL(mapURL(url.toExternalForm())); + log.debug("Opening connection stream to " + url); if (url.getProtocol().toLowerCase().equals("ftp")) { String userInfo = url.getUserInfo(); From e9ccb3bff170fd2cfbe7b22d0516e2dc8cf6e6b3 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:46:02 -0700 Subject: [PATCH 011/130] update default genome to hg38 --- .../java/org/broad/igv/ui/commandbar/GenomeListManager.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/broad/igv/ui/commandbar/GenomeListManager.java b/src/main/java/org/broad/igv/ui/commandbar/GenomeListManager.java index 5b4e90741f..89501936a5 100644 --- a/src/main/java/org/broad/igv/ui/commandbar/GenomeListManager.java +++ b/src/main/java/org/broad/igv/ui/commandbar/GenomeListManager.java @@ -39,9 +39,9 @@ public class GenomeListManager { private static final String ACT_USER_DEFINED_GENOME_LIST_FILE = "user-defined-genomes.txt"; public static final GenomeListItem DEFAULT_GENOME = new GenomeListItem( - "Human (hg19)", - "https://s3.amazonaws.com/igv.org.genomes/hg19/hg19.json", - "hg19"); + "Human (hg38)", + "https://igv-genepattern-org.s3.amazonaws.com/genomes/hg38/hg38.json", + "hg38"); private Map genomeItemMap; From 0f0b4fb16e3f95a7579f2216804ec8240baaeac1 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Thu, 25 Jul 2024 19:55:57 -0700 Subject: [PATCH 012/130] Check RNA stats before 3rd gen to detect long-read rna seq. Fixes #1491. Fix bug interpreting read sequence = "*". Was interpreting as mismatch to reference. --- src/main/java/org/broad/igv/sam/AlignmentBlockImpl.java | 2 +- src/main/java/org/broad/igv/sam/ReadStats.java | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/broad/igv/sam/AlignmentBlockImpl.java b/src/main/java/org/broad/igv/sam/AlignmentBlockImpl.java index 865a183cae..cfb5654f5e 100644 --- a/src/main/java/org/broad/igv/sam/AlignmentBlockImpl.java +++ b/src/main/java/org/broad/igv/sam/AlignmentBlockImpl.java @@ -52,7 +52,7 @@ public AlignmentBlockImpl(int start, byte[] bases, byte[] qualities, int offset, this.start = start; this.offset = offset; - this.bases = new ByteSubarray(bases, offset, nBases, (byte) '?'); + this.bases = bases.length == 0 ? EMPTY_ARRAY :new ByteSubarray(bases, offset, nBases, (byte) '?'); this.basesLength = nBases; // qualities are optional in a SAMRecord, we might get null or an array of zero diff --git a/src/main/java/org/broad/igv/sam/ReadStats.java b/src/main/java/org/broad/igv/sam/ReadStats.java index 14bd051722..0a18b1595c 100644 --- a/src/main/java/org/broad/igv/sam/ReadStats.java +++ b/src/main/java/org/broad/igv/sam/ReadStats.java @@ -60,7 +60,7 @@ public class ReadStats { public void addAlignment(Alignment alignment) { - if(readCount > MAX_READ_COUNT) return; + if (readCount > MAX_READ_COUNT) return; if (alignment.isMapped()) { @@ -135,10 +135,10 @@ private double[] downsample(DoubleArrayList list, int size) { public AlignmentTrack.ExperimentType inferType() { compute(); if (readCount < 20) return AlignmentTrack.ExperimentType.UNKOWN; // Not enough reads - if ((readLengthStdDev > 100 || medianReadLength > 1000) && averageCigarLength > 10) { // Cigar length to filter consensus reads - return AlignmentTrack.ExperimentType.THIRD_GEN; - } else if (medianRefToReadRatio > 5 || fracReadsWithNs > 0.01) { + if (medianRefToReadRatio > 5 || fracReadsWithNs > 0.01) { return AlignmentTrack.ExperimentType.RNA; + } else if ((readLengthStdDev > 100 || medianReadLength > 1000) && averageCigarLength > 10) { // Cigar length to filter consensus reads + return AlignmentTrack.ExperimentType.THIRD_GEN; } else { return AlignmentTrack.ExperimentType.OTHER; } From a4cd2d51dd828a871af025ab77697295cef8f0e7 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Thu, 25 Jul 2024 22:35:42 -0700 Subject: [PATCH 013/130] Expanded insertion issue - collapsing seemed to shift view, due to attempt o center the collapsed insertion. This is visually disturbing. --- .../org/broad/igv/sam/AlignmentRenderer.java | 98 ------------------- .../java/org/broad/igv/sam/BaseRenderer.java | 11 ++- .../org/broad/igv/sam/InsertionManager.java | 8 +- .../broad/igv/ui/panel/ReferenceFrame.java | 4 - .../org/broad/igv/ui/panel/RulerPanel.java | 7 -- 5 files changed, 7 insertions(+), 121 deletions(-) diff --git a/src/main/java/org/broad/igv/sam/AlignmentRenderer.java b/src/main/java/org/broad/igv/sam/AlignmentRenderer.java index 3e709e8601..b93184bd8b 100644 --- a/src/main/java/org/broad/igv/sam/AlignmentRenderer.java +++ b/src/main/java/org/broad/igv/sam/AlignmentRenderer.java @@ -1083,104 +1083,6 @@ private void drawLargeIndelLabel(Graphics2D g, boolean isInsertion, String label } } - public void renderExpandedInsertion(InsertionMarker i, - List alignments, - RenderContext context, - Rectangle rect, - boolean leaveMargin) { - double origin = context.getOrigin(); - double locScale = context.getScale(); - if ((alignments != null) && (alignments.size() > 0)) { - - Graphics2D g = context.getGraphics2D("INSERTIONS"); - double dX = 1 / context.getScale(); - int fontSize = (int) Math.min(dX, 12); - if (fontSize >= 8) { - Font f = FontManager.getFont(Font.BOLD, fontSize); - g.setFont(f); - } - - for (Alignment alignment : alignments) { - if (alignment.getEnd() < i.position) continue; - if (alignment.getStart() > i.position) break; - AlignmentBlock insertion = alignment.getInsertionAt(i.position); - if (insertion != null) { - - // Compute the start and dend of the alignment in pixels - double pixelStart = (insertion.getStart() - origin) / locScale; - double pixelEnd = (insertion.getEnd() - origin) / locScale; - int x = (int) pixelStart; - - // If any any part of the feature fits in the track rectangle draw it - if (pixelEnd < rect.x || pixelStart > rect.getMaxX()) { - continue; - } - - int bpWidth = insertion.getBasesLength(); - double pxWidthExact = ((double) bpWidth) / locScale; - int h = (int) Math.max(1, rect.getHeight() - 2); - int y = (int) (rect.getY() + (rect.getHeight() - h) / 2) - 1; - - if (!insertion.hasBases()) { - g.setColor(purple); - g.fillRect(x, y, (int) pxWidthExact, h); - - } else { - drawExpandedInsertionBases(x, context, rect, insertion, leaveMargin); - } - } - } - } - } - - - private void drawExpandedInsertionBases(int pixelPosition, - RenderContext context, - Rectangle rect, - AlignmentBlock block, - boolean leaveMargin) { - Graphics2D g = context.getGraphics2D("INSERTIONS"); - ByteSubarray bases = block.getBases(); - int padding = block.getPadding(); - - double locScale = context.getScale(); - double origin = context.getOrigin(); - - // Compute bounds - int pY = (int) rect.getY(); - int dY = (int) rect.getHeight(); - int dX = (int) Math.max(1, (1.0 / locScale)); - - final int size = bases.length + padding; - for (int p = 0; p < size; p++) { - - char c = p < padding ? '-' : (char) bases.getByte(p - padding); - - Color color = SequenceRenderer.nucleotideColors.get(c); - if (color == null) { - color = Color.black; - } - - // If there is room for text draw the character, otherwise - // just draw a rectangle to represent the - int pX = (int) (pixelPosition + (p / locScale)); - - // Don't draw out of clipping rect - if (pX > rect.getMaxX()) { - break; - } else if (pX + dX < rect.getX()) { - continue; - } - BaseRenderer.drawBase(g, color, c, pX, pY, dX, dY - (leaveMargin ? 2 : 0), false, null); - } - - int leftX = pixelPosition + context.translateX; - int rightX = leftX + rect.width; - block.setPixelRange(leftX, rightX); - - } - - private Color getAlignmentColor(Alignment alignment, AlignmentTrack track) { // Set color used to draw the feature. Highlight features that intersect the diff --git a/src/main/java/org/broad/igv/sam/BaseRenderer.java b/src/main/java/org/broad/igv/sam/BaseRenderer.java index 8e8d29aad6..a2b0244311 100644 --- a/src/main/java/org/broad/igv/sam/BaseRenderer.java +++ b/src/main/java/org/broad/igv/sam/BaseRenderer.java @@ -88,7 +88,7 @@ public static void drawExpandedInsertions(InsertionMarker insertionMarker, if (alignment.getStart() > insertionMarker.position) break; AlignmentBlock insertion = alignment.getInsertionAt(insertionMarker.position); - if (insertion != null && insertion.hasBases()) { + if (insertion != null && insertion.getBasesLength() > 0) { double origin = context.getOrigin(); double locScale = context.getScale(); @@ -104,8 +104,7 @@ public static void drawExpandedInsertions(InsertionMarker insertionMarker, ByteSubarray bases = insertion.getBases(); int padding = insertion.getPadding(); - - final int size = bases.length + padding; + final int size = insertion.getBasesLength() + padding; for (int p = 0; p < size; p++) { double pX = (pixelStart + (p / locScale)); @@ -114,8 +113,10 @@ public static void drawExpandedInsertions(InsertionMarker insertionMarker, if (pX > rect.getMaxX()) break; else if (pX + dX < rect.getX()) continue; - char c = p < padding ? '-' : (char) bases.getByte(p - padding); - + // idx can be out of range due to (1) padding, (2) no read sequence (sequence == *) + int idx = p - padding; + char c = idx >= 0 && idx < bases.length ? (char) bases.getByte(p - padding) : + padding > 0 ? '-' : '*'; Color color = null; // TODO -- support bisulfite mode? Probably not possible as that depends on the reference diff --git a/src/main/java/org/broad/igv/sam/InsertionManager.java b/src/main/java/org/broad/igv/sam/InsertionManager.java index be96920350..098f9efdc8 100644 --- a/src/main/java/org/broad/igv/sam/InsertionManager.java +++ b/src/main/java/org/broad/igv/sam/InsertionManager.java @@ -69,7 +69,6 @@ public void clear() { public List getInsertions(String chrName, double start, double end) { - Map insertionMap = insertionMaps.get(chrName); if(insertionMap == null ) return null; @@ -81,11 +80,6 @@ public void setSelected(InsertionMarker insertionMarker) { this.selectedInsertion = insertionMarker; } - public InsertionMarker getSelectedInsertion(String chrName) { - return this.selectedInsertion; - } - - public synchronized void processAlignments(String chr, List alignments) { Genome genome = GenomeManager.getInstance().getCurrentGenome(); @@ -107,7 +101,7 @@ public synchronized void processAlignments(String chr, List alignment AlignmentBlock[] blocks = a.getInsertions(); if (blocks != null) { for (AlignmentBlock block : blocks) { - if (block.getBases() == null || block.getBases().length < minLength) continue; + if (block.getBasesLength() < minLength) continue; Integer key = block.getStart(); InsertionMarker insertionMarker = insertionMap.get(key); if (insertionMarker == null) { diff --git a/src/main/java/org/broad/igv/ui/panel/ReferenceFrame.java b/src/main/java/org/broad/igv/ui/panel/ReferenceFrame.java index 3941c86608..8e315fdd9f 100644 --- a/src/main/java/org/broad/igv/ui/panel/ReferenceFrame.java +++ b/src/main/java/org/broad/igv/ui/panel/ReferenceFrame.java @@ -762,11 +762,7 @@ private static int getChromosomeLength(String chrName) { } public void setExpandedInsertion(InsertionMarker im) { - InsertionMarker previousInsertion = this.expandedInsertion; this.expandedInsertion = im; - if (im == null && previousInsertion != null) { - this.centerOnLocation(previousInsertion.position); - } } public InsertionMarker getExpandedInsertion() { diff --git a/src/main/java/org/broad/igv/ui/panel/RulerPanel.java b/src/main/java/org/broad/igv/ui/panel/RulerPanel.java index 5dcd826922..19a2a42e81 100644 --- a/src/main/java/org/broad/igv/ui/panel/RulerPanel.java +++ b/src/main/java/org/broad/igv/ui/panel/RulerPanel.java @@ -501,18 +501,11 @@ public void doMouseClick(MouseEvent evt) { setCursor(Cursor.getDefaultCursor()); WaitCursorManager.CursorToken token = WaitCursorManager.showWaitCursor(); try { - boolean clickHandled = false; for (final ClickLink link : clickLinks) { if (link.region.contains(e.getPoint())) { link.action.accept(link.value); - clickHandled = true; } } - if (!clickHandled && !isWholeGenomeView()) { - double newLocation = frame.getChromosomePosition(e); - frame.centerOnLocation(newLocation); - } - } finally { WaitCursorManager.removeWaitCursor(token); } From cf9dc3415a08c0a0433f5de6d48d5e05a756b7bd Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Fri, 26 Jul 2024 09:06:27 -0700 Subject: [PATCH 014/130] Change "hide small indels" default for 3rd gen to false --- src/main/resources/org/broad/igv/prefs/preferences.tab | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/org/broad/igv/prefs/preferences.tab b/src/main/resources/org/broad/igv/prefs/preferences.tab index 08d8e0f07b..a7d2bb5692 100644 --- a/src/main/resources/org/broad/igv/prefs/preferences.tab +++ b/src/main/resources/org/broad/igv/prefs/preferences.tab @@ -206,7 +206,7 @@ SAM.MAX_LEVELS Number of reads per window integer 100 SAM.FLAG_LARGE_INDELS Label indels > label threshold boolean TRUE SAM.LARGE_INSERTIONS_THRESOLD Label threshold (bases) integer 1 --- -SAM.HIDE_SMALL_INDEL Hide indels < show indel threshold boolean TRUE +SAM.HIDE_SMALL_INDEL Hide indels < show indel threshold boolean FALSE SAM.SMALL_INDEL_BP_THRESHOLD Show indel threshold (bases) integer 2 ##Clipping From 0e6b0e8ac136d95f2cb6dd35271784df97ab8247 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Fri, 26 Jul 2024 09:07:18 -0700 Subject: [PATCH 015/130] Don't popup information if size is < hidel small indel threshold. Fixes #1480 --- src/main/java/org/broad/igv/sam/SAMAlignment.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/org/broad/igv/sam/SAMAlignment.java b/src/main/java/org/broad/igv/sam/SAMAlignment.java index 889da5d393..a5c8b59722 100644 --- a/src/main/java/org/broad/igv/sam/SAMAlignment.java +++ b/src/main/java/org/broad/igv/sam/SAMAlignment.java @@ -55,6 +55,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static org.broad.igv.prefs.Constants.SAM_HIDE_SMALL_INDEL; + /** * @author jrobinso */ @@ -690,12 +692,18 @@ public String getAlignmentValueString(double position, int mouseX, AlignmentTrac buf.append("Dist: " + getClusterDistance() + "
"); } + boolean hideSmallIndels = renderOptions.isHideSmallIndels(); + int smallIndelThreshold = renderOptions.getSmallIndelThreshold(); + boolean atInsertion = false; boolean atBaseMod = false; // First check insertions. Position is zero based, block coords 1 based if (this.insertions != null) { for (AlignmentBlock block : this.insertions) { if (block.containsPixel(mouseX)) { + if(hideSmallIndels && block.getBasesLength() < smallIndelThreshold) { + continue; + } ByteSubarray bases = block.getBases(); if (bases == null) { buf.append("Insertion: " + block.getLength() + " bases
"); From b1510d0ce3f3aecf4e26dd571e3a2080ea252a43 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Fri, 26 Jul 2024 10:49:43 -0700 Subject: [PATCH 016/130] Proxy server for testing. --- test/server/proxyServer.js | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/test/server/proxyServer.js b/test/server/proxyServer.js index 2126c93b1b..60c88590d8 100644 --- a/test/server/proxyServer.js +++ b/test/server/proxyServer.js @@ -1,3 +1,5 @@ +// Simple proxy server for testing IGV + // Proxy server from https://github.com/kasattejaswi/nodejs-proxy-server // Copyright (c) 2021 Tejaswi Kasat @@ -31,8 +33,8 @@ server.on("connection", (clientToProxySocket) => { // Require a password //if(dataString.indexOf("Proxy-Authorization") < 0) { - // clientToProxySocket.write("HTTP/1.1 407 Proxy requires authentication\r\n\r\n") - // return; + // clientToProxySocket.write("HTTP/1.1 407 Proxy requires authentication\r\n\r\n") + // return; //} // Creating a connection from proxy to destination server @@ -69,12 +71,4 @@ server.on("close", () => { console.log("Client disconnected") }) -server.listen( - { - host: "0.0.0.0", - port: 9999, - }, - () => { - console.log("Server listening on 0.0.0.0:9999") - } -) \ No newline at end of file +server.listen(9999) From bf7f46f5a981e262d662ced5cdbaa919cb212232 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Tue, 30 Jul 2024 08:07:08 -0700 Subject: [PATCH 017/130] Cleanup git ignore --- .gitignore | 27 +++------------------------ CONTRIBUTING.md | 2 +- 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index fc6433d38e..9daad7f241 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,4 @@ -# Created by .gitignore support plugin (hsz.mobi) -.idea/workspace.xml -*.iml -test/data/out/ -/build -/build_java11 -/bin -*.log -/.gradle -.project -.classpath +/.github/ +/.gradle/ +/build/ /.idea/ -/.settings -/proguard_log -.vscode -out/ -.DS_Store -*.bedgraph -*.bam -*.csi.gz -*.sai -/test/batch/snapshots/ -/src/main/java/org/broad/igv/sam/mods/BMScale.java -/src/main/java/org/broad/igv/sam/mods/BMScale.form -/ignore/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2b367d7d26..0145b5a1f1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ First off, thanks for taking the time to contribute! -The following is a set of guidelines for contributing to igv.js, which is hosted in the [igvteam repositories](https://github.com/igvteam) on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. +The following is a set of guidelines for contributing to IGV, which is hosted in the [igvteam repositories](https://github.com/igvteam/igv) on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. ## Code of Conduct From fa872343022360c67d34e09cb4172781e89a3d91 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Fri, 2 Aug 2024 21:04:16 -0700 Subject: [PATCH 018/130] Restore center-on-click to ruler panel. Fixes #1538 --- src/main/java/org/broad/igv/ui/panel/RulerPanel.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/org/broad/igv/ui/panel/RulerPanel.java b/src/main/java/org/broad/igv/ui/panel/RulerPanel.java index 19a2a42e81..5dcd826922 100644 --- a/src/main/java/org/broad/igv/ui/panel/RulerPanel.java +++ b/src/main/java/org/broad/igv/ui/panel/RulerPanel.java @@ -501,11 +501,18 @@ public void doMouseClick(MouseEvent evt) { setCursor(Cursor.getDefaultCursor()); WaitCursorManager.CursorToken token = WaitCursorManager.showWaitCursor(); try { + boolean clickHandled = false; for (final ClickLink link : clickLinks) { if (link.region.contains(e.getPoint())) { link.action.accept(link.value); + clickHandled = true; } } + if (!clickHandled && !isWholeGenomeView()) { + double newLocation = frame.getChromosomePosition(e); + frame.centerOnLocation(newLocation); + } + } finally { WaitCursorManager.removeWaitCursor(token); } From 7663069d2e2239257982813b701c08ca58724edb Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Mon, 12 Aug 2024 12:53:30 -0700 Subject: [PATCH 019/130] Clip splice junction arcs at junction boundaries. Fix for Bezier curve artifact that sometimes carries curve behond junction boundaries. Fixes #1535 --- .../igv/feature/SpliceJunctionFeature.java | 15 +- .../igv/renderer/SpliceJunctionRenderer.java | 584 +++++++++--------- 2 files changed, 284 insertions(+), 315 deletions(-) diff --git a/src/main/java/org/broad/igv/feature/SpliceJunctionFeature.java b/src/main/java/org/broad/igv/feature/SpliceJunctionFeature.java index 7d98a95238..a6051eadcb 100644 --- a/src/main/java/org/broad/igv/feature/SpliceJunctionFeature.java +++ b/src/main/java/org/broad/igv/feature/SpliceJunctionFeature.java @@ -26,6 +26,7 @@ package org.broad.igv.feature; +import htsjdk.tribble.Feature; import org.broad.igv.track.WindowFunction; import java.util.ArrayList; @@ -45,8 +46,8 @@ public class SpliceJunctionFeature extends BasicFeature { protected int junctionDepth = 0; //start and end locations of the junction - protected int junctionStart = 0; - protected int junctionEnd = 0; + private int junctionStart = 0; + private int junctionEnd = 0; int[] startFlankingRegionDepthArray, endFlankingRegionDepthArray; @@ -69,11 +70,11 @@ public SpliceJunctionFeature(String chr, int start, int end, Strand strand) { * @param otherFeature * @return */ - public boolean isSameJunction(SpliceJunctionFeature otherFeature) { - if (otherFeature.getJunctionStart() == getJunctionStart() && - otherFeature.getJunctionEnd() == getJunctionEnd()) - return true; - return false; + public boolean isSameJunction(Feature otherFeature) { + return (otherFeature != null && + otherFeature instanceof SpliceJunctionFeature && + ((SpliceJunctionFeature) otherFeature).getJunctionStart() == getJunctionStart() && + ((SpliceJunctionFeature) otherFeature).getJunctionEnd() == getJunctionEnd()); } /** diff --git a/src/main/java/org/broad/igv/renderer/SpliceJunctionRenderer.java b/src/main/java/org/broad/igv/renderer/SpliceJunctionRenderer.java index 7a1bbebf28..dec058e58e 100644 --- a/src/main/java/org/broad/igv/renderer/SpliceJunctionRenderer.java +++ b/src/main/java/org/broad/igv/renderer/SpliceJunctionRenderer.java @@ -1,34 +1,35 @@ /* - * The MIT License (MIT) - * - * Copyright (c) 2007-2015 Fred Hutchinson Cancer Research Center and Broad Institute - * Portions of code, copyright (c) 2013 by F. Hoffmann-La Roche Ltd and Tessella Inc. - * - * 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.renderer; + * The MIT License (MIT) + * + * Copyright (c) 2007-2015 Fred Hutchinson Cancer Research Center and Broad Institute + * Portions of code, copyright (c) 2013 by F. Hoffmann-La Roche Ltd and Tessella Inc. + * + * 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.renderer; //~--- non-JDK imports -------------------------------------------------------- + import htsjdk.tribble.Feature; import org.broad.igv.logging.*; import org.broad.igv.feature.IGVFeature; import org.broad.igv.feature.SpliceJunctionFeature; @@ -39,289 +40,256 @@ import org.broad.igv.track.FeatureTrack; import org.broad.igv.track.RenderContext; import org.broad.igv.track.Track; - import org.broad.igv.ui.FontManager; import java.awt.*; import java.awt.geom.GeneralPath; - import java.util.ArrayList; - import java.util.Collections; import java.util.List; -/** - * Renderer for splice junctions. Draws a filled-in arc for each junction, with the width of the - * arc representing the depth of coverage. If coverage information is present for the flanking - * regions, draws that, too; otherwise indicates flanking regions with rectangles - * - * @author dhmay - */ -public class SpliceJunctionRenderer extends IGVFeatureRenderer { - - private static Logger log = LogManager.getLogger(SpliceJunctionRenderer.class); - - //color for drawing all arcs - private static Color ARC_COLOR_NEG = new Color(50, 50, 150, 140); //transparent dull blue - private static Color ARC_COLOR_POS = new Color(150, 50, 50, 140); //transparent dull red - private static Color ARC_COLOR_HIGHLIGHT_NEG = new Color(90, 90, 255, 255); //opaque, brighter blue - private Color ARC_COLOR_HIGHLIGHT_POS = new Color(255, 90, 90, 255); //opaque, brighter red - private static Color COLOR_CENTERLINE = new Color(0, 0, 0, 100); - - private int maxDepth = 50; - - /** - * Note: assumption is that featureList is sorted by pStart position. - * - * @param featureList - * @param context - * @param trackRectangle - * @param track - */ - @Override - public void render(List featureList, - RenderContext context, - Rectangle trackRectangle, - Track track) { - - double origin = context.getOrigin(); - double locScale = context.getScale(); - - // TODO -- use enum instead of string "Color" - if ((featureList != null) && !featureList.isEmpty()) { - - // Create a graphics object to draw font names. Graphics are not cached - // by font, only by color, so its neccessary to create a new one to prevent - // affecting other tracks. - Font font = FontManager.getFont(track.getFontSize()); - Graphics2D fontGraphics = (Graphics2D) context.getGraphic2DForColor(Color.BLACK).create(); - fontGraphics.setFont(font); - - //determine whether to show flanking regions - IGVPreferences prefs = PreferencesManager.getPreferences(); - boolean shouldShowFlankingRegions = prefs.getAsBoolean(Constants.SAM_SHOW_JUNCTION_FLANKINGREGIONS); - - // Track coordinates - double trackRectangleX = trackRectangle.getX(); - double trackRectangleMaxX = trackRectangle.getMaxX(); - - - SpliceJunctionFeature selectedFeature = - (SpliceJunctionFeature) ((FeatureTrack) track).getSelectedFeature(); - - - for (IGVFeature feature : featureList) { - SpliceJunctionFeature junctionFeature = (SpliceJunctionFeature) feature; - //if same junction as selected feature, highlight - boolean shouldHighlight = false; - if (selectedFeature != null && selectedFeature.isSameJunction(junctionFeature)) { - setHighlightFeature(junctionFeature); - shouldHighlight = true; - } - - // Get the pStart and pEnd of the entire feature. at extreme zoom levels the - // virtual pixel value can be too large for an int, so the computation is - // done in double precision and cast to an int only when its confirmed its - // within the field of view. - int flankingStart = junctionFeature.getStart(); - int flankingEnd = junctionFeature.getEnd(); - - int junctionStart = junctionFeature.getJunctionStart(); - int junctionEnd = junctionFeature.getJunctionEnd(); - - double virtualPixelStart = Math.round((flankingStart - origin) / locScale); - double virtualPixelEnd = Math.round((flankingEnd - origin) / locScale); - - double virtualPixelJunctionStart = Math.round((junctionStart - origin) / locScale); - double virtualPixelJunctionEnd = Math.round((junctionEnd - origin) / locScale); - - // If the any part of the feature fits in the - // Track rectangle draw it - if ((virtualPixelEnd >= trackRectangleX) && (virtualPixelStart <= trackRectangleMaxX)) { - - // - int displayPixelEnd = (int) Math.min(trackRectangleMaxX, virtualPixelEnd); - int displayPixelStart = (int) Math.max(trackRectangleX, virtualPixelStart); - - float depth = junctionFeature.getJunctionDepth(); - Color color = feature.getColor(); - - drawFeature((int) virtualPixelStart, (int) virtualPixelEnd, - (int) virtualPixelJunctionStart, (int) virtualPixelJunctionEnd, depth, - trackRectangle, context, feature.getStrand(), junctionFeature, shouldHighlight, color, - shouldShowFlankingRegions); - } - } - - //draw a central horizontal line - Graphics2D g2D = context.getGraphic2DForColor(COLOR_CENTERLINE); - g2D.drawLine((int) trackRectangleX, (int) trackRectangle.getCenterY(), - (int) trackRectangleMaxX, (int) trackRectangle.getCenterY()); - - } - } - - - /** - * Draw depth of coverage for the starting or ending flanking region - * - * @param g2D - * @param pixelStart - * @param pixelLength - * @param regionDepthArray - * @param maxPossibleArcHeight - * @param trackRectangle - * @param isPositiveStrand - */ - protected void drawFlankingRegion(Graphics g2D, int pixelStart, int pixelLength, int[] regionDepthArray, - int maxPossibleArcHeight, Rectangle trackRectangle, boolean isPositiveStrand) { - for (int i = 0; i < pixelLength; i++) { - float arrayIndicesPerPixel = (float) regionDepthArray.length / - (float) pixelLength; - int flankingRegionArrayPixelMinIndex = (int) (i * arrayIndicesPerPixel); - int flankingRegionArrayPixelMaxIndex = (int) ((i + 1) * arrayIndicesPerPixel); - flankingRegionArrayPixelMinIndex = - Math.max(0, Math.min(flankingRegionArrayPixelMinIndex, regionDepthArray.length - 1)); - flankingRegionArrayPixelMaxIndex = - Math.max(0, Math.min(flankingRegionArrayPixelMaxIndex, regionDepthArray.length - 1)); - - int meanDepthThisPixel = 0; - for (int j = flankingRegionArrayPixelMinIndex; j <= flankingRegionArrayPixelMaxIndex; j++) - meanDepthThisPixel += regionDepthArray[j]; - meanDepthThisPixel /= (flankingRegionArrayPixelMaxIndex - flankingRegionArrayPixelMinIndex + 1); - meanDepthThisPixel = Math.min(maxDepth, meanDepthThisPixel); - int pixelHeight = Math.max(maxPossibleArcHeight * meanDepthThisPixel / maxDepth, 2); - g2D.fillRect(pixelStart + i, - (int) trackRectangle.getCenterY() + (isPositiveStrand ? -pixelHeight : 0), - 1, pixelHeight); - } - } - - /** - * Draw a filled arc representing a single feature. The thickness and height of the arc are proportional to the - * depth of coverage. Some of this gets a bit arcane -- the result of lots of visual tweaking. - * - * @param pixelFeatureStart the starting position of the feature, whether on-screen or not - * @param pixelFeatureEnd the ending position of the feature, whether on-screen or not - * @param pixelJunctionStart the starting position of the junction, whether on-screen or not - * @param pixelJunctionEnd the ending position of the junction, whether on-screen or not - * @param depth coverage depth - * @param trackRectangle - * @param context - * @param strand - * @param junctionFeature - * @param shouldHighlight - * @param featureColor the color specified for this feature. May be null. - */ - protected void drawFeature(int pixelFeatureStart, int pixelFeatureEnd, - int pixelJunctionStart, int pixelJunctionEnd, float depth, - Rectangle trackRectangle, RenderContext context, Strand strand, - SpliceJunctionFeature junctionFeature, boolean shouldHighlight, Color featureColor, - boolean shouldShowFlankingRegions) { - - - boolean isPositiveStrand = true; - // Get the feature's direction, color appropriately - if (strand != null && strand.equals(Strand.NEGATIVE)) - isPositiveStrand = false; - - //If the feature color is specified, use it, except that we set our own alpha depending on whether - //the feature is highlighted. Otherwise default based on strand and highlight. - Color color; - if (featureColor != null) { - int r = featureColor.getRed(); - int g = featureColor.getGreen(); - int b = featureColor.getBlue(); - int alpha = shouldHighlight ? 255 : 140; - color = new Color(r, g, b, alpha); - } else { - if (isPositiveStrand) - color = shouldHighlight ? ARC_COLOR_HIGHLIGHT_POS : ARC_COLOR_POS; - else - color = shouldHighlight ? ARC_COLOR_HIGHLIGHT_NEG : ARC_COLOR_NEG; - } - - Graphics2D g2D = context.getGraphic2DForColor(color); - - //Height of top of an arc of maximum depth - int maxPossibleArcHeight = (trackRectangle.height - 1) / 2; - - if (shouldShowFlankingRegions) { - if (junctionFeature.hasFlankingRegionDepthArrays()) { - //draw a wigglegram of the splice junction flanking region depth of coverage - - int startFlankingRegionPixelLength = pixelJunctionStart - pixelFeatureStart; - int endFlankingRegionPixelLength = pixelFeatureEnd - pixelJunctionEnd; - - drawFlankingRegion(g2D, pixelFeatureStart, startFlankingRegionPixelLength, - junctionFeature.getStartFlankingRegionDepthArray(), maxPossibleArcHeight, - trackRectangle, isPositiveStrand); - drawFlankingRegion(g2D, pixelJunctionEnd + 1, endFlankingRegionPixelLength, - junctionFeature.getEndFlankingRegionDepthArray(), maxPossibleArcHeight, - trackRectangle, isPositiveStrand); - } else { - //Draw rectangles indicating the overlap on each side of the junction - int overlapRectHeight = 3; - int overlapRectTopX = (int) trackRectangle.getCenterY() + (isPositiveStrand ? -2 : 0); - if (pixelFeatureStart < pixelJunctionStart) { - g2D.fillRect(pixelFeatureStart, overlapRectTopX, - pixelJunctionStart - pixelFeatureStart, overlapRectHeight); - } - if (pixelJunctionEnd < pixelFeatureEnd) { - g2D.fillRect(pixelJunctionEnd, overlapRectTopX, - pixelFeatureEnd - pixelJunctionEnd, overlapRectHeight); - } - } - } - - //Create a path describing the arc, using Bezier curves. The Bezier control points for the top and - //bottom arcs are based on the boundary points of the rectangles containing the arcs - - //proportion of the maximum arc height used by a minimum-height arc - double minArcHeightProportion = 0.33; - - int innerArcHeight = (int) (maxPossibleArcHeight * minArcHeightProportion); - float depthProportionOfMax = Math.min(1, depth / maxDepth); - int arcWidth = Math.max(1, (int) ((1 - minArcHeightProportion) * maxPossibleArcHeight * depthProportionOfMax)); - int outerArcHeight = innerArcHeight + arcWidth; - - - //Height of bottom of the arc - int arcBeginY = (int) trackRectangle.getCenterY() + - (isPositiveStrand ? -1 : 1); - int outerArcPeakY = isPositiveStrand ? - arcBeginY - outerArcHeight : - arcBeginY + outerArcHeight; - int innerArcPeakY = isPositiveStrand ? - arcBeginY - innerArcHeight : - arcBeginY + innerArcHeight; - //dhmay: I don't really understand Bezier curves. For some reason I have to put the Bezier control - //points farther up or down than I want the arcs to extend. This multiplier seems about right - int outerBezierY = arcBeginY + (int) (1.3 * (outerArcPeakY - arcBeginY)); - int innerBezierY = arcBeginY + (int) (1.3 * (innerArcPeakY - arcBeginY)); - - //Putting the Bezier control points slightly off to the sides of the arc - int bezierXPad = Math.max(1, (pixelJunctionEnd - pixelJunctionStart) / 30); - - GeneralPath arcPath = new GeneralPath(); - arcPath.moveTo(pixelJunctionStart, arcBeginY); - arcPath.curveTo(pixelJunctionStart - bezierXPad, outerBezierY, //Bezier 1 - pixelJunctionEnd + bezierXPad, outerBezierY, //Bezier 2 - pixelJunctionEnd, arcBeginY); //Arc end - arcPath.curveTo(pixelJunctionEnd + bezierXPad, innerBezierY, //Bezier 1 - pixelJunctionStart - bezierXPad, innerBezierY, //Bezier 2 - pixelJunctionStart, arcBeginY); //Arc end - - //Draw the arc, to ensure outline is drawn completely (fill won't do it, necessarily). This will also - //give the arc a darker outline - g2D.draw(arcPath); - //Fill the arc - g2D.fill(arcPath); - - } - - public int getMaxDepth() { - return maxDepth; - } - - public void setMaxDepth(int maxDepth) { - this.maxDepth = maxDepth; - } -} + /** + * Renderer for splice junctions. Draws a filled-in arc for each junction, with the width of the + * arc representing the depth of coverage. If coverage information is present for the flanking + * regions, draws that, too; otherwise indicates flanking regions with rectangles + * + * @author dhmay + */ + public class SpliceJunctionRenderer extends IGVFeatureRenderer { + + private static Logger log = LogManager.getLogger(SpliceJunctionRenderer.class); + + //color for drawing all arcs + private static Color ARC_COLOR_NEG = new Color(50, 50, 150, 140); //transparent dull blue + private static Color ARC_COLOR_POS = new Color(150, 50, 50, 140); //transparent dull red + private static Color ARC_COLOR_HIGHLIGHT_NEG = new Color(90, 90, 255, 255); //opaque, brighter blue + private Color ARC_COLOR_HIGHLIGHT_POS = new Color(255, 90, 90, 255); //opaque, brighter red + private static Color COLOR_CENTERLINE = new Color(0, 0, 0, 100); + + private int maxDepth = 50; + + /** + * Note: assumption is that featureList is sorted by pStart position. + * + * @param featureList + * @param context + * @param trackRectangle + * @param track + */ + @Override + public void render(List featureList, + RenderContext context, + Rectangle trackRectangle, + Track track) { + + + // TODO -- use enum instead of string "Color" + if ((featureList != null) && !featureList.isEmpty()) { + + Graphics2D g2D = null; + + try { + g2D = (Graphics2D) context.getGraphics().create(); + + double origin = context.getOrigin(); + double locScale = context.getScale(); + double end = origin + locScale * trackRectangle.width; + + Feature selectedFeature =((FeatureTrack) track).getSelectedFeature(); + boolean shouldShowFlankingRegions = PreferencesManager.getPreferences().getAsBoolean(Constants.SAM_SHOW_JUNCTION_FLANKINGREGIONS); + + for (IGVFeature feature : featureList) { + SpliceJunctionFeature junctionFeature = (SpliceJunctionFeature) feature; + + // If any part of the feature fits in the track rectangle draw it + if (junctionFeature.getEnd() > origin && junctionFeature.getStart() < end) { + boolean shouldHighlight = junctionFeature.isSameJunction(selectedFeature); + drawFeature(junctionFeature, shouldShowFlankingRegions, shouldHighlight, origin, locScale, trackRectangle, g2D); + } + } + + //draw a central horizontal line + g2D.setColor(COLOR_CENTERLINE); + g2D.drawLine(trackRectangle.x, (int) trackRectangle.getCenterY(), + trackRectangle.x + trackRectangle.width, (int) trackRectangle.getCenterY()); + } finally { + g2D.dispose(); + } + + } + } + + /** + * Draw a filled arc representing a single feature. The thickness and height of the arc are proportional to the + * depth of coverage. Some of this gets a bit arcane -- the result of lots of visual tweaking. + * + * @param junctionFeature + * @param highlight + * @param trackRectangle + * @param g2D + */ + protected void drawFeature(SpliceJunctionFeature junctionFeature, boolean shouldShowFlankingRegions, + boolean highlight, double origin, double locScale, Rectangle trackRectangle, Graphics2D g2D) { + + int flankingStart = junctionFeature.getStart(); + int flankingEnd = junctionFeature.getEnd(); + + int junctionStart = junctionFeature.getJunctionStart(); + int junctionEnd = junctionFeature.getJunctionEnd(); + + int pixelFeatureStart = (int) Math.round((flankingStart - origin) / locScale); + int pixelFeatureEnd = (int) Math.round((flankingEnd - origin) / locScale); + + int pixelJunctionStart = (int) Math.round((junctionStart - origin) / locScale); + int pixelJunctionEnd = (int) Math.round((junctionEnd - origin) / locScale); + + Strand strand = junctionFeature.getStrand(); + float depth = junctionFeature.getJunctionDepth(); + Color featureColor = junctionFeature.getColor(); + + + boolean isPositiveStrand = true; + // Get the feature's direction, color appropriately + if (strand != null && strand.equals(Strand.NEGATIVE)) + isPositiveStrand = false; + + //If the feature color is specified, use it, except that we set our own alpha depending on whether + //the feature is highlighted. Otherwise default based on strand and highlight. + Color color; + if (featureColor != null) { + int r = featureColor.getRed(); + int g = featureColor.getGreen(); + int b = featureColor.getBlue(); + int alpha = highlight ? 255 : 140; + color = new Color(r, g, b, alpha); + } else { + if (isPositiveStrand) + color = highlight ? ARC_COLOR_HIGHLIGHT_POS : ARC_COLOR_POS; + else + color = highlight ? ARC_COLOR_HIGHLIGHT_NEG : ARC_COLOR_NEG; + } + + g2D.setColor(color); + + //Height of top of an arc of maximum depth + int maxPossibleArcHeight = (trackRectangle.height - 1) / 2; + + if (shouldShowFlankingRegions) { + if (junctionFeature.hasFlankingRegionDepthArrays()) { + //draw a wigglegram of the splice junction flanking region depth of coverage + + int startFlankingRegionPixelLength = pixelJunctionStart - pixelFeatureStart; + int endFlankingRegionPixelLength = pixelFeatureEnd - pixelJunctionEnd; + + drawFlankingRegion(g2D, pixelFeatureStart, startFlankingRegionPixelLength, + junctionFeature.getStartFlankingRegionDepthArray(), maxPossibleArcHeight, + trackRectangle, isPositiveStrand); + drawFlankingRegion(g2D, pixelJunctionEnd + 1, endFlankingRegionPixelLength, + junctionFeature.getEndFlankingRegionDepthArray(), maxPossibleArcHeight, + trackRectangle, isPositiveStrand); + } else { + //Draw rectangles indicating the overlap on each side of the junction + int overlapRectHeight = 3; + int overlapRectTopX = (int) trackRectangle.getCenterY() + (isPositiveStrand ? -2 : 0); + if (pixelFeatureStart < pixelJunctionStart) { + g2D.fillRect(pixelFeatureStart, overlapRectTopX, + pixelJunctionStart - pixelFeatureStart, overlapRectHeight); + } + if (pixelJunctionEnd < pixelFeatureEnd) { + g2D.fillRect(pixelJunctionEnd, overlapRectTopX, + pixelFeatureEnd - pixelJunctionEnd, overlapRectHeight); + } + } + } + + //Create a path describing the arc, using Bezier curves. The Bezier control points for the top and + //bottom arcs are based on the boundary points of the rectangles containing the arcs + + //proportion of the maximum arc height used by a minimum-height arc + double minArcHeightProportion = 0.33; + + int innerArcHeight = (int) (maxPossibleArcHeight * minArcHeightProportion); + float depthProportionOfMax = Math.min(1, depth / maxDepth); + int arcWidth = Math.max(1, (int) ((1 - minArcHeightProportion) * maxPossibleArcHeight * depthProportionOfMax)); + int outerArcHeight = innerArcHeight + arcWidth; + + + //Height of bottom of the arc + int arcBeginY = (int) trackRectangle.getCenterY() + (isPositiveStrand ? -1 : 1); + int outerArcPeakY = isPositiveStrand ? arcBeginY - outerArcHeight : arcBeginY + outerArcHeight; + int innerArcPeakY = isPositiveStrand ? arcBeginY - innerArcHeight : arcBeginY + innerArcHeight; + + //dhmay: I don't really understand Bezier curves. For some reason I have to put the Bezier control + //points farther up or down than I want the arcs to extend. This multiplier seems about right + int outerBezierY = arcBeginY + (int) (1.3 * (outerArcPeakY - arcBeginY)); + int innerBezierY = arcBeginY + (int) (1.3 * (innerArcPeakY - arcBeginY)); + + //Putting the Bezier control points slightly off to the sides of the arc + int bezierXPad = Math.max(1, (pixelJunctionEnd - pixelJunctionStart) / 30); + + GeneralPath arcPath = new GeneralPath(); + arcPath.moveTo(pixelJunctionStart, arcBeginY); + arcPath.curveTo(pixelJunctionStart - bezierXPad, outerBezierY, //Bezier 1 + pixelJunctionEnd + bezierXPad, outerBezierY, //Bezier 2 + pixelJunctionEnd, arcBeginY); //Arc end + arcPath.curveTo(pixelJunctionEnd + bezierXPad, innerBezierY, //Bezier 1 + pixelJunctionStart - bezierXPad, innerBezierY, //Bezier 2 + pixelJunctionStart, arcBeginY); //Arc end + + g2D.setClip(new Rectangle(pixelJunctionStart, trackRectangle.y, pixelJunctionEnd - pixelJunctionStart, trackRectangle.height)); + + //Draw the arc, to ensure outline is drawn completely (fill won't do it, necessarily). This will also + //give the arc a darker outline + g2D.draw(arcPath); + //Fill the arc + g2D.fill(arcPath); + + g2D.setClip(null); + //g2D.drawLine(pixelJunctionStart, trackRectangle.y, pixelJunctionStart, trackRectangle.y + trackRectangle.height); + //g2D.drawLine(pixelJunctionEnd, trackRectangle.y, pixelJunctionEnd, trackRectangle.y + trackRectangle.height); + + } + + + + /** + * Draw depth of coverage for the starting or ending flanking region + * + * @param g2D + * @param pixelStart + * @param pixelLength + * @param regionDepthArray + * @param maxPossibleArcHeight + * @param trackRectangle + * @param isPositiveStrand + */ + protected void drawFlankingRegion(Graphics g2D, int pixelStart, int pixelLength, int[] regionDepthArray, + int maxPossibleArcHeight, Rectangle trackRectangle, boolean isPositiveStrand) { + for (int i = 0; i < pixelLength; i++) { + float arrayIndicesPerPixel = (float) regionDepthArray.length / + (float) pixelLength; + int flankingRegionArrayPixelMinIndex = (int) (i * arrayIndicesPerPixel); + int flankingRegionArrayPixelMaxIndex = (int) ((i + 1) * arrayIndicesPerPixel); + flankingRegionArrayPixelMinIndex = + Math.max(0, Math.min(flankingRegionArrayPixelMinIndex, regionDepthArray.length - 1)); + flankingRegionArrayPixelMaxIndex = + Math.max(0, Math.min(flankingRegionArrayPixelMaxIndex, regionDepthArray.length - 1)); + + int meanDepthThisPixel = 0; + for (int j = flankingRegionArrayPixelMinIndex; j <= flankingRegionArrayPixelMaxIndex; j++) + meanDepthThisPixel += regionDepthArray[j]; + meanDepthThisPixel /= (flankingRegionArrayPixelMaxIndex - flankingRegionArrayPixelMinIndex + 1); + meanDepthThisPixel = Math.min(maxDepth, meanDepthThisPixel); + int pixelHeight = Math.max(maxPossibleArcHeight * meanDepthThisPixel / maxDepth, 2); + g2D.fillRect(pixelStart + i, + (int) trackRectangle.getCenterY() + (isPositiveStrand ? -pixelHeight : 0), + 1, pixelHeight); + } + } + + + public int getMaxDepth() { + return maxDepth; + } + + public void setMaxDepth(int maxDepth) { + this.maxDepth = maxDepth; + } + } From 8c599b8ca39d4de5017478edb20429d25c57391e Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Mon, 12 Aug 2024 12:54:52 -0700 Subject: [PATCH 020/130] compute long chr names for chrom.sizes file --- .../org/broad/igv/feature/genome/Genome.java | 3 ++- .../broad/igv/tools/IGVToolsCountTest.java | 24 +++++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/broad/igv/feature/genome/Genome.java b/src/main/java/org/broad/igv/feature/genome/Genome.java index 80fd99ed78..86b9366656 100644 --- a/src/main/java/org/broad/igv/feature/genome/Genome.java +++ b/src/main/java/org/broad/igv/feature/genome/Genome.java @@ -224,7 +224,7 @@ public Genome(GenomeConfig config) throws IOException { /** * Alternate constructor for defining a minimal genome, usually from parsing a chrom.sizes file. Used to - * create mock genomes for testing. + * create mock genomes for igvtools and testing. * * @param id * @param chromosomes @@ -241,6 +241,7 @@ public Genome(String id, List chromosomes) { chromosomeNames.add(chromosome.getName()); chromosomeMap.put(chromosome.getName(), chromosome); } + this.longChromosomeNames = computeLongChromosomeNames(); } private void addTracks(GenomeConfig config) { diff --git a/src/test/java/org/broad/igv/tools/IGVToolsCountTest.java b/src/test/java/org/broad/igv/tools/IGVToolsCountTest.java index dbd6c5903f..38dad46245 100644 --- a/src/test/java/org/broad/igv/tools/IGVToolsCountTest.java +++ b/src/test/java/org/broad/igv/tools/IGVToolsCountTest.java @@ -30,6 +30,7 @@ import org.broad.igv.data.WiggleParser; import org.broad.igv.feature.LocusScore; import org.broad.igv.feature.genome.Genome; +import org.broad.igv.feature.genome.GenomeUtils; import org.broad.igv.feature.tribble.CodecFactory; import org.broad.igv.tdf.TDFDataSource; import org.broad.igv.tdf.TDFDataset; @@ -57,7 +58,7 @@ public class IGVToolsCountTest extends AbstractHeadlessTest { IgvTools igvTools; - private static final String hg18id = TestUtils.DATA_DIR + "genomes/hg18.unittest.genome"; + private static final String genomePath = TestUtils.DATA_DIR + "genomes/hg19.chrom.sizes"; private static final int MAX_LINES_CHECK = 200; @Rule @@ -91,9 +92,12 @@ public void testCountBEDoutWig() throws Exception { @Test public void testCountBEDoutWigCheckBW() throws Exception { + + GenomeUtils.main(null); + String inputFile = TestUtils.DATA_DIR + "bed/test2.bed"; String fullout = TestUtils.TMP_OUTPUT_DIR + "twig.wig"; - String input = "count " + inputFile + " " + fullout + " " + hg18id; + String input = "count " + inputFile + " " + fullout + " " + genomePath; String[] args = input.split("\\s+"); igvTools.run(args); @@ -126,7 +130,7 @@ public void testCountBEDstdoutWig() throws Exception { //Write to a file String fileOutArg = TestUtils.TMP_OUTPUT_DIR + "testfilewig.wig"; - String input = "count " + inputFile + " " + fileOutArg + " " + hg18id; + String input = "count " + inputFile + " " + fileOutArg + " " + genomePath; String[] args = input.split("\\s+"); igvTools.run(args); @@ -135,7 +139,7 @@ public void testCountBEDstdoutWig() throws Exception { PrintStream oldOut = System.out; System.setOut(new PrintStream(os)); - input = "count " + inputFile + " " + IgvTools.STDOUT_FILE_STR + " " + hg18id; + input = "count " + inputFile + " " + IgvTools.STDOUT_FILE_STR + " " + genomePath; args = input.split("\\s+"); (new IgvTools()).run(args); @@ -230,7 +234,7 @@ public void tstCountOptsGen(String inputFile, String outputBase, String outputEx if (ind == 0) refOpt = opt; String fullout = outputFile + ind + "." + outputExt; - String input = "count " + opt + " " + inputFile + " " + fullout + " " + hg18id; + String input = "count " + opt + " " + inputFile + " " + fullout + " " + genomePath; String[] args = input.split("\\s+"); igvTools.run(args); @@ -253,7 +257,7 @@ public void tstCountOptsGen(String inputFile, String outputBase, String outputEx assertTrue(outFile.canRead()); ResourceLocator locator = new ResourceLocator(fullout); - WiggleDataset ds = (new WiggleParser(locator, IgvTools.loadGenome(hg18id))).parse(); + WiggleDataset ds = (new WiggleParser(locator, IgvTools.loadGenome(genomePath))).parse(); //We miss a few alignments with this option sometimes, //so it doesn't total up the same @@ -358,7 +362,7 @@ private void tstCountBamList(String listArg) throws Exception { for (int ind = 0; ind < opts.length; ind++) { String opt = opts[ind]; String fullout = outputFile + ind + ".tdf"; - String input = "count " + opt + " " + listArg + " " + fullout + " " + hg18id; + String input = "count " + opt + " " + listArg + " " + fullout + " " + genomePath; String[] args = input.split("\\s+"); igvTools.run(args); @@ -379,10 +383,10 @@ public void testCountDups() throws Exception { int pos = 9718611; String queryStr = queryChr + ":" + (pos - 100) + "-" + (pos + 100) + " "; - String cmd_nodups = "count --windowSize 1 -z 7 --query " + queryStr + inputFile + " " + outputFileND + " " + hg18id; + String cmd_nodups = "count --windowSize 1 -z 7 --query " + queryStr + inputFile + " " + outputFileND + " " + genomePath; igvTools.run(cmd_nodups.split("\\s+")); - String cmd_withdups = "count --includeDuplicates -z 7 --windowSize 1 --query " + queryStr + inputFile + " " + outputFileWithDup + " " + hg18id; + String cmd_withdups = "count --includeDuplicates -z 7 --windowSize 1 --query " + queryStr + inputFile + " " + outputFileWithDup + " " + genomePath; igvTools.run(cmd_withdups.split("\\s+")); assertTrue((new File(outputFileND).exists())); @@ -532,7 +536,7 @@ private void tstCountWindowFunctions(String inputFile, String chr, Iterable Date: Mon, 12 Aug 2024 18:13:54 -0700 Subject: [PATCH 021/130] Fix unit tests and issue with "chrom.sizes" genomes. --- .../java/org/broad/igv/feature/genome/Genome.java | 14 ++++++++------ .../igv/feature/genome/load/DotGenomeLoader.java | 2 +- .../org/broad/igv/tools/IGVToolsCountTest.java | 2 -- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/broad/igv/feature/genome/Genome.java b/src/main/java/org/broad/igv/feature/genome/Genome.java index 86b9366656..990bd1ffd6 100644 --- a/src/main/java/org/broad/igv/feature/genome/Genome.java +++ b/src/main/java/org/broad/igv/feature/genome/Genome.java @@ -139,7 +139,7 @@ public Genome(GenomeConfig config) throws IOException { List chromosomeList = null; if (config.chromSizesURL != null) { chromosomeList = ChromSizesParser.parse(config.chromSizesURL); - } else if (sequence.hasChromosomes()) { + } else if (sequence != null && sequence.hasChromosomes()) { chromosomeList = sequence.getChromosomes(); } else if (config.indexURL != null) { FastaIndex index = new FastaIndex(config.indexURL); @@ -173,7 +173,7 @@ public Genome(GenomeConfig config) throws IOException { // Whole genome view is enabled by default if we have the chromosome information amd the // number of chromosomes is not too large - showWholeGenomeView = config.wholeGenomeView && + showWholeGenomeView = config.wholeGenomeView && chromosomeList.size() > 1 && longChromosomeNames.size() <= MAX_WHOLE_GENOME_LONG; @@ -242,6 +242,7 @@ public Genome(String id, List chromosomes) { chromosomeMap.put(chromosome.getName(), chromosome); } this.longChromosomeNames = computeLongChromosomeNames(); + this.chromAliasSource = (new ChromAliasDefaults(id, chromosomeNames)); } private void addTracks(GenomeConfig config) { @@ -291,7 +292,9 @@ public String getCanonicalChrName(String str) { ChromAlias aliasRecord = chromAliasSource.search(str); if (aliasRecord != null) { String chr = aliasRecord.getChr(); - chrAliasCache.put(str, chr); + for (String a : aliasRecord.values()) { + chrAliasCache.put(a, chr); + } return chr; } } catch (IOException e) { @@ -404,17 +407,16 @@ public Chromosome getChromosome(String name) { String chrName = getCanonicalChrName(name); if (chromosomeMap.containsKey(chrName)) { return chromosomeMap.get(chrName); - } else { + } else if (sequence != null) { int length = this.sequence.getChromosomeLength(chrName); if (length > 0) { int idx = this.chromosomeMap.size(); Chromosome chromosome = new Chromosome(idx, chrName, length); chromosomeMap.put(chrName, chromosome); return chromosome; - } else { - return null; } } + return null; } diff --git a/src/main/java/org/broad/igv/feature/genome/load/DotGenomeLoader.java b/src/main/java/org/broad/igv/feature/genome/load/DotGenomeLoader.java index 561a2e5671..d764ba5a63 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/DotGenomeLoader.java +++ b/src/main/java/org/broad/igv/feature/genome/load/DotGenomeLoader.java @@ -102,7 +102,7 @@ public Genome loadGenome() throws IOException { config.fastaURL = sequencePath; config.indexURL = sequencePath + ".fai"; if (sequencePath.endsWith(".gz")) { - config.gziIndexURL = sequencePath + "gzi"; + config.gziIndexURL = sequencePath + ".gzi"; } } diff --git a/src/test/java/org/broad/igv/tools/IGVToolsCountTest.java b/src/test/java/org/broad/igv/tools/IGVToolsCountTest.java index 38dad46245..3c4a891964 100644 --- a/src/test/java/org/broad/igv/tools/IGVToolsCountTest.java +++ b/src/test/java/org/broad/igv/tools/IGVToolsCountTest.java @@ -93,8 +93,6 @@ public void testCountBEDoutWig() throws Exception { @Test public void testCountBEDoutWigCheckBW() throws Exception { - GenomeUtils.main(null); - String inputFile = TestUtils.DATA_DIR + "bed/test2.bed"; String fullout = TestUtils.TMP_OUTPUT_DIR + "twig.wig"; String input = "count " + inputFile + " " + fullout + " " + genomePath; From 71e111a74ef061b35f3325cf38cc7d8e8a102556 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Tue, 13 Aug 2024 21:51:39 -0700 Subject: [PATCH 022/130] Fix logic searching for features containing a position. Fixes #1541 --- .../org/broad/igv/feature/FeatureUtils.java | 37 ++----------------- .../org/broad/igv/feature/dsi/DSITrack.java | 2 +- .../org/broad/igv/track/FeatureTrack.java | 16 +++----- .../broad/igv/feature/FeatureUtilsTest.java | 23 +----------- 4 files changed, 12 insertions(+), 66 deletions(-) diff --git a/src/main/java/org/broad/igv/feature/FeatureUtils.java b/src/main/java/org/broad/igv/feature/FeatureUtils.java index edc129e7c4..213ebeff54 100644 --- a/src/main/java/org/broad/igv/feature/FeatureUtils.java +++ b/src/main/java/org/broad/igv/feature/FeatureUtils.java @@ -116,33 +116,6 @@ public static Feature getFeatureStartsAfter(double position, List features) { - - int index = getIndexBefore(position, features); - Feature prevFeature = null; - while (index >= 0) { - Feature f = features.get(index); - if (f.getEnd() < position) { - if (prevFeature == null) { - prevFeature = f; - } else { - if (f.getStart() == prevFeature.getStart()) { - // Prefer smallest feature - if (f.getEnd() < prevFeature.getEnd()) { - prevFeature = f; - } - } else { - // Done - break; - } - } - } - index--; - } - return prevFeature; - - } - /** * Return the first feature whose center is > the given position. If no features satisfy the criteria * return null; @@ -374,15 +347,13 @@ public static int getIndexBeforeOld(double position, List fea * @param features * @return */ - public static List getAllFeaturesAt(double position, - double flanking, - List features) { + public static List getAllFeaturesContaining(double position, + double flanking, + List features) { List returnList = null; - int startIdx = Math.max(0, getIndexBefore(position, features)); - for (int idx = startIdx; idx < features.size(); idx++) { - Feature feature = features.get(idx); + for (Feature feature : features) { if (feature.getStart() > position + flanking) { break; } diff --git a/src/main/java/org/broad/igv/feature/dsi/DSITrack.java b/src/main/java/org/broad/igv/feature/dsi/DSITrack.java index a4f0e4d853..2431fb4751 100644 --- a/src/main/java/org/broad/igv/feature/dsi/DSITrack.java +++ b/src/main/java/org/broad/igv/feature/dsi/DSITrack.java @@ -26,7 +26,7 @@ public DSITrack(ResourceLocator locator, FeatureSource src) { @Override public String getValueStringAt(String chr, double position, int mouseX, int mouseY, ReferenceFrame frame) { - List allFeatures = getAllFeatureAt(position, mouseY, frame); + List allFeatures = getAllFeaturesContaining(position, mouseY, frame); if (allFeatures == null) { return null; } diff --git a/src/main/java/org/broad/igv/track/FeatureTrack.java b/src/main/java/org/broad/igv/track/FeatureTrack.java index 2210c5a8aa..97e0291305 100644 --- a/src/main/java/org/broad/igv/track/FeatureTrack.java +++ b/src/main/java/org/broad/igv/track/FeatureTrack.java @@ -382,7 +382,7 @@ public String getValueStringAt(String chr, double position, int mouseX, int mous if (showFeatures) { - List allFeatures = getAllFeatureAt(position, mouseY, frame); + List allFeatures = getAllFeaturesContaining(position, mouseY, frame); if (allFeatures == null) { return null; } @@ -495,7 +495,7 @@ public List getFeatures(String chr, int start, int end) { * @param frame * @return */ - protected List getAllFeatureAt(double position, int y, ReferenceFrame frame) { + protected List getAllFeaturesContaining(double position, int y, ReferenceFrame frame) { // Determine the level number (for expanded tracks) int featureRow = getFeatureRow(y); return getFeaturesAtPositionInFeatureRow(position, featureRow, frame); @@ -549,19 +549,15 @@ public List getFeaturesAtPositionInFeatureRow(double position, int feat //If features are stacked we look at only the row. //If they are collapsed on top of each other, we get all features in all rows - List possFeatures; - if (getDisplayMode() == DisplayMode.COLLAPSED) { - possFeatures = packedFeatures.getFeatures(); - } else { - possFeatures = rows.get(featureRow).getFeatures(); - } + List possFeatures = rows.get(featureRow).getFeatures(); + List featureList = null; if (possFeatures != null) { // give a minum 2 pixel or 1/2 bp window, otherwise very narrow features will be missed. double bpPerPixel = frame.getScale(); double flanking = 4 * bpPerPixel; - featureList = FeatureUtils.getAllFeaturesAt(position, flanking, possFeatures); + featureList = FeatureUtils.getAllFeaturesContaining(position, flanking, possFeatures); } return featureList; } @@ -631,7 +627,7 @@ public Feature getFeatureAtMousePosition(TrackClickEvent te) { final ReferenceFrame referenceFrame = te.getFrame(); if (referenceFrame != null) { double location = referenceFrame.getChromosomePosition(e); - List features = getAllFeatureAt(location, e.getY(), referenceFrame); + List features = getAllFeaturesContaining(location, e.getY(), referenceFrame); return (features != null && features.size() > 0) ? features.get(0) : null; } else { return null; diff --git a/src/test/java/org/broad/igv/feature/FeatureUtilsTest.java b/src/test/java/org/broad/igv/feature/FeatureUtilsTest.java index 6635cd669a..0e6b6cc326 100644 --- a/src/test/java/org/broad/igv/feature/FeatureUtilsTest.java +++ b/src/test/java/org/broad/igv/feature/FeatureUtilsTest.java @@ -45,7 +45,6 @@ import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import static org.junit.Assert.*; @@ -129,10 +128,8 @@ public void testGetFeatureClosestWithIndels(){ public void testGetAllFeaturesAt() { int position = 56078756; - int maxLength = 100000; - - List result = FeatureUtils.getAllFeaturesAt(position, 0, featureList); + List result = FeatureUtils.getAllFeaturesContaining(position, 0, featureList); assertEquals(21, result.size()); for (Feature f : result) { assertTrue(position >= f.getStart() && position <= f.getEnd()); @@ -192,24 +189,6 @@ public void testGetFeatureCenterAfter() { assertNull(f); } - @Test - public void testGetFeatureBefore() { - - // feature - // chr7 56078756 56119137 - Feature f = FeatureUtils.getFeatureEndsBefore(56119137 + 1, featureList); - assertEquals(56078756, f.getStart()); - - // chr7 55086709 55236328 - f = FeatureUtils.getFeatureStartsAfter(0, featureList); - assertEquals(55086709, f.getStart()); - - // last feature - // chr7 56182373 56184110 - f = FeatureUtils.getFeatureStartsAfter(56182373, featureList); - assertNull(f); - } - @Test public void testGetFeatureCenterBefore() { From b730c8ad9cac677c76d070b1a05031270c0fc4f8 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Wed, 14 Aug 2024 07:17:00 -0700 Subject: [PATCH 023/130] Clarify zoom in instructions for whole genome view. Fixes #1539 --- src/main/java/org/broad/igv/sam/AlignmentTrack.java | 6 +++++- src/main/java/org/broad/igv/sam/CoverageTrack.java | 5 ++++- .../java/org/broad/igv/sam/SpliceJunctionTrack.java | 11 ++++------- src/main/java/org/broad/igv/track/FeatureTrack.java | 2 +- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/broad/igv/sam/AlignmentTrack.java b/src/main/java/org/broad/igv/sam/AlignmentTrack.java index e79af83c6d..1fbb85d916 100644 --- a/src/main/java/org/broad/igv/sam/AlignmentTrack.java +++ b/src/main/java/org/broad/igv/sam/AlignmentTrack.java @@ -543,7 +543,11 @@ public void render(RenderContext context, Rectangle rect) { if (viewWindowSize > getVisibilityWindow()) { Rectangle visibleRect = context.getVisibleRect().intersection(rect); Graphics2D g2 = context.getGraphic2DForColor(Color.gray); - GraphicUtils.drawCenteredText("Zoom in to see alignments.", visibleRect, g2); + String message = context.getReferenceFrame().getChrName().equals(Globals.CHR_ALL) ? + "Select a chromosome and zoom in to see alignments." : + "Zoom in to see alignments."; + + GraphicUtils.drawCenteredText(message, visibleRect, g2); return; } diff --git a/src/main/java/org/broad/igv/sam/CoverageTrack.java b/src/main/java/org/broad/igv/sam/CoverageTrack.java index 1d39e6a320..c7bcfae143 100644 --- a/src/main/java/org/broad/igv/sam/CoverageTrack.java +++ b/src/main/java/org/broad/igv/sam/CoverageTrack.java @@ -224,7 +224,10 @@ public void render(RenderContext context, Rectangle rect) { if (viewWindowSize > getVisibilityWindow() && dataSource == null) { Rectangle visibleRect = context.getVisibleRect().intersection(rect); Graphics2D g = context.getGraphic2DForColor(Color.gray); - GraphicUtils.drawCenteredText("Zoom in to see coverage.", visibleRect, g); + String message = context.getReferenceFrame().getChrName().equals(Globals.CHR_ALL) ? + "Select a chromosome and zoom in to see coverage." : + "Zoom in to see coverage."; + GraphicUtils.drawCenteredText(message, visibleRect, g); return; } diff --git a/src/main/java/org/broad/igv/sam/SpliceJunctionTrack.java b/src/main/java/org/broad/igv/sam/SpliceJunctionTrack.java index bda5a5941d..a65989ff65 100644 --- a/src/main/java/org/broad/igv/sam/SpliceJunctionTrack.java +++ b/src/main/java/org/broad/igv/sam/SpliceJunctionTrack.java @@ -123,8 +123,10 @@ public void render(RenderContext context, Rectangle rect) { if (!isShowFeatures(context.getReferenceFrame())) { Rectangle visibleRect = context.getVisibleRect().intersection(rect); Graphics2D g = context.getGraphic2DForColor(Color.gray); - GraphicUtils.drawCenteredText("Zoom in to see features.", visibleRect, g); - return; + String message = context.getReferenceFrame().getChrName().equals(Globals.CHR_ALL) ? + "Select a chromosome and zoom in to see features." : + "Zoom in to see features."; + GraphicUtils.drawCenteredText(message, visibleRect, g); } else { super.render(context, rect); } @@ -207,11 +209,6 @@ public float getRegionScore(String chr, int start, int end, int zoom, RegionScor return 0; } - @Override - protected String getZoomInMessage(String chr) { - return "Zoom in to see junctions."; - } - @Override protected void renderFeatures(RenderContext context, Rectangle inputRect) { diff --git a/src/main/java/org/broad/igv/track/FeatureTrack.java b/src/main/java/org/broad/igv/track/FeatureTrack.java index 97e0291305..056c33f3b1 100644 --- a/src/main/java/org/broad/igv/track/FeatureTrack.java +++ b/src/main/java/org/broad/igv/track/FeatureTrack.java @@ -820,7 +820,7 @@ protected void renderCoverage(RenderContext context, Rectangle inputRect) { } protected String getZoomInMessage(String chr) { - return chr.equals(Globals.CHR_ALL) ? "Zoom in to see features." : + return chr.equals(Globals.CHR_ALL) ? "Select a chromosome and zoom in to see features." : "Zoom in to see features, or right-click to increase Feature Visibility Window."; } From d372765e32e7f80c619f1bdb52579e0b800a6e8e Mon Sep 17 00:00:00 2001 From: Louis Bergelson Date: Wed, 14 Aug 2024 18:55:16 -0400 Subject: [PATCH 024/130] Move the amazon credentials check at startup into an async thread (#1543) * Move the amazon credentials check at startup into an async thread * Profiling showed it was adding about half a second at startup --------- Co-authored-by: jrobinso <933148+jrobinso@users.noreply.github.com> --- .../java/org/broad/igv/ui/IGVMenuBar.java | 20 +++++++++---------- .../java/org/broad/igv/util/AmazonUtils.java | 2 +- .../org/broad/igv/util/LongRunningTask.java | 9 +++++---- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/broad/igv/ui/IGVMenuBar.java b/src/main/java/org/broad/igv/ui/IGVMenuBar.java index ab46607fa1..0d1d91da48 100644 --- a/src/main/java/org/broad/igv/ui/IGVMenuBar.java +++ b/src/main/java/org/broad/igv/ui/IGVMenuBar.java @@ -194,14 +194,14 @@ private List createMenus() { log.error("Error creating google menu: " + e.getMessage()); } - try { - AWSMenu = createAWSMenu(); - AWSMenu.setVisible(AmazonUtils.isAwsProviderPresent()); - menus.add(AWSMenu); - } catch (IOException e) { - log.error("Error creating the Amazon AWS menu: " + e.getMessage()); - AWSMenu.setVisible(false); - } + + AWSMenu = createAWSMenu(); + AWSMenu.setVisible(false); + menus.add(AWSMenu); + //detecting the provider is slow, do it in another thread + LongRunningTask.submit(this::updateAWSMenu); + + menus.add(createHelpMenu()); @@ -211,7 +211,7 @@ private List createMenus() { } public void updateAWSMenu() { - AWSMenu.setVisible(AmazonUtils.isAwsProviderPresent()); + SwingUtilities.invokeLater(() -> AWSMenu.setVisible(AmazonUtils.isAwsProviderPresent())); } /** @@ -963,7 +963,7 @@ public void actionPerformed(ActionEvent actionEvent) { return menu; } - private JMenu createAWSMenu() throws IOException { + private JMenu createAWSMenu() { boolean usingCognito = AmazonUtils.GetCognitoConfig() != null; diff --git a/src/main/java/org/broad/igv/util/AmazonUtils.java b/src/main/java/org/broad/igv/util/AmazonUtils.java index 2df905a817..e832e5a67d 100644 --- a/src/main/java/org/broad/igv/util/AmazonUtils.java +++ b/src/main/java/org/broad/igv/util/AmazonUtils.java @@ -83,7 +83,7 @@ public static JsonObject GetCognitoConfig() { /** - * Test to see if aws credentials are avaialable, either through IGV configuration of Cognito, or from the + * Test to see if aws credentials are available, either through IGV configuration of Cognito, or from the * default provider chain. See https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html#credentials-default * * @return diff --git a/src/main/java/org/broad/igv/util/LongRunningTask.java b/src/main/java/org/broad/igv/util/LongRunningTask.java index c3a64650f9..a37d21804d 100644 --- a/src/main/java/org/broad/igv/util/LongRunningTask.java +++ b/src/main/java/org/broad/igv/util/LongRunningTask.java @@ -40,9 +40,9 @@ * * @author jrobinso */ -public class LongRunningTask implements Callable { +public class LongRunningTask implements Callable { - private static Logger log = LogManager.getLogger(LongRunningTask.class); + private static final Logger log = LogManager.getLogger(LongRunningTask.class); private static final ExecutorService threadExecutor = Executors.newFixedThreadPool(5); @@ -52,7 +52,7 @@ public static Executor getThreadExecutor() { return threadExecutor; } - public static Future submit(Runnable runnable) { + public static Future submit(Runnable runnable) { if (Globals.isBatch()) { runnable.run(); return null; @@ -65,7 +65,8 @@ private LongRunningTask(Runnable runnable) { this.runnable = runnable; } - public Object call() { + @Override + public Void call() { CursorToken token = WaitCursorManager.showWaitCursor(); try { From b611dbc82231b802f9bf30cc4fbca85ca07a27e4 Mon Sep 17 00:00:00 2001 From: Louis Bergelson Date: Wed, 14 Aug 2024 22:46:25 -0400 Subject: [PATCH 025/130] Update dependencies to fix vulnerabilities (#1544) --- build.gradle | 25 ++++++++++++++----------- src/main/java/module-info.java | 2 +- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/build.gradle b/build.gradle index b41acb8c8f..6141ae4ea1 100644 --- a/build.gradle +++ b/build.gradle @@ -101,35 +101,38 @@ configurations { } } +def amazonVersion = '2.27.4' +def xmlGraphicsVersion = '1.17' + dependencies { implementation( fileTree(dir: 'lib', include: '*.jar'), // first search on disk (old behavior), then maven repos [group: 'com.google.code.gson', name: 'gson', version: '2.8.9'], [group: 'org.apache.commons', name: 'commons-math3', version: '3.6.1'], [group: 'commons-io', name: 'commons-io', version: '2.7'], - [group: 'org.apache.commons', name: 'commons-compress', version: '1.21'], - [group: 'org.xerial.snappy', name: 'snappy-java', version: '1.1.7.3'], + [group: 'org.apache.commons', name: 'commons-compress', version: '1.26.0'], + [group: 'org.xerial.snappy', name: 'snappy-java', version: '1.1.10.4'], [group: 'org.apache.commons', name: 'commons-jexl', version: '2.1.1'], [group: 'org.apache.commons', name: 'commons-lang3', version: '3.9'], - [group: 'com.github.samtools', name: 'htsjdk', version: '4.0.2'], + [group: 'com.github.samtools', name: 'htsjdk', version: '4.1.1'], [group: 'org.swinglabs', name: 'swing-layout', version: '1.0.3'], [group: 'com.formdev', name: 'jide-oss', version: '3.7.12'], [group: 'com.google.guava', name: 'guava', version: '32.1.3-jre'], - [group: 'org.apache.xmlgraphics', name: 'batik-dom', version: '1.11'], - [group: 'org.apache.xmlgraphics', name: 'batik-svggen', version: '1.11'], - [group: 'org.apache.xmlgraphics', name: 'batik-codec', version: '1.11'], + [group: 'org.apache.xmlgraphics', name: 'batik-dom', version: xmlGraphicsVersion], + [group: 'org.apache.xmlgraphics', name: 'batik-svggen', version: xmlGraphicsVersion], + [group: 'org.apache.xmlgraphics', name: 'batik-codec', version: xmlGraphicsVersion], [group: 'org.netbeans.external', name: 'AbsoluteLayout', version: 'RELEASE110'], // Amazon deps - [group: 'software.amazon.awssdk', name: 'cognitoidentity', version: '2.16.7'], - [group: 'software.amazon.awssdk', name: 'sts', version: '2.16.7'], - [group: 'software.amazon.awssdk', name: 's3', version: '2.16.7'], - [group: 'software.amazon.awssdk', name: 'sso', version: '2.16.7'] + [group: 'software.amazon.awssdk', name: 'cognitoidentity', version: amazonVersion], + [group: 'software.amazon.awssdk', name: 'sts', version: amazonVersion], + [group: 'software.amazon.awssdk', name: 's3', version: amazonVersion], + [group: 'software.amazon.awssdk', name: 'sso', version: amazonVersion] ) testImplementation( [group: 'junit', name: 'junit', version: '4.13.1'], - [group: 'com.sparkjava', name: 'spark-core', version: '2.7.2'], + [group: 'com.sparkjava', name: 'spark-core', version: '2.9.4'], [group: 'org.glassfish.jersey.core', name: 'jersey-common', version: '2.34'] ) testRuntimeOnly( diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index fbbc4eb96e..fb0b4a1f3c 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -42,6 +42,6 @@ requires software.amazon.awssdk.services.sts; requires software.amazon.awssdk.http; requires software.amazon.awssdk.utils; - requires com.fasterxml.jackson.core; + requires jide.oss; } From 1ed0fc4f9517aa553b1c72301e506ae7e086134d Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Fri, 16 Aug 2024 16:54:43 -0700 Subject: [PATCH 026/130] Use specifed nameSet, if specified, for whole genome and pulldown chromosome names. --- .../java/org/broad/igv/ui/commandbar/ChromosomeComboBox.java | 4 +++- src/main/java/org/broad/igv/ui/panel/RulerPanel.java | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/broad/igv/ui/commandbar/ChromosomeComboBox.java b/src/main/java/org/broad/igv/ui/commandbar/ChromosomeComboBox.java index 4c5c4bb577..77b9051326 100644 --- a/src/main/java/org/broad/igv/ui/commandbar/ChromosomeComboBox.java +++ b/src/main/java/org/broad/igv/ui/commandbar/ChromosomeComboBox.java @@ -10,6 +10,7 @@ import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; /** * Created by jrobinso on 7/6/17. @@ -39,7 +40,8 @@ public void updateChromosFromGenome(Genome genome) { UIUtilities.invokeAndWaitOnEventThread(() -> { - List allChromosomeNames = genome.getChromosomeNames(); + List allChromosomeNames = genome.getChromosomeNames().stream().map(chr -> genome.getChromosomeDisplayName(chr)).collect(Collectors.toList()); + if (allChromosomeNames.size() > 1) { this.setVisible(true); diff --git a/src/main/java/org/broad/igv/ui/panel/RulerPanel.java b/src/main/java/org/broad/igv/ui/panel/RulerPanel.java index 5dcd826922..dd27d5b5fa 100644 --- a/src/main/java/org/broad/igv/ui/panel/RulerPanel.java +++ b/src/main/java/org/broad/igv/ui/panel/RulerPanel.java @@ -277,7 +277,7 @@ private void drawChromosomeTicks(Graphics g) { if (dw > 5) { int center = x + dw / 2; - String displayName = chrName; + String displayName = genome.getChromosomeDisplayName(chrName); if (chrName.startsWith("gi|")) { displayName = Genome.getNCBIName(chrName); } else if (chrName.length() < 6) { From cc37d33fd6d26672c2e557717dc174a04862c0fc Mon Sep 17 00:00:00 2001 From: Louis Bergelson Date: Sun, 18 Aug 2024 00:52:04 -0400 Subject: [PATCH 027/130] Adding additional options for duplicate reads. (#1542) Adding additional options for duplicate reads. * new GroupBy duplicates option * new Duplicates submenu which allows rapid toggling of duplicates between three states * Filtered: not visible * Visible: treated like normal reads * Textures: rendered with a visible slash texture --- .../org/broad/igv/sam/AlignmentPacker.java | 117 +++++++++--------- .../org/broad/igv/sam/AlignmentRenderer.java | 49 +++++++- .../broad/igv/sam/AlignmentTileLoader.java | 7 +- .../org/broad/igv/sam/AlignmentTrack.java | 40 +++++- .../org/broad/igv/sam/AlignmentTrackMenu.java | 36 +++++- .../org/broad/igv/prefs/preferences.tab | 2 +- 6 files changed, 178 insertions(+), 73 deletions(-) diff --git a/src/main/java/org/broad/igv/sam/AlignmentPacker.java b/src/main/java/org/broad/igv/sam/AlignmentPacker.java index 6c269e786f..48bfee048f 100644 --- a/src/main/java/org/broad/igv/sam/AlignmentPacker.java +++ b/src/main/java/org/broad/igv/sam/AlignmentPacker.java @@ -438,69 +438,64 @@ private Object getGroupValue(Alignment al, AlignmentTrack.RenderOptions renderOp Range pos = renderOptions.getGroupByPos(); String readNameParts[], movieName, zmw; - switch (groupBy) { - case CLUSTER: - return al.getClusterName(); - case STRAND: - return al.isNegativeStrand() ? "-" : "+"; - case SAMPLE: - return al.getSample(); - case LIBRARY: - return al.getLibrary(); - case READ_GROUP: - return al.getReadGroup(); - case LINKED: - return (al instanceof LinkedAlignment) ? "Linked" : ""; - case PHASE: - return al.getAttribute("HP"); - case TAG: + return switch (groupBy) { + case CLUSTER -> al.getClusterName(); + case STRAND -> al.isNegativeStrand() ? "-" : "+"; + case SAMPLE -> al.getSample(); + case LIBRARY -> al.getLibrary(); + case READ_GROUP -> al.getReadGroup(); + case LINKED -> (al instanceof LinkedAlignment) ? "Linked" : ""; + case PHASE -> al.getAttribute("HP"); + case TAG -> { Object tagValue = tag == null ? null : al.getAttribute(tag); if (tagValue == null) { - return null; + yield null; } else if (tagValue instanceof Integer || tagValue instanceof Float || tagValue instanceof Double) { - return tagValue; + yield tagValue; } else { - return tagValue.toString(); + yield tagValue.toString(); } - case FIRST_OF_PAIR_STRAND: + } + case FIRST_OF_PAIR_STRAND -> { Strand strand = al.getFirstOfPairStrand(); - String strandString = strand == Strand.NONE ? null : strand.toString(); - return strandString; - case READ_ORDER: + yield strand == Strand.NONE ? null : strand.toString(); + } + case READ_ORDER -> { if (al.isPaired() && al.isFirstOfPair()) { - return "FIRST"; + yield "FIRST"; } else if (al.isPaired() && al.isSecondOfPair()) { - return "SECOND"; + yield "SECOND"; } else { - return ""; + yield ""; } - case PAIR_ORIENTATION: + } + case PAIR_ORIENTATION -> { PEStats peStats = AlignmentRenderer.getPEStats(al, renderOptions); AlignmentTrack.OrientationType type = AlignmentRenderer.getOrientationType(al, peStats); if (type == null) { - return AlignmentTrack.OrientationType.UNKNOWN.name(); + yield AlignmentTrack.OrientationType.UNKNOWN.name(); } - return type.name(); - case MATE_CHROMOSOME: + yield type.name(); + } + case MATE_CHROMOSOME -> { ReadMate mate = al.getMate(); if (mate == null) { - return null; + yield null; } if (!mate.isMapped()) { - return "UNMAPPED"; + yield "UNMAPPED"; } else { - return mate.getChr(); + yield mate.getChr(); } - case CHIMERIC: - return al.getAttribute(SAMTag.SA.name()) != null ? "CHIMERIC" : ""; - case SUPPLEMENTARY: - return al.isSupplementary() ? "SUPPLEMENTARY" : ""; - case REFERENCE_CONCORDANCE: - return !al.isProperPair() || - al.getCigarString().toUpperCase().contains("S") || - al.isSupplementary() ? - "DISCORDANT" : ""; - case BASE_AT_POS: + } + case NONE -> null; + case CHIMERIC -> al.getAttribute(SAMTag.SA.name()) != null ? "CHIMERIC" : ""; + case SUPPLEMENTARY -> al.isSupplementary() ? "SUPPLEMENTARY" : ""; + case REFERENCE_CONCORDANCE -> !al.isProperPair() || + al.getCigarString().toUpperCase().contains("S") || + al.isSupplementary() ? + "DISCORDANT" : ""; + case BASE_AT_POS -> { // Use a string prefix to enforce grouping rules: // 1: alignments with a base at the position // 2: alignments with a gap at the position @@ -513,14 +508,15 @@ private Object getGroupValue(Alignment al, AlignmentTrack.RenderOptions renderOp byte[] baseAtPos = new byte[]{al.getBase(pos.getStart())}; if (baseAtPos[0] == 0) { // gap at position - return "2:"; + yield "2:"; } else { // base at position - return "1:" + new String(baseAtPos); + yield "1:" + new String(baseAtPos); } } else { // does not overlap position - return "3:"; + yield "3:"; } - case INSERTION_AT_POS: + } + case INSERTION_AT_POS -> { // Use a string prefix to enforce grouping rules: // 1: alignments with a base at the position // 2: alignments with a gap at the position @@ -538,31 +534,32 @@ private Object getGroupValue(Alignment al, AlignmentTrack.RenderOptions renderOp if (rightInsertion != null) { insertionBaseCount += rightInsertion.getLength(); } - return insertionBaseCount; + yield insertionBaseCount; } else { - return 0; + yield 0; } - - case MOVIE: // group PacBio reads by movie + } + case MOVIE -> { readNameParts = al.getReadName().split("/"); if (readNameParts.length < 3) { - return ""; + yield ""; } movieName = readNameParts[0]; - return movieName; - case ZMW: // group PacBio reads by ZMW + yield movieName; // group PacBio reads by movie + } + case ZMW -> { readNameParts = al.getReadName().split("/"); if (readNameParts.length < 3) { - return ""; + yield ""; } movieName = readNameParts[0]; zmw = readNameParts[1]; - return movieName + "/" + zmw; - case MAPPING_QUALITY: - return al.getMappingQuality(); - } - return null; + yield movieName + "/" + zmw; // group PacBio reads by ZMW + } + case MAPPING_QUALITY -> al.getMappingQuality(); + case DUPLICATE -> al.isDuplicate() ? "duplicate" : "non-duplicate"; + }; } interface BucketCollection { diff --git a/src/main/java/org/broad/igv/sam/AlignmentRenderer.java b/src/main/java/org/broad/igv/sam/AlignmentRenderer.java index b93184bd8b..12ed964701 100644 --- a/src/main/java/org/broad/igv/sam/AlignmentRenderer.java +++ b/src/main/java/org/broad/igv/sam/AlignmentRenderer.java @@ -35,7 +35,6 @@ import org.broad.igv.prefs.IGVPreferences; import org.broad.igv.prefs.PreferencesManager; import org.broad.igv.renderer.GraphicUtils; -import org.broad.igv.renderer.SequenceRenderer; import org.broad.igv.sam.AlignmentTrack.ColorOption; import org.broad.igv.sam.BisulfiteBaseInfo.DisplayStatus; import org.broad.igv.sam.mods.BaseModificationRenderer; @@ -55,6 +54,7 @@ import java.awt.*; import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -118,6 +118,7 @@ public class AlignmentRenderer { private static Map f2f1OrientationTypes; private static Map rfOrientationTypes; private static Map typeToColorMap; + private static final Map duplicateTextures = new HashMap<>(); public static final HSLColorTable tenXColorTable1 = new HSLColorTable(30); public static final HSLColorTable tenXColorTable2 = new HSLColorTable(270); @@ -325,11 +326,11 @@ public void renderAlignments(List alignments, int lastPixelDrawn = -1; for (Alignment alignment : alignments) { - // Compute the start and dend of the alignment in pixels + // Compute the start and end of the alignment in pixels double pixelStart = ((alignment.getStart() - origin) / locScale); double pixelEnd = ((alignment.getEnd() - origin) / locScale); - // If any any part of the feature fits in the track rectangle draw it + // If any part of the feature fits in the track rectangle draw it if (pixelEnd < rowRect.x || pixelStart > rowRect.getMaxX()) { continue; } @@ -757,7 +758,18 @@ private void drawAlignment( else if (block.getCigarOperator() == 'X') g = context.getGraphics2D("MISMATCH"); } - g.fill(blockShape); + if(renderOptions.getDuplicatesOption() == AlignmentTrack.DuplicatesOption.TEXTURE && alignment.isDuplicate()) { + final Graphics2D tg = (Graphics2D) g.create(); + + final TexturePaint tp = getDuplicateTexture(tg.getColor()); + // Add the texture paint to the graphics context. + tg.setPaint(tp); + tg.fill(blockShape); + tg.dispose(); + } else { + g.fill(blockShape); + } + if (outlineGraphics != null) { outlineGraphics.draw(blockShape); } @@ -944,6 +956,35 @@ private void drawAlignment( } + /** + * get a texture to apply to duplicate reads, caches the created textures according to their color + * @param baseColor the color to render the read + * @return a texture matching the base color with shading + */ + private TexturePaint getDuplicateTexture(Color baseColor) { + return duplicateTextures.computeIfAbsent(baseColor, color -> { + BufferedImage texture = new BufferedImage(5, 5, BufferedImage.TYPE_INT_RGB); + Graphics2D big = texture.createGraphics(); + //Render into the BufferedImage graphics to create the texture + big.setColor(baseColor); + big.fillRect(0, 0, 5, 5); + + final Color darker = baseColor.darker(); + big.setColor(darker); + // hash pattern, probably better to save it as a bitmap somewhere + big.drawLine(0, 2, 0, 3); + big.drawLine(1, 3, 1, 4); + big.drawLine(2, 4, 2, 4); + big.drawLine(2, 0, 2, 0); + big.drawLine(3, 0, 3, 1); + big.drawLine(4, 1, 4, 2); + big.dispose(); + // Create a texture paint from the buffered image + Rectangle r = new Rectangle(0, 0, 5, 5); + return new TexturePaint(texture, r); + }); + } + private static void drawClippedEnds(final Graphics2D g, final int[] xPoly, final int[] yPoly, final boolean drawLeftClip, final boolean drawRightClip, final SupplementaryAlignment.SupplementaryNeighbors sri) { diff --git a/src/main/java/org/broad/igv/sam/AlignmentTileLoader.java b/src/main/java/org/broad/igv/sam/AlignmentTileLoader.java index cde1606679..2ea3f2a284 100644 --- a/src/main/java/org/broad/igv/sam/AlignmentTileLoader.java +++ b/src/main/java/org/broad/igv/sam/AlignmentTileLoader.java @@ -26,7 +26,6 @@ package org.broad.igv.sam; import htsjdk.samtools.SAMFileHeader; -import htsjdk.samtools.SAMTag; import htsjdk.samtools.util.CloseableIterator; import org.broad.igv.event.IGVEvent; import org.broad.igv.logging.*; @@ -43,7 +42,6 @@ import org.broad.igv.util.ObjectCache; import org.broad.igv.util.RuntimeUtils; -import javax.swing.*; import java.io.IOException; import java.lang.ref.WeakReference; import java.text.DecimalFormat; @@ -134,7 +132,10 @@ AlignmentTile loadTile(String chr, boolean filterSecondaryAlignments = prefMgr.getAsBoolean(SAM_FILTER_SECONDARY_ALIGNMENTS); boolean filterSupplementaryAlignments = prefMgr.getAsBoolean(SAM_FILTER_SUPPLEMENTARY_ALIGNMENTS); ReadGroupFilter filter = ReadGroupFilter.getFilter(); - boolean filterDuplicates = prefMgr.getAsBoolean(SAM_FILTER_DUPLICATES); + boolean filterDuplicates = renderOptions != null + ? renderOptions.getDuplicatesOption() == AlignmentTrack.DuplicatesOption.FILTER + : prefMgr.getAsBoolean(SAM_FILTER_DUPLICATES); + int qualityThreshold = prefMgr.getAsInt(SAM_QUALITY_THRESHOLD); int alignmentScoreTheshold = prefMgr.getAsInt(SAM_ALIGNMENT_SCORE_THRESHOLD); diff --git a/src/main/java/org/broad/igv/sam/AlignmentTrack.java b/src/main/java/org/broad/igv/sam/AlignmentTrack.java index 1fbb85d916..feaccdef42 100644 --- a/src/main/java/org/broad/igv/sam/AlignmentTrack.java +++ b/src/main/java/org/broad/igv/sam/AlignmentTrack.java @@ -135,6 +135,19 @@ public boolean isSMRTKinetics() { } } + public enum DuplicatesOption { + FILTER("filter duplicates", true), + SHOW("show duplicates", false), + TEXTURE("texture duplicates", false); + + public final String label; + public final boolean filtered; + + DuplicatesOption(String label, Boolean filtered) { + this.label = label; + this.filtered = filtered; + } + } public enum ShadeAlignmentsOption { NONE("none"), @@ -169,7 +182,8 @@ public enum GroupOption { LINKED("linked"), PHASE("phase"), REFERENCE_CONCORDANCE("reference concordance"), - MAPPING_QUALITY("mapping quality"); + MAPPING_QUALITY("mapping quality"), + DUPLICATE("duplicate flag"); public final String label; public final boolean reverse; @@ -1221,6 +1235,7 @@ public static class RenderOptions implements Cloneable, Persistable { private SortOption sortOption; private GroupOption groupByOption; private ShadeAlignmentsOption shadeAlignmentsOption; + private DuplicatesOption duplicatesOption; private Integer mappingQualityLow; private Integer mappingQualityHigh; private boolean viewPairs = false; @@ -1472,6 +1487,22 @@ public ShadeAlignmentsOption getShadeAlignmentsOption() { } } + public DuplicatesOption getDuplicatesOption() { + final IGVPreferences prefs = getPreferences(); + if (duplicatesOption != null) { + return duplicatesOption; + } else { + duplicatesOption = prefs.getAsBoolean(SAM_FILTER_DUPLICATES) + ? DuplicatesOption.FILTER + : DuplicatesOption.SHOW; + } + return duplicatesOption; + } + + public void setDuplicatesOption(final DuplicatesOption duplicatesOption) { + this.duplicatesOption = duplicatesOption; + } + public int getMappingQualityLow() { return mappingQualityLow == null ? getPreferences().getAsInt(SAM_SHADE_QUALITY_LOW) : mappingQualityLow; } @@ -1595,6 +1626,9 @@ public void marshalXML(Document document, Element element) { if (shadeAlignmentsOption != null) { element.setAttribute("shadeAlignmentsByOption", shadeAlignmentsOption.toString()); } + if (duplicatesOption != null) { + element.setAttribute("duplicatesOption", duplicatesOption.toString()); + } if (mappingQualityLow != null) { element.setAttribute("mappingQualityLow", mappingQualityLow.toString()); } @@ -1724,6 +1758,9 @@ public void unmarshalXML(Element element, Integer version) { if (element.hasAttribute("shadeAlignmentsByOption")) { shadeAlignmentsOption = ShadeAlignmentsOption.valueOf(element.getAttribute("shadeAlignmentsByOption")); } + if (element.hasAttribute("duplicatesOption")) { + duplicatesOption = CollUtils.valueOf(DuplicatesOption.class, element.getAttribute("duplicatesOption"), null); + } if (element.hasAttribute("mappingQualityLow")) { mappingQualityLow = Integer.parseInt(element.getAttribute("mappingQualityLow")); } @@ -1798,6 +1835,7 @@ public void unmarshalXML(Element element, Integer version) { minJunctionCoverage = Integer.parseInt(element.getAttribute("minJunctionCoverage")); } } + } diff --git a/src/main/java/org/broad/igv/sam/AlignmentTrackMenu.java b/src/main/java/org/broad/igv/sam/AlignmentTrackMenu.java index 2bfa447000..d767857ae7 100644 --- a/src/main/java/org/broad/igv/sam/AlignmentTrackMenu.java +++ b/src/main/java/org/broad/igv/sam/AlignmentTrackMenu.java @@ -2,6 +2,8 @@ import htsjdk.samtools.SAMTag; import org.broad.igv.Globals; +import org.broad.igv.event.AlignmentTrackEvent; +import org.broad.igv.event.IGVEventBus; import org.broad.igv.feature.Range; import org.broad.igv.feature.Strand; import org.broad.igv.jbrowse.CircularViewUtilities; @@ -112,6 +114,8 @@ class AlignmentTrackMenu extends IGVPopupMenu { misMatchesItem.addActionListener(new Deselector(misMatchesItem, showAllItem)); showAllItem.addActionListener(new Deselector(showAllItem, misMatchesItem)); + // Duplicates + addDuplicatesMenuItem(); // Hide small indels JMenuItem smallIndelsItem = new JCheckBoxMenuItem("Hide small indels"); @@ -192,6 +196,31 @@ class AlignmentTrackMenu extends IGVPopupMenu { addShowItems(); } + private void addDuplicatesMenuItem() { + JMenu duplicatesMenu = new JMenu("Duplicates"); + for (AlignmentTrack.DuplicatesOption option : AlignmentTrack.DuplicatesOption.values()) { + JRadioButtonMenuItem mi = new JRadioButtonMenuItem(option.label); + final AlignmentTrack.DuplicatesOption previous = renderOptions.getDuplicatesOption(); + mi.setSelected(previous == option); + mi.addActionListener(aEvt -> { + renderOptions.setDuplicatesOption(option); + if(previous != option) { + if (previous.filtered != option.filtered){ + // duplicates are filtered out when loading the read data so a reload has to be performed in this case + IGVEventBus.getInstance().post(new AlignmentTrackEvent(AlignmentTrackEvent.Type.RELOAD)); + } else { + alignmentTrack.repaint(); + } + } else { + alignmentTrack.repaint(); + } + }); + + duplicatesMenu.add(mi); + } + add(duplicatesMenu); + } + private void addShowChimericRegions(final AlignmentTrack alignmentTrack, final TrackClickEvent e, final Alignment clickedAlignment) { JMenuItem item = new JMenuItem("View chimeric alignments in split screen"); @@ -421,15 +450,14 @@ void addGroupMenuItem(final TrackClickEvent te) {//ReferenceFrame frame) { AlignmentTrack.GroupOption.SUPPLEMENTARY, AlignmentTrack.GroupOption.REFERENCE_CONCORDANCE, AlignmentTrack.GroupOption.MOVIE, AlignmentTrack.GroupOption.ZMW, AlignmentTrack.GroupOption.READ_ORDER, AlignmentTrack.GroupOption.LINKED, AlignmentTrack.GroupOption.PHASE, - AlignmentTrack.GroupOption.MAPPING_QUALITY + AlignmentTrack.GroupOption.MAPPING_QUALITY, + AlignmentTrack.GroupOption.DUPLICATE }; for (final AlignmentTrack.GroupOption option : groupOptions) { JCheckBoxMenuItem mi = new JCheckBoxMenuItem(option.label); mi.setSelected(renderOptions.getGroupByOption() == option); - mi.addActionListener(aEvt -> { - groupAlignments(option, null, null); - }); + mi.addActionListener(aEvt -> groupAlignments(option, null, null)); groupMenu.add(mi); group.add(mi); } diff --git a/src/main/resources/org/broad/igv/prefs/preferences.tab b/src/main/resources/org/broad/igv/prefs/preferences.tab index a7d2bb5692..d208a27913 100644 --- a/src/main/resources/org/broad/igv/prefs/preferences.tab +++ b/src/main/resources/org/broad/igv/prefs/preferences.tab @@ -107,7 +107,7 @@ SAM.ALLELE_USE_QUALITY Quality weight allele fraction boolean TRUE SAM.COLOR_BY Color alignments by select NONE|READ_STRAND|FIRST_OF_PAIR_STRAND|PAIR_ORIENTATION|UNEXPECTED_PAIR|INSERT_SIZE|BASE_MODIFICATION|BASE_MODIFICATION_2COLOR|SAMPLE|READ_GROUP|LIBRARY|MOVIE|ZMW|BISULFITE|NOMESEQ|TAG|MAPPED_SIZE|LINK_STRAND|YC_TAG UNEXPECTED_PAIR SAM.COLOR_BY_TAG Color by TAG string -SAM.GROUP_OPTION Group alignments by select NONE|STRAND|SAMPLE|READ_GROUP|LIBRARY|FIRST_OF_PAIR_STRAND|TAG|PAIR_ORIENTATION|MATE_CHROMOSOME|SV_ALIGNMENT|SUPPLEMENTARY|BASE_AT_POS|MOVIE|ZMW|HAPLOTYPE|READ_ORDER|LINKED|PHASE|MAPPING_QUALITY +SAM.GROUP_OPTION Group alignments by select NONE|STRAND|SAMPLE|READ_GROUP|LIBRARY|FIRST_OF_PAIR_STRAND|TAG|PAIR_ORIENTATION|MATE_CHROMOSOME|SV_ALIGNMENT|SUPPLEMENTARY|BASE_AT_POS|MOVIE|ZMW|HAPLOTYPE|READ_ORDER|LINKED|PHASE|MAPPING_QUALITY|DUPLICATE SAM.GROUP_BY_TAG Group by TAG string SAM.GROUP_ALL Syncrohize alignment track grouping boolean FALSE From 0d4375db93a04327b0845a0e15042fa6bd59f3a2 Mon Sep 17 00:00:00 2001 From: Louis Bergelson Date: Sun, 18 Aug 2024 00:52:40 -0400 Subject: [PATCH 028/130] SpliceJunction tracks now honor manual color setttings (#1546) * Fixes https://github.com/igvteam/igv/issues/1504 --- .../igv/renderer/SpliceJunctionRenderer.java | 48 ++++++++++++------- .../broad/igv/sam/SpliceJunctionTrack.java | 2 +- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/broad/igv/renderer/SpliceJunctionRenderer.java b/src/main/java/org/broad/igv/renderer/SpliceJunctionRenderer.java index dec058e58e..5d9cb6daaf 100644 --- a/src/main/java/org/broad/igv/renderer/SpliceJunctionRenderer.java +++ b/src/main/java/org/broad/igv/renderer/SpliceJunctionRenderer.java @@ -35,7 +35,6 @@ import org.broad.igv.feature.SpliceJunctionFeature; import org.broad.igv.feature.Strand; import org.broad.igv.prefs.Constants; - import org.broad.igv.prefs.IGVPreferences; import org.broad.igv.prefs.PreferencesManager; import org.broad.igv.track.FeatureTrack; import org.broad.igv.track.RenderContext; @@ -101,7 +100,7 @@ public void render(List featureList, // If any part of the feature fits in the track rectangle draw it if (junctionFeature.getEnd() > origin && junctionFeature.getStart() < end) { boolean shouldHighlight = junctionFeature.isSameJunction(selectedFeature); - drawFeature(junctionFeature, shouldShowFlankingRegions, shouldHighlight, origin, locScale, trackRectangle, g2D); + drawFeature(junctionFeature, shouldShowFlankingRegions, shouldHighlight, origin, locScale, trackRectangle, track, g2D); } } @@ -126,7 +125,7 @@ public void render(List featureList, * @param g2D */ protected void drawFeature(SpliceJunctionFeature junctionFeature, boolean shouldShowFlankingRegions, - boolean highlight, double origin, double locScale, Rectangle trackRectangle, Graphics2D g2D) { + boolean highlight, double origin, double locScale, Rectangle trackRectangle, Track track, Graphics2D g2D) { int flankingStart = junctionFeature.getStart(); int flankingEnd = junctionFeature.getEnd(); @@ -150,20 +149,25 @@ protected void drawFeature(SpliceJunctionFeature junctionFeature, boolean should if (strand != null && strand.equals(Strand.NEGATIVE)) isPositiveStrand = false; - //If the feature color is specified, use it, except that we set our own alpha depending on whether - //the feature is highlighted. Otherwise default based on strand and highlight. - Color color; - if (featureColor != null) { - int r = featureColor.getRed(); - int g = featureColor.getGreen(); - int b = featureColor.getBlue(); - int alpha = highlight ? 255 : 140; - color = new Color(r, g, b, alpha); - } else { - if (isPositiveStrand) + /* + Choose the feature color: + 1. Check if the user specified a positive / negative track color + 2. Check if the feature has its own color + 3. Use the default + We modify the alpha value of the chosen color to indicate if it is highlighted + */ + final Color color; + final Color trackColor = isPositiveStrand ? track.getExplicitColor() : track.getExplicitAltColor(); + if( trackColor != null ) { + color = adjustAlpha(trackColor, highlight); + } else if (featureColor != null) { + color = adjustAlpha(featureColor, highlight); + } else { + if (isPositiveStrand) { color = highlight ? ARC_COLOR_HIGHLIGHT_POS : ARC_COLOR_POS; - else - color = highlight ? ARC_COLOR_HIGHLIGHT_NEG : ARC_COLOR_NEG; + } else { + color = highlight ? ARC_COLOR_HIGHLIGHT_NEG : ARC_COLOR_NEG; + } } g2D.setColor(color); @@ -247,6 +251,18 @@ protected void drawFeature(SpliceJunctionFeature junctionFeature, boolean should } + /** + * @return a variant of the input color with a different alpha depending on if it is a highlight color or not + */ + private static Color adjustAlpha(final Color featureColor, final boolean highlight) { + Color color; + int r = featureColor.getRed(); + int g = featureColor.getGreen(); + int b = featureColor.getBlue(); + int alpha = highlight ? 255 : 140; + color = new Color(r, g, b, alpha); + return color; + } /** diff --git a/src/main/java/org/broad/igv/sam/SpliceJunctionTrack.java b/src/main/java/org/broad/igv/sam/SpliceJunctionTrack.java index a65989ff65..2e3d5af972 100644 --- a/src/main/java/org/broad/igv/sam/SpliceJunctionTrack.java +++ b/src/main/java/org/broad/igv/sam/SpliceJunctionTrack.java @@ -94,7 +94,7 @@ public SpliceJunctionTrack(ResourceLocator locator, String name, this.dataManager = dataManager; this.dataManager.subscribe(this); this.alignmentTrack = alignmentTrack; - this.strandOption = ignoreStrand; + SpliceJunctionTrack.strandOption = ignoreStrand; } public SpliceJunctionTrack() { From 3f4d87bd2bac9a817bbf53dc422bc3e4b34269bb Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Mon, 19 Aug 2024 17:42:13 -0700 Subject: [PATCH 029/130] Alignment track - don't repack on drag unless in FULL mode --- src/main/java/org/broad/igv/sam/AlignmentTrack.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/broad/igv/sam/AlignmentTrack.java b/src/main/java/org/broad/igv/sam/AlignmentTrack.java index feaccdef42..2443e2f4dd 100644 --- a/src/main/java/org/broad/igv/sam/AlignmentTrack.java +++ b/src/main/java/org/broad/igv/sam/AlignmentTrack.java @@ -414,7 +414,7 @@ public void receiveEvent(IGVEvent event) { }); } } else if (event instanceof ViewChange viewChange) { - if(viewChange.type == ViewChange.Type.LocusChange && !viewChange.panning) { + if(viewChange.type == ViewChange.Type.LocusChange && !viewChange.panning && getDisplayMode() == DisplayMode.FULL) { packAlignments(); } } From 4a739bff7fd4e730547da3c18d199ca62c3ef0ec Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Mon, 19 Aug 2024 19:05:50 -0700 Subject: [PATCH 030/130] Unit test maintenance --- .../broad/igv/feature/genome/GenomeTest.java | 39 ------------------- .../FastaBlockCompressedSequenceTest.java | 10 ++--- .../genome/load/JsonGenomeLoaderTest.java | 24 ------------ .../igv/sam/cram/IGVReferenceSourceTest.java | 4 +- .../org/broad/igv/util/HttpUtilsTest.java | 1 + .../stream/IGVSeekableBufferedStreamTest.java | 4 +- 6 files changed, 10 insertions(+), 72 deletions(-) diff --git a/src/test/java/org/broad/igv/feature/genome/GenomeTest.java b/src/test/java/org/broad/igv/feature/genome/GenomeTest.java index a3e22ab25c..9ff8839e81 100644 --- a/src/test/java/org/broad/igv/feature/genome/GenomeTest.java +++ b/src/test/java/org/broad/igv/feature/genome/GenomeTest.java @@ -53,45 +53,6 @@ public class GenomeTest extends AbstractHeadlessTest { @Rule public TestRule testTimeout = new Timeout((int) 60e3); - /** - * Test some aliases, both manually entered and automatic. - * - * @throws Exception - */ - @Test - public void testAlias_01() throws Exception { - String genomeURL = "https://s3.amazonaws.com/igv.broadinstitute.org/genomes/hg19.genome"; - Genome genome = loadGenomeAssumeSuccess(genomeURL); - - assertEquals("chrUn_gl000229", genome.getCanonicalChrName("GL000229.1")); - assertEquals("chr14", genome.getCanonicalChrName("14")); - } - - @Test - public void testAlias_02() throws Exception { - // NCBI genome, test an auto-generated alias - String genomeURL = "https://igvdata.broadinstitute.org/genomes/NC_000964.genome"; - Genome genome = loadGenomeAssumeSuccess(genomeURL); - assertEquals("gi|255767013|ref|NC_000964.3|", genome.getCanonicalChrName("NC_000964.3")); - } - - /** - * Loads a genome - * - * @param genomeURL - * @return - */ - private Genome loadGenomeAssumeSuccess(String genomeURL) { - Genome genome = null; - try { - genome = GenomeManager.getInstance().loadGenome(genomeURL); - } catch (Exception e) { - e.printStackTrace(); - } - Assume.assumeNotNull(genome); - return genome; - } - @Test public void testGetNCBIName() throws Exception { String ncbiID = "gi|125745044|ref|NC_002229.3|"; diff --git a/src/test/java/org/broad/igv/feature/genome/fasta/FastaBlockCompressedSequenceTest.java b/src/test/java/org/broad/igv/feature/genome/fasta/FastaBlockCompressedSequenceTest.java index af1cfb283d..877389c035 100644 --- a/src/test/java/org/broad/igv/feature/genome/fasta/FastaBlockCompressedSequenceTest.java +++ b/src/test/java/org/broad/igv/feature/genome/fasta/FastaBlockCompressedSequenceTest.java @@ -15,7 +15,7 @@ public class FastaBlockCompressedSequenceTest { @Test public void findBlockContaining() throws Exception { - String fasta = "https://s3.amazonaws.com/igv.broadinstitute.org/genomes/seq/hg38/hg38.fa.gz"; + String fasta = "https://igv-genepattern-org.s3.amazonaws.com/genomes/seq/hg38/hg38.fa.gz"; FastaBlockCompressedSequence seq = new FastaBlockCompressedSequence(fasta); @@ -28,18 +28,18 @@ public void findBlockContaining() throws Exception { @Test public void compareSequences() throws Exception { - String sequencePath = "https://s3.amazonaws.com/igv.broadinstitute.org/genomes/seq/hg38/hg38.fa"; - String compressedSequencePath = "https://s3.amazonaws.com/igv.broadinstitute.org/genomes/seq/hg38/hg38.fa.gz"; + String sequencePath = "https://igv-genepattern-org.s3.amazonaws.com/genomes/seq/hg38/hg38.fa"; + String compressedSequencePath = "https://igv-genepattern-org.s3.amazonaws.com/genomes/seq/hg38/hg38.fa.gz"; Sequence fastaSequence = new FastaIndexedSequence(sequencePath); Sequence bgSequence = new FastaBlockCompressedSequence(compressedSequencePath); - int len1 = fastaSequence.getChromosomeLength("chr12"); int len2 = bgSequence.getChromosomeLength("chr12"); + int len1 = fastaSequence.getChromosomeLength("chr12"); assertEquals(len1, len2); + byte[] seq2 = bgSequence.getSequence("chr12", 50000, 51000); byte[] seq1 = fastaSequence.getSequence("chr12", 50000, 51000); - byte[] seq2 = fastaSequence.getSequence("chr12", 50000, 51000); for (int i = 0; i < seq1.length; i++) { byte b1 = seq1[i]; diff --git a/src/test/java/org/broad/igv/feature/genome/load/JsonGenomeLoaderTest.java b/src/test/java/org/broad/igv/feature/genome/load/JsonGenomeLoaderTest.java index f42671b2a3..bc40f0f917 100644 --- a/src/test/java/org/broad/igv/feature/genome/load/JsonGenomeLoaderTest.java +++ b/src/test/java/org/broad/igv/feature/genome/load/JsonGenomeLoaderTest.java @@ -36,27 +36,3 @@ public void loadGenome() throws IOException { public void loadDescriptor() { } } - - -/* -{ - "id": "hg38", - "name": "Human (GRCh38/hg38)", - "fastaURL": "https://s3.amazonaws.com/igv.broadinstitute.org/genomes/seq/hg38/hg38.fa", - "indexURL": "https://s3.amazonaws.com/igv.broadinstitute.org/genomes/seq/hg38/hg38.fa.fai", - "cytobandURL": "https://s3.amazonaws.com/igv.org.genomes/hg38/annotations/cytoBandIdeo.txt.gz", - "aliasURL": "https://s3.amazonaws.com/igv.org.genomes/hg38/hg38_alias.tab", - "tracks": [ - { - "name": "Refseq Genes", - "format": "refgene", - "url": "https://s3.amazonaws.com/igv.org.genomes/hg38/ncbiRefSeq.sorted.txt.gz", - "indexURL": "https://s3.amazonaws.com/igv.org.genomes/hg38/ncbiRefSeq.sorted.txt.gz.tbi", - "visibilityWindow": -1, - "removable": false, - "order": 1000000 - } - ], - "chromosomeOrder": "chr1, chr2, chr3, chr4, chr5, chr6, chr7, chr8, chr9, chr10, chr11, chr12, chr13, chr14, chr15, chr16, chr17, chr18, chr19, chr20, chr21, chr22, chrX, chrY" -} - */ \ No newline at end of file diff --git a/src/test/java/org/broad/igv/sam/cram/IGVReferenceSourceTest.java b/src/test/java/org/broad/igv/sam/cram/IGVReferenceSourceTest.java index 24b4bd4112..6021445bf9 100644 --- a/src/test/java/org/broad/igv/sam/cram/IGVReferenceSourceTest.java +++ b/src/test/java/org/broad/igv/sam/cram/IGVReferenceSourceTest.java @@ -43,8 +43,8 @@ public class IGVReferenceSourceTest { - public static final String FASTA_URL = "https://s3.amazonaws.com/igv.broadinstitute.org/genomes/seq/hg38/hg38.fa"; - public static final String COMPRESSED_FASTA_URL = "https://s3.amazonaws.com/igv.broadinstitute.org/genomes/seq/hg38/hg38.fa.gz"; + public static final String FASTA_URL = "https://igv-genepattern-org.s3.amazonaws.com/genomes/seq/hg38/hg38.fa"; + public static final String COMPRESSED_FASTA_URL = "https://igv-genepattern-org.s3.amazonaws.com/genomes/seq/hg38/hg38.fa.gz"; public static final String EXPECTED_REFERENCE_BASES = "AAACCCAGGGCAAAGAATCTGGCCCTA"; //bases at 22:27198875-271988902 @Before diff --git a/src/test/java/org/broad/igv/util/HttpUtilsTest.java b/src/test/java/org/broad/igv/util/HttpUtilsTest.java index 433104bee0..85c8165785 100644 --- a/src/test/java/org/broad/igv/util/HttpUtilsTest.java +++ b/src/test/java/org/broad/igv/util/HttpUtilsTest.java @@ -66,6 +66,7 @@ public void testGetContentLength() throws IOException { HttpURLConnection conn = null; try { conn = (HttpURLConnection) (HttpUtils.createURL(broadURLString)).openConnection(); + conn.setRequestProperty("User-Agent", "IGV"); String contentLength = conn.getHeaderField("Content-length"); assertEquals("52330665", contentLength); diff --git a/src/test/java/org/broad/igv/util/stream/IGVSeekableBufferedStreamTest.java b/src/test/java/org/broad/igv/util/stream/IGVSeekableBufferedStreamTest.java index 1568e410ce..5f8f23ac65 100644 --- a/src/test/java/org/broad/igv/util/stream/IGVSeekableBufferedStreamTest.java +++ b/src/test/java/org/broad/igv/util/stream/IGVSeekableBufferedStreamTest.java @@ -261,7 +261,7 @@ public void testRandomRead() throws IOException { assertEquals(length, bytesRead); byte[] buffer2 = new byte[length]; - SeekableStream bufferedStream = new IGVSeekableBufferedStream(new SeekableHTTPStream(HttpUtils.createURL(BAM_URL_STRING))); + SeekableStream bufferedStream = new IGVSeekableBufferedStream(new IGVSeekableHTTPStream(HttpUtils.createURL(BAM_URL_STRING))); bufferedStream.seek(startPosition); bytesRead = bufferedStream.read(buffer2, 0, length); assertEquals(length, bytesRead); @@ -286,7 +286,7 @@ public void testEOF() throws IOException { byte[] buffer = new byte[length]; - SeekableStream bufferedStream = new IGVSeekableBufferedStream(new SeekableHTTPStream(HttpUtils.createURL(BAM_URL_STRING))); + SeekableStream bufferedStream = new IGVSeekableBufferedStream(new IGVSeekableHTTPStream(HttpUtils.createURL(BAM_URL_STRING))); bufferedStream.seek(startPosition); int bytesRead = bufferedStream.read(buffer, 0, length); assertEquals(remainder, bytesRead); From 916c1739db3fa90c96cc3c813524d8254ec309e2 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Tue, 20 Aug 2024 13:21:24 -0700 Subject: [PATCH 031/130] Simplify genome selection dialog * Single select * Remove "download sequence" option --- .../igv/ui/commandbar/GenomeComboBox.java | 41 ++++------- .../ui/commandbar/GenomeSelectionDialog.java | 73 +++++-------------- 2 files changed, 32 insertions(+), 82 deletions(-) diff --git a/src/main/java/org/broad/igv/ui/commandbar/GenomeComboBox.java b/src/main/java/org/broad/igv/ui/commandbar/GenomeComboBox.java index 96302500c3..9bb1fedafe 100644 --- a/src/main/java/org/broad/igv/ui/commandbar/GenomeComboBox.java +++ b/src/main/java/org/broad/igv/ui/commandbar/GenomeComboBox.java @@ -10,7 +10,6 @@ import org.broad.igv.ui.IGV; import org.broad.igv.ui.UIConstants; import org.broad.igv.ui.util.MessageUtils; -import org.broad.igv.ui.util.ProgressBar; import org.broad.igv.ui.util.UIUtilities; import org.broad.igv.util.LongRunningTask; @@ -223,46 +222,36 @@ public static void loadGenomeFromServer() { Collection inputListItems = GenomeListManager.getInstance().getServerGenomeList(); if (inputListItems == null) { - //Could not reach genome server. Not necessary to display a message, getServerGenomeArchiveList does it already return; } GenomeSelectionDialog dialog = new GenomeSelectionDialog(IGV.getInstance().getMainFrame(), inputListItems); UIUtilities.invokeAndWaitOnEventThread(() -> dialog.setVisible(true)); if (dialog.isCanceled()) { - // Clear the "More..." selection in pulldown IGVEventBus.getInstance().post(new GenomeResetEvent()); } else { - List selectedValueList = dialog.getSelectedValues(); - GenomeListItem firstItem = null; - for (GenomeListItem selectedValue : selectedValueList) { + GenomeListItem selectedValue = dialog.getSelectedValue(); if (selectedValue != null) { - boolean success = GenomeManager.getInstance().downloadGenome(selectedValue, dialog.downloadSequence()); - if (success) { + + try { + GenomeManager.getInstance().loadGenome(selectedValue.getPath()); + GenomeListManager.getInstance().addServerGenomeItem(selectedValue); - firstItem = selectedValue; - } - } - } - if (firstItem != null) { - try { - GenomeManager.getInstance().loadGenome(firstItem.getPath()); - // If the user has previously defined this genome, remove it. - GenomeListManager.getInstance().removeUserDefinedGenome(firstItem.getId()); + GenomeListManager.getInstance().removeUserDefinedGenome(selectedValue.getId()); - // If this is a .json genome, attempt to remove existing .genome files - if(firstItem.getPath().endsWith(".json")) { - removeDotGenomeFile(firstItem.getId()); - } + // If this is a .json genome, attempt to remove existing .genome files + if(selectedValue.getPath().endsWith(".json")) { + removeDotGenomeFile(selectedValue.getId()); + } + } catch (IOException e) { + GenomeListManager.getInstance().removeGenomeListItem(selectedValue); + MessageUtils.showErrorMessage("Error loading genome " + selectedValue.getDisplayableName(), e); + log.error("Error loading genome " + selectedValue.getDisplayableName(), e); + } - } catch (IOException e) { - GenomeListManager.getInstance().removeGenomeListItem(firstItem); - MessageUtils.showErrorMessage("Error loading genome " + firstItem.getDisplayableName(), e); - log.error("Error loading genome " + firstItem.getDisplayableName(), e); } - } } }; diff --git a/src/main/java/org/broad/igv/ui/commandbar/GenomeSelectionDialog.java b/src/main/java/org/broad/igv/ui/commandbar/GenomeSelectionDialog.java index baa3cc6bf0..4b4d9cbdf0 100644 --- a/src/main/java/org/broad/igv/ui/commandbar/GenomeSelectionDialog.java +++ b/src/main/java/org/broad/igv/ui/commandbar/GenomeSelectionDialog.java @@ -47,23 +47,21 @@ * Dialog box for selecting genomes. User can choose from a list, * which is filtered according to text box input */ -public class GenomeSelectionDialog extends org.broad.igv.ui.IGVDialog { +public class GenomeSelectionDialog extends org.broad.igv.ui.IGVDialog { private JPanel dialogPane; private JPanel contentPanel; - private JTextArea textArea1; private JPanel filterPanel; private JLabel label1; private JTextField genomeFilter; private JScrollPane scrollPane1; private JList genomeList; - private JCheckBox downloadSequenceCB; private JPanel buttonBar; private JButton okButton; private JButton cancelButton; private boolean isCanceled = true; - private List selectedValues = null; + private GenomeListItem selectedValue = null; private List allListItems; private DefaultListModel genomeListModel; @@ -74,7 +72,6 @@ public GenomeSelectionDialog(java.awt.Frame parent, Collection i super(parent); initComponents(); initData(inputListItems); - downloadSequenceCB.setVisible(true); } private void initData(Collection inputListItems) { @@ -84,18 +81,12 @@ private void initData(Collection inputListItems) { } /** - * Filter the list of displayed genomes so we only show this - * with the text the user entered. + * Filter the list of displayed genomes with the text the user entered. */ private void rebuildGenomeList(String filterText) { if (genomeListModel == null) { genomeListModel = new DefaultListModel(); - UIUtilities.invokeOnEventThread(new Runnable() { - @Override - public void run() { - genomeList.setModel(genomeListModel); - } - }); + UIUtilities.invokeOnEventThread(() -> genomeList.setModel(genomeListModel)); } genomeListModel.clear(); filterText = filterText.toLowerCase().trim(); @@ -114,14 +105,8 @@ public void run() { * @param e */ private void genomeListMouseClicked(MouseEvent e) { - switch (e.getClickCount()) { - case 1: - List selValues = genomeList.getSelectedValuesList(); - downloadSequenceCB.setEnabled(selValues != null && selValues.size() == 1); - break; - case 2: - okButtonActionPerformed(null); - break; + if (e.getClickCount() == 2) { + okButtonActionPerformed(null); } } @@ -129,12 +114,8 @@ private void genomeEntryKeyReleased(KeyEvent e) { rebuildGenomeList(genomeFilter.getText()); } - public List getSelectedValues() { - return selectedValues; - } - - public boolean downloadSequence(){ - return !isCanceled() && downloadSequenceCB.isEnabled() && downloadSequenceCB.isSelected(); + public GenomeListItem getSelectedValue() { + return selectedValue; } public boolean isCanceled() { @@ -143,14 +124,14 @@ public boolean isCanceled() { private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cancelButtonActionPerformed isCanceled = true; - selectedValues = null; + selectedValue = null; setVisible(false); dispose(); } private void okButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_okButtonActionPerformed isCanceled = false; - selectedValues = genomeList.getSelectedValuesList(); + selectedValue = genomeList.getSelectedValue(); setVisible(false); dispose(); } @@ -159,13 +140,11 @@ private void okButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRS private void initComponents() { dialogPane = new JPanel(); contentPanel = new JPanel(); - textArea1 = new JTextArea(); filterPanel = new JPanel(); label1 = new JLabel(); genomeFilter = new JTextField(); scrollPane1 = new JScrollPane(); genomeList = new JList(); - downloadSequenceCB = new JCheckBox(); buttonBar = new JPanel(); okButton = new JButton(); cancelButton = new JButton(); @@ -186,25 +165,15 @@ private void initComponents() { { contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS)); - //---- textArea1 ---- - textArea1.setText("Selected genomes will be downloaded and added to the genome dropdown list."); - textArea1.setLineWrap(true); - textArea1.setWrapStyleWord(true); - textArea1.setBackground(UIManager.getColor("Button.background")); - textArea1.setRows(2); - textArea1.setMaximumSize(new Dimension(2147483647, 60)); - textArea1.setRequestFocusEnabled(false); - textArea1.setEditable(false); - contentPanel.add(textArea1); //======== filterPanel ======== { filterPanel.setMaximumSize(new Dimension(2147483647, 28)); filterPanel.setLayout(new GridBagLayout()); - ((GridBagLayout)filterPanel.getLayout()).columnWidths = new int[] {0, 0, 0}; - ((GridBagLayout)filterPanel.getLayout()).rowHeights = new int[] {0, 0}; - ((GridBagLayout)filterPanel.getLayout()).columnWeights = new double[] {1.0, 1.0, 1.0E-4}; - ((GridBagLayout)filterPanel.getLayout()).rowWeights = new double[] {1.0, 1.0E-4}; + ((GridBagLayout) filterPanel.getLayout()).columnWidths = new int[]{0, 0, 0}; + ((GridBagLayout) filterPanel.getLayout()).rowHeights = new int[]{0, 0}; + ((GridBagLayout) filterPanel.getLayout()).columnWeights = new double[]{1.0, 1.0, 1.0E-4}; + ((GridBagLayout) filterPanel.getLayout()).rowWeights = new double[]{1.0, 1.0E-4}; //---- label1 ---- label1.setText("Filter:"); @@ -235,7 +204,7 @@ public void keyReleased(KeyEvent e) { { //---- genomeList ---- - //genomeList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + genomeList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); genomeList.addMouseListener(new IGVMouseInputAdapter() { @Override public void igvMouseClicked(MouseEvent e) { @@ -246,14 +215,6 @@ public void igvMouseClicked(MouseEvent e) { } contentPanel.add(scrollPane1); - //---- downloadSequenceCB ---- - downloadSequenceCB.setText("Download Sequence"); - downloadSequenceCB.setAlignmentX(1.0F); - downloadSequenceCB.setToolTipText("Download the full sequence for this organism. Note that these files can be very large (human is about 3 gigabytes)"); - downloadSequenceCB.setMaximumSize(new Dimension(1000, 23)); - downloadSequenceCB.setPreferredSize(new Dimension(300, 23)); - downloadSequenceCB.setMinimumSize(new Dimension(300, 23)); - contentPanel.add(downloadSequenceCB); } dialogPane.add(contentPanel, BorderLayout.CENTER); @@ -261,8 +222,8 @@ public void igvMouseClicked(MouseEvent e) { { buttonBar.setBorder(new EmptyBorder(12, 0, 0, 0)); buttonBar.setLayout(new GridBagLayout()); - ((GridBagLayout)buttonBar.getLayout()).columnWidths = new int[] {0, 85, 80}; - ((GridBagLayout)buttonBar.getLayout()).columnWeights = new double[] {1.0, 0.0, 0.0}; + ((GridBagLayout) buttonBar.getLayout()).columnWidths = new int[]{0, 85, 80}; + ((GridBagLayout) buttonBar.getLayout()).columnWeights = new double[]{1.0, 0.0, 0.0}; //---- okButton ---- okButton.setText("OK"); From c597d32a5cede14d9376b9ac08317eff07bbeb0c Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Sat, 31 Aug 2024 21:44:40 -0700 Subject: [PATCH 032/130] Display unrecognized modifications in popup text --- src/main/java/org/broad/igv/sam/mods/BaseModificationSet.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/broad/igv/sam/mods/BaseModificationSet.java b/src/main/java/org/broad/igv/sam/mods/BaseModificationSet.java index e53ffe9a2f..6544a2003c 100644 --- a/src/main/java/org/broad/igv/sam/mods/BaseModificationSet.java +++ b/src/main/java/org/broad/igv/sam/mods/BaseModificationSet.java @@ -53,7 +53,7 @@ public boolean containsPosition(Integer pos) { public String valueString(int pos) { int l = (int) (100.0 * Byte.toUnsignedInt(likelihoods.get(pos)) / 255); return "Base modification: " + - ((codeValues.containsKey(modification)) ? codeValues.get(modification) : "Uknown") + " (" + l + "%)"; + ((codeValues.containsKey(modification)) ? codeValues.get(modification) : modification) + " (" + l + "%)"; } static Map codeValues; From 7aca9eab075980dcbe4a4ecf6e4a50f0608e6e05 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Tue, 3 Sep 2024 18:57:18 -0700 Subject: [PATCH 033/130] Introduce hidden preference for default oAuth provisioning url --- .../java/org/broad/igv/oauth/OAuthUtils.java | 16 +++++++--- .../java/org/broad/igv/prefs/Constants.java | 1 + .../java/org/broad/igv/ui/IGVMenuBar.java | 31 +++++++++++++------ .../org/broad/igv/prefs/preferences.tab | 1 + 4 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/broad/igv/oauth/OAuthUtils.java b/src/main/java/org/broad/igv/oauth/OAuthUtils.java index 8e67c11f20..b8af2a17e6 100644 --- a/src/main/java/org/broad/igv/oauth/OAuthUtils.java +++ b/src/main/java/org/broad/igv/oauth/OAuthUtils.java @@ -30,6 +30,7 @@ import com.google.gson.JsonParser; import org.broad.igv.logging.*; import org.broad.igv.DirectoryManager; +import org.broad.igv.prefs.Constants; import org.broad.igv.prefs.PreferencesManager; import org.broad.igv.ui.IGVMenuBar; import org.broad.igv.ui.util.MessageUtils; @@ -124,7 +125,7 @@ public OAuthProvider getGoogleProvider() { try { log.info("Loading Google oAuth properties"); googleProvider = loadDefaultOauthProperties(); - if (IGVMenuBar.getInstance() != null) { + if (googleProvider != null && IGVMenuBar.getInstance() != null) { IGVMenuBar.getInstance().enableGoogleMenu(true); } } catch (IOException e) { @@ -142,10 +143,15 @@ public OAuthProvider getGoogleProvider() { * @throws IOException */ private OAuthProvider loadDefaultOauthProperties() throws IOException { - String json = loadAsString(PROPERTIES_URL); - JsonParser parser = new JsonParser(); - JsonObject obj = parser.parse(json).getAsJsonObject(); - return parseProviderObject(obj); + String propertiesURL = PreferencesManager.getPreferences().get(Constants.PROVISIONING_URL_DEFAULT); + if(propertiesURL != null && propertiesURL.length() > 0) { + String json = loadAsString(propertiesURL); + JsonParser parser = new JsonParser(); + JsonObject obj = parser.parse(json).getAsJsonObject(); + return parseProviderObject(obj); + } else { + return null; + } } /** diff --git a/src/main/java/org/broad/igv/prefs/Constants.java b/src/main/java/org/broad/igv/prefs/Constants.java index a9c40fcf6e..aa93e9656f 100644 --- a/src/main/java/org/broad/igv/prefs/Constants.java +++ b/src/main/java/org/broad/igv/prefs/Constants.java @@ -292,6 +292,7 @@ private Constants() { // OAuth provisioning public static final String PROVISIONING_URL = "PROVISIONING.URL"; + public static final String PROVISIONING_URL_DEFAULT = "PROVISIONING_URL_DEFAULT"; // JBrowse circular view integration public static final String CIRC_VIEW_ENABLED = "CIRC_VIEW_ENABLED"; diff --git a/src/main/java/org/broad/igv/ui/IGVMenuBar.java b/src/main/java/org/broad/igv/ui/IGVMenuBar.java index 0d1d91da48..8f72951c98 100644 --- a/src/main/java/org/broad/igv/ui/IGVMenuBar.java +++ b/src/main/java/org/broad/igv/ui/IGVMenuBar.java @@ -187,9 +187,11 @@ private List createMenus() { // by loading a protected Google resource try { googleMenu = createGoogleMenu(); - boolean enabled = PreferencesManager.getPreferences().getAsBoolean(ENABLE_GOOGLE_MENU); - enableGoogleMenu(enabled); - menus.add(googleMenu); + if(googleMenu != null) { + boolean enabled = PreferencesManager.getPreferences().getAsBoolean(ENABLE_GOOGLE_MENU); + enableGoogleMenu(enabled); + menus.add(googleMenu); + } } catch (IOException e) { log.error("Error creating google menu: " + e.getMessage()); } @@ -1049,12 +1051,20 @@ public void menuCanceled(MenuEvent e) { private JMenu createGoogleMenu() { + final OAuthProvider googleProvider = OAuthUtils.getInstance().getGoogleProvider(); + if(googleProvider == null) { + log.error("Error creating google oauth provider"); + return null; + } + + googleMenu = new JMenu("Google"); final JMenuItem login = new JMenuItem("Login ... "); + login.addActionListener(e -> { try { - OAuthUtils.getInstance().getGoogleProvider().openAuthorizationPage(); + googleProvider.openAuthorizationPage(); } catch (Exception ex) { MessageUtils.showErrorMessage("Error fetching oAuth tokens. See log for details", ex); log.error("Error fetching oAuth tokens", ex); @@ -1065,7 +1075,7 @@ private JMenu createGoogleMenu() { final JMenuItem logout = new JMenuItem("Logout "); logout.addActionListener(e -> { - OAuthUtils.getInstance().getGoogleProvider().logout(); + googleProvider.logout(); GoogleUtils.setProjectID(null); }); googleMenu.add(logout); @@ -1077,10 +1087,9 @@ private JMenu createGoogleMenu() { googleMenu.addMenuListener(new MenuListener() { @Override public void menuSelected(MenuEvent e) { - OAuthProvider oauth = OAuthUtils.getInstance().getGoogleProvider(); - boolean loggedIn = oauth.isLoggedIn(); - if (loggedIn && oauth.getCurrentUserName() != null) { - login.setText(oauth.getCurrentUserName()); + boolean loggedIn = googleProvider.isLoggedIn(); + if (loggedIn && googleProvider.getCurrentUserName() != null) { + login.setText(googleProvider.getCurrentUserName()); } else { login.setText("Login ..."); } @@ -1112,7 +1121,9 @@ public void menuCanceled(MenuEvent e) { * @throws IOException */ public void enableGoogleMenu(boolean enable) throws IOException { - googleMenu.setVisible(enable); + if(googleMenu != null) { + googleMenu.setVisible(enable); + } } // public void enableRemoveGenomes() { diff --git a/src/main/resources/org/broad/igv/prefs/preferences.tab b/src/main/resources/org/broad/igv/prefs/preferences.tab index d208a27913..e58cd6e1f5 100644 --- a/src/main/resources/org/broad/igv/prefs/preferences.tab +++ b/src/main/resources/org/broad/igv/prefs/preferences.tab @@ -291,6 +291,7 @@ CIRC_VIEW_PORT CircView port integer 60152 #Hidden +PROVISIONING_URL_DEFAULT https://igv.org/services/desktop_google SAM.SHOW_JUNCTION_FLANKINGREGIONS FALSE SAM.JUNCTION_MIN_FLANKING_WIDTH 0 SAM.JUNCTION_MIN_COVERAGE 1 From 15d7569173b90bf0c74076b2f83b44b22286a4cc Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Wed, 4 Sep 2024 15:56:54 -0700 Subject: [PATCH 034/130] Look for "TS" attribute to determine transcript strand. See #1558 --- src/main/java/org/broad/igv/sam/SpliceJunctionHelper.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/broad/igv/sam/SpliceJunctionHelper.java b/src/main/java/org/broad/igv/sam/SpliceJunctionHelper.java index 8782ddabbc..6e306fc7a7 100644 --- a/src/main/java/org/broad/igv/sam/SpliceJunctionHelper.java +++ b/src/main/java/org/broad/igv/sam/SpliceJunctionHelper.java @@ -110,7 +110,11 @@ public void addAlignment(Alignment alignment) { // Determine strand. First check for explicit attribute. boolean isNegativeStrand; - Object strandAttr = alignment.getAttribute("XS"); + Object strandAttr = alignment.getAttribute("TS"); + if(strandAttr == null) { + strandAttr = alignment.getAttribute("XS"); // Older convention + } + if (strandAttr != null) { isNegativeStrand = strandAttr.toString().charAt(0) == '-'; } else { From 1767b02b84612f44b26549c9bc0b1a479c8c9027 Mon Sep 17 00:00:00 2001 From: Louis Bergelson Date: Thu, 5 Sep 2024 18:24:23 -0400 Subject: [PATCH 035/130] Delete unused Apache Tomcat Utils package (#1562) --- .../java/org/apache/tomcat/util/Ascii.java | 214 ------------------ .../java/org/apache/tomcat/util/HttpDate.java | 206 ----------------- 2 files changed, 420 deletions(-) delete mode 100644 src/main/java/org/apache/tomcat/util/Ascii.java delete mode 100644 src/main/java/org/apache/tomcat/util/HttpDate.java diff --git a/src/main/java/org/apache/tomcat/util/Ascii.java b/src/main/java/org/apache/tomcat/util/Ascii.java deleted file mode 100644 index a7be18bbf1..0000000000 --- a/src/main/java/org/apache/tomcat/util/Ascii.java +++ /dev/null @@ -1,214 +0,0 @@ -/* -* -* The Apache Software License, Version 1.1 -* -* Copyright (c) 1999 The Apache Software Foundation. All rights -* reserved. -* -* Redistribution and use in source and binary forms, with or without -* modification, are permitted provided that the following conditions -* are met: -* -* 1. Redistributions of source code must retain the above copyright -* notice, this list of conditions and the following disclaimer. -* -* 2. Redistributions in binary form must reproduce the above copyright -* notice, this list of conditions and the following disclaimer in -* the documentation and/or other materials provided with the -* distribution. -* -* 3. The end-user documentation included with the redistribution, if -* any, must include the following acknowlegement: -* "This product includes software developed by the -* Apache Software Foundation (http://www.apache.org/)." -* Alternately, this acknowlegement may appear in the software itself, -* if and wherever such third-party acknowlegements normally appear. -* -* 4. The names "The Jakarta Project", "Tomcat", and "Apache Software -* Foundation" must not be used to endorse or promote products derived -* from this software without prior written permission. For written -* permission, please contact apache@apache.org. -* -* 5. Products derived from this software may not be called "Apache" -* nor may "Apache" appear in their names without prior written -* permission of the Apache Group. -* -* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED -* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -* DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR -* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT -* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -* SUCH DAMAGE. -* ==================================================================== -* -* This software consists of voluntary contributions made by many -* individuals on behalf of the Apache Software Foundation. For more -* information on the Apache Software Foundation, please see -* . -* -* [Additional notices, if required by prior licensing conditions] -* -*/ - - -package org.apache.tomcat.util; - -/** - * This class implements some basic ASCII character handling functions. - * - * @author dac@eng.sun.com - * @author James Todd [gonzo@eng.sun.com] - */ - -public class Ascii { - /* - * Character translation tables. - */ - - private static final byte[] toUpper = new byte[256]; - private static final byte[] toLower = new byte[256]; - - /* - * Character type tables. - */ - - private static final boolean[] isAlpha = new boolean[256]; - private static final boolean[] isUpper = new boolean[256]; - private static final boolean[] isLower = new boolean[256]; - private static final boolean[] isWhite = new boolean[256]; - private static final boolean[] isDigit = new boolean[256]; - - /* - * Initialize character translation and type tables. - */ - - static { - for (int i = 0; i < 256; i++) { - toUpper[i] = (byte) i; - toLower[i] = (byte) i; - } - - for (int lc = 'a'; lc <= 'z'; lc++) { - int uc = lc + 'A' - 'a'; - - toUpper[lc] = (byte) uc; - toLower[uc] = (byte) lc; - isAlpha[lc] = true; - isAlpha[uc] = true; - isLower[lc] = true; - isUpper[uc] = true; - } - - isWhite[' '] = true; - isWhite['\t'] = true; - isWhite['\r'] = true; - isWhite['\n'] = true; - isWhite['\f'] = true; - isWhite['\b'] = true; - - for (int d = '0'; d <= '9'; d++) { - isDigit[d] = true; - } - } - - /** - * Returns the upper case equivalent of the specified ASCII character. - */ - - public static int toUpper(int c) { - return toUpper[c & 0xff] & 0xff; - } - - /** - * Returns the lower case equivalent of the specified ASCII character. - */ - - public static int toLower(int c) { - return toLower[c & 0xff] & 0xff; - } - - /** - * Returns true if the specified ASCII character is upper or lower case. - */ - - public static boolean isAlpha(int c) { - return isAlpha[c & 0xff]; - } - - /** - * Returns true if the specified ASCII character is upper case. - */ - - public static boolean isUpper(int c) { - return isUpper[c & 0xff]; - } - - /** - * Returns true if the specified ASCII character is lower case. - */ - - public static boolean isLower(int c) { - return isLower[c & 0xff]; - } - - /** - * Returns true if the specified ASCII character is white space. - */ - - public static boolean isWhite(int c) { - return isWhite[c & 0xff]; - } - - /** - * Returns true if the specified ASCII character is a digit. - */ - - public static boolean isDigit(int c) { - return isDigit[c & 0xff]; - } - - /** - * Parses an unsigned integer from the specified subarray of bytes. - * - * @param b the bytes to parse - * @param off the start offset of the bytes - * @param len the length of the bytes - * @throws NumberFormatException if the integer format was invalid - */ - - public static int parseInt(byte[] b, int off, int len) - throws NumberFormatException { - int c; - - if (b == null || len <= 0 || !isDigit(c = b[off++])) { -// StringManager sm = -// StringManager.getManager(Constants.Package); -// String msg = sm.getString("ascii.parseInit.nfe", b); - String msg = "Could not parse byte array as integer: " + new String(b); - throw new NumberFormatException(msg); - } - - int n = c - '0'; - - while (--len > 0) { - if (!isDigit(c = b[off++])) { -// StringManager sm = -// StringManager.getManager(Constants.Package); - // String msg = sm.getString("ascii.parseInit.nfe", b); - String msg = "Could not parse byte array as integer: " + new String(b); - - throw new NumberFormatException(msg); - } - - n = n * 10 + c - '0'; - } - - return n; - } -} diff --git a/src/main/java/org/apache/tomcat/util/HttpDate.java b/src/main/java/org/apache/tomcat/util/HttpDate.java deleted file mode 100644 index 48a614d7d6..0000000000 --- a/src/main/java/org/apache/tomcat/util/HttpDate.java +++ /dev/null @@ -1,206 +0,0 @@ -/* -* -* The Apache Software License, Version 1.1 -* -* Copyright (c) 1999 The Apache Software Foundation. All rights -* reserved. -* -* Redistribution and use in source and binary forms, with or without -* modification, are permitted provided that the following conditions -* are met: -* -* 1. Redistributions of source code must retain the above copyright -* notice, this list of conditions and the following disclaimer. -* -* 2. Redistributions in binary form must reproduce the above copyright -* notice, this list of conditions and the following disclaimer in -* the documentation and/or other materials provided with the -* distribution. -* -* 3. The end-user documentation included with the redistribution, if -* any, must include the following acknowlegement: -* "This product includes software developed by the -* Apache Software Foundation (http://www.apache.org/)." -* Alternately, this acknowlegement may appear in the software itself, -* if and wherever such third-party acknowlegements normally appear. -* -* 4. The names "The Jakarta Project", "Tomcat", and "Apache Software -* Foundation" must not be used to endorse or promote products derived -* from this software without prior written permission. For written -* permission, please contact apache@apache.org. -* -* 5. Products derived from this software may not be called "Apache" -* nor may "Apache" appear in their names without prior written -* permission of the Apache Group. -* -* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED -* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -* DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR -* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT -* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -* SUCH DAMAGE. -* ==================================================================== -* -* This software consists of voluntary contributions made by many -* individuals on behalf of the Apache Software Foundation. For more -* information on the Apache Software Foundation, please see -* . -* -* [Additional notices, if required by prior licensing conditions] -* -*/ - - -package org.apache.tomcat.util; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.*; -import java.text.*; - -/** - * This class can be used to efficiently parse and write an RFC 1123 - * formatted date in an HTTP message header. Also supports reading the - * RFC 1036 format and ANSI C's asctime() format, as suggested by HTTP/1.0 - * and mandated by HTTP/1.1. - * - * @author dac@eng.sun.com - * @author Jason Hunter [jch@eng.sun.com] - * @author James Todd [gonzo@eng.sun.com] - */ - -public class HttpDate extends Ascii { - -// private StringManager sm = -// StringManager.getManager(Constants.Package); - - // ONLY FOR COMPAT -- KILL ASAP -- just make sure that dependant - // classes know what's up. ref. MimeHeaderField - private static final String DATESTR = "Sun, 06 Nov 1994 08:49:37 GMT"; - public static final int DATELEN = DATESTR.length(); - // END COMPAT -- DON'T FORGET TO KILL - - // we force our locale here as all http dates are in english - private final static Locale loc = Locale.US; - - // all http dates are expressed as time at GMT - private final static TimeZone zone = TimeZone.getTimeZone("GMT"); - - // format for RFC 1123 date string -- "Sun, 06 Nov 1994 08:49:37 GMT" - private final static String rfc1123Pattern = - "EEE, dd MMM yyyy HH:mm:ss z"; - - // format for RFC 1036 date string -- "Sunday, 06-Nov-94 08:49:37 GMT" - private final static String rfc1036Pattern = - "EEEEEEEEE, dd-MMM-yy HH:mm:ss z"; - - // format for C asctime() date string -- "Sun Nov 6 08:49:37 1994" - private final static String asctimePattern = - "EEE MMM d HH:mm:ss yyyy"; - - private final static SimpleDateFormat rfc1123Format = - new SimpleDateFormat(rfc1123Pattern, loc); - - private final static SimpleDateFormat rfc1036Format = - new SimpleDateFormat(rfc1036Pattern, loc); - - private final static SimpleDateFormat asctimeFormat = - new SimpleDateFormat(asctimePattern, loc); - - static { - rfc1123Format.setTimeZone(zone); - rfc1036Format.setTimeZone(zone); - asctimeFormat.setTimeZone(zone); - } - - // protected so that oldcookieexpiry in cookieutils can use - // yes, this is sloppy as crap and could stand to be done better. - protected Calendar calendar = new GregorianCalendar(zone, loc); - - public HttpDate() { - calendar.setTime(new Date(System.currentTimeMillis())); - } - - public HttpDate(long ms) { - calendar.setTime(new Date(ms)); - } - - public void setTime() { - calendar.setTime(new Date(System.currentTimeMillis())); - } - - public void setTime(long ms) { - calendar.setTime(new Date(ms)); - } - - public void parse(String dateString) { - try { - Date date = rfc1123Format.parse(dateString); - calendar.setTime(date); - return; - } catch (ParseException e) { - } - - try { - Date date = rfc1036Format.parse(dateString); - calendar.setTime(date); - return; - } catch (ParseException e) { - } - - try { - Date date = asctimeFormat.parse(dateString); - calendar.setTime(date); - return; - } catch (ParseException pe) { - //String msg = sm.getString("httpDate.pe", dateString); - String msg = "Could not parse data: " + dateString; - throw new IllegalArgumentException(msg); - } - } - - public void parse(byte[] b, int off, int len) { - // ok -- so this is pretty stoopid, but the old version of this - // source took this arg set, so we will too for now (backwards compat) - String dateString = new String(b, off, len); - parse(dateString); - } - - public void write(OutputStream out) throws IOException { - String dateString = rfc1123Format.format(calendar.getTime()); - byte[] b = dateString.getBytes(); - out.write(b); - } - - public String toString() { - return rfc1123Format.format(calendar.getTime()); - } - - public long getTime() { - return calendar.getTime().getTime(); - } - - public static long getCurrentTime() { - return System.currentTimeMillis(); - } -// -// // KILL, THIS IS ONLY HERE FOR TEMP COMPAT as MimeHeaderField uses it. -// public int getBytes(byte[] buf, int off, int len) { -// if (len < DATELEN) { -// String msg = sm.getString("httpDate.iae", new Integer(len)); -// -// throw new IllegalArgumentException(msg); -// } -// -// String dateString = rfc1123Format.format(calendar.getTime()); -// byte[] b = dateString.getBytes(); -// System.arraycopy(b, 0, buf, off, DATELEN); -// return DATELEN; -// } -} From 8b364e7320d8c7736707b271fcc4174354ff3a24 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Thu, 5 Sep 2024 14:46:22 -0700 Subject: [PATCH 036/130] Changes to support BB files with alternative layouts, specifically chromTree following the full data section. --- .../org/broad/igv/track/FileFormatUtils.java | 7 +- src/main/java/org/broad/igv/ucsc/BPTree.java | 3 +- .../org/broad/igv/ucsc/bb/BBDataSource.java | 5 +- .../java/org/broad/igv/ucsc/bb/BBFile.java | 61 ++--- .../java/org/broad/igv/ucsc/bb/RPTree.java | 9 +- .../broad/igv/ucsc/twobit/TwoBitSequence.java | 8 +- .../igv/ucsc/twobit/UnsignedByteBuffer.java | 111 ++-------- .../twobit/UnsignedByteBufferDynamic.java | 209 ++++++++++++++++++ .../ucsc/twobit/UnsignedByteBufferImpl.java | 124 +++++++++++ 9 files changed, 399 insertions(+), 138 deletions(-) create mode 100644 src/main/java/org/broad/igv/ucsc/twobit/UnsignedByteBufferDynamic.java create mode 100644 src/main/java/org/broad/igv/ucsc/twobit/UnsignedByteBufferImpl.java diff --git a/src/main/java/org/broad/igv/track/FileFormatUtils.java b/src/main/java/org/broad/igv/track/FileFormatUtils.java index 3dc9eb7c8c..4969624c83 100644 --- a/src/main/java/org/broad/igv/track/FileFormatUtils.java +++ b/src/main/java/org/broad/igv/track/FileFormatUtils.java @@ -1,15 +1,12 @@ package org.broad.igv.track; -import htsjdk.samtools.seekablestream.SeekableBufferedStream; import htsjdk.samtools.seekablestream.SeekableStream; import htsjdk.samtools.util.BlockCompressedInputStream; -import org.broad.igv.exceptions.DataLoadException; import org.broad.igv.ucsc.twobit.UnsignedByteBuffer; +import org.broad.igv.ucsc.twobit.UnsignedByteBufferImpl; import org.broad.igv.util.stream.IGVSeekableStreamFactory; import java.io.*; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.zip.GZIPInputStream; @@ -65,7 +62,7 @@ public static String determineFormat(String path) throws IOException { } // BIGWIG - BIGBED - UnsignedByteBuffer byteBuffer = UnsignedByteBuffer.wrap(bytes); + UnsignedByteBuffer byteBuffer = UnsignedByteBufferImpl.wrap(bytes); long m = byteBuffer.getUInt(); if (m == BIGWIG_MAGIC) { return "bigwig"; diff --git a/src/main/java/org/broad/igv/ucsc/BPTree.java b/src/main/java/org/broad/igv/ucsc/BPTree.java index e37cd34de6..863644d3dd 100644 --- a/src/main/java/org/broad/igv/ucsc/BPTree.java +++ b/src/main/java/org/broad/igv/ucsc/BPTree.java @@ -43,6 +43,7 @@ import org.broad.igv.ucsc.twobit.UnsignedByteBuffer; +import org.broad.igv.ucsc.twobit.UnsignedByteBufferImpl; import java.io.IOException; import java.nio.ByteOrder; @@ -83,7 +84,7 @@ private BPTree(String path, long fileOffset) throws IOException { } UnsignedByteBuffer loadBinaryBuffer(long start, int size) throws IOException { - return UnsignedByteBuffer.loadBinaryBuffer(this.path, this.byteOrder, start, size); + return UnsignedByteBufferImpl.loadBinaryBuffer(this.path, this.byteOrder, start, size); } private void init() throws IOException { diff --git a/src/main/java/org/broad/igv/ucsc/bb/BBDataSource.java b/src/main/java/org/broad/igv/ucsc/bb/BBDataSource.java index fb176717f1..a6f8f0d93b 100644 --- a/src/main/java/org/broad/igv/ucsc/bb/BBDataSource.java +++ b/src/main/java/org/broad/igv/ucsc/bb/BBDataSource.java @@ -25,6 +25,7 @@ package org.broad.igv.ucsc.bb; +import htsjdk.samtools.util.Locatable; import org.broad.igv.Globals; import org.broad.igv.data.AbstractDataSource; import org.broad.igv.data.BasicScore; @@ -260,7 +261,7 @@ private List getWholeGenomeScores() { ArrayList scores = new ArrayList(); wholeGenomeScores.put(windowFunction, scores); - BBZoomHeader lowestResHeader = reader.zoomLevelForScale(scale); + BBZoomHeader lowestResHeader = reader.zoomLevelForScale(scale, 1000); if (lowestResHeader == null) return null; Set wgChrNames = new HashSet<>(genome.getLongChromosomeNames()); @@ -282,7 +283,7 @@ private List getWholeGenomeScores() { scores.add(new BasicScore(genomeStart, genomeEnd, rec.getScore())); } } - scores.sort((o1, o2) -> o1.getStart() - o2.getStart()); + scores.sort(Comparator.comparingInt(Locatable::getStart)); } return wholeGenomeScores.get(windowFunction); diff --git a/src/main/java/org/broad/igv/ucsc/bb/BBFile.java b/src/main/java/org/broad/igv/ucsc/bb/BBFile.java index 0188cb639b..264ab7731e 100644 --- a/src/main/java/org/broad/igv/ucsc/bb/BBFile.java +++ b/src/main/java/org/broad/igv/ucsc/bb/BBFile.java @@ -1,7 +1,6 @@ package org.broad.igv.ucsc.bb; import htsjdk.samtools.seekablestream.SeekableStream; -import htsjdk.tribble.NamedFeature; import org.broad.igv.data.BasicScore; import org.broad.igv.feature.BasicFeature; import org.broad.igv.feature.LocusScore; @@ -13,6 +12,8 @@ import org.broad.igv.ucsc.twobit.UnsignedByteBuffer; import org.broad.igv.ucsc.bb.codecs.BBCodec; import org.broad.igv.ucsc.bb.codecs.BBCodecFactory; +import org.broad.igv.ucsc.twobit.UnsignedByteBufferDynamic; +import org.broad.igv.ucsc.twobit.UnsignedByteBufferImpl; import org.broad.igv.util.CompressionUtils; import org.broad.igv.util.stream.IGVSeekableStreamFactory; @@ -110,7 +111,7 @@ public String getAutoSql() { return autosql; } - public String [] getChromosomeNames() { + public String[] getChromosomeNames() { return chrNames; } @@ -148,7 +149,7 @@ void init() throws IOException { BBHeader readHeader() throws IOException { ByteOrder order = ByteOrder.LITTLE_ENDIAN; - UnsignedByteBuffer buffer = UnsignedByteBuffer.loadBinaryBuffer(this.path, order, 0, BBFILE_HEADER_SIZE); + UnsignedByteBuffer buffer = UnsignedByteBufferImpl.loadBinaryBuffer(this.path, order, 0, BBFILE_HEADER_SIZE); long magic = buffer.getUInt(); if (magic == BIGWIG_MAGIC) { this.type = Type.BIGWIG; @@ -157,7 +158,7 @@ BBHeader readHeader() throws IOException { } else { //Try big endian order order = ByteOrder.BIG_ENDIAN; - buffer = UnsignedByteBuffer.loadBinaryBuffer(this.path, order, 0, BBFILE_HEADER_SIZE); + buffer = UnsignedByteBufferImpl.loadBinaryBuffer(this.path, order, 0, BBFILE_HEADER_SIZE); magic = buffer.getUInt(); if (magic == BIGWIG_MAGIC) { this.type = Type.BIGWIG; @@ -183,9 +184,9 @@ BBHeader readHeader() throws IOException { header.extensionOffset = buffer.getLong(); // Read rest of fields up to full data offset - buffer = UnsignedByteBuffer.loadBinaryBuffer(this.path, order, BBFILE_HEADER_SIZE, (int) (header.fullDataOffset - BBFILE_HEADER_SIZE + 4)); + buffer = UnsignedByteBufferImpl.loadBinaryBuffer(this.path, order, BBFILE_HEADER_SIZE, (int) (header.fullDataOffset - BBFILE_HEADER_SIZE + 4)); - // Zoom headers + // Zoom headers -- immediately follows the common header this.zoomHeaders = new BBZoomHeader[header.nZoomLevels]; for (int i = 0; i < header.nZoomLevels; ++i) { BBZoomHeader zlh = new BBZoomHeader(); @@ -198,29 +199,34 @@ BBHeader readHeader() throws IOException { // Sort in order of decreasing reduction level (increasing resolution Arrays.sort(zoomHeaders, (o1, o2) -> o2.reductionLevel - o1.reductionLevel); - // Autosql + // Autosql -- spec implies this follows the zoom headers final int startOffset = BBFILE_HEADER_SIZE; if (header.autoSqlOffset > 0) { buffer.position((int) (header.autoSqlOffset - startOffset)); this.autosql = buffer.getString(); } - // Total summary -- present in versions >= 2 + // Total summary -- present in versions >= 2. Follows the zoom headers and autosql if (header.version > 1 && header.totalSummaryOffset > 0) { buffer.position((int) (header.totalSummaryOffset - startOffset)); this.totalSummary = BBTotalSummary.parseSummary(buffer); } - // Chromosome tree - // TODO replace with BPTree - buffer.position((int) (header.chromTreeOffset - startOffset)); - this.chromTree = ChromTree.parseTree(buffer, startOffset, this.genome); - this.chrNames = this.chromTree.names(); - //Total data count -- for bigbed this is the number of features, for bigwig it is number of sections buffer.position((int) (header.fullDataOffset - BBFILE_HEADER_SIZE)); header.dataCount = buffer.getInt(); + // Chromosome tree -- this normally preceeds fullDataOffset so will be within the buffer. However, this + // isn't guaranteed, we have to check + int chromtreeBufferPosition = (int) (header.chromTreeOffset - startOffset); + if(chromtreeBufferPosition > 0 && chromtreeBufferPosition < buffer.position() + buffer.remaining()) { + buffer.position((int) (header.chromTreeOffset - startOffset)); + } else { + buffer = UnsignedByteBufferDynamic.loadBinaryBuffer(this.path, order, header.chromTreeOffset, 1000); + } + this.chromTree = ChromTree.parseTree(buffer, startOffset, this.genome); + this.chrNames = this.chromTree.names(); + this.header = header; //extension @@ -230,7 +236,7 @@ BBHeader readHeader() throws IOException { // total summary stats if (header.version > 1) { - buffer = UnsignedByteBuffer.loadBinaryBuffer(this.path, order, header.totalSummaryOffset, 40); + buffer = UnsignedByteBufferImpl.loadBinaryBuffer(this.path, order, header.totalSummaryOffset, 40); this.totalSummary = BBTotalSummary.parseSummary(buffer); } @@ -247,7 +253,7 @@ BBHeader readHeader() throws IOException { void loadExtendedHeader(long offset) throws IOException { - UnsignedByteBuffer binaryParser = UnsignedByteBuffer.loadBinaryBuffer(this.path, byteOrder, offset, BBFILE_EXTENDED_HEADER_HEADER_SIZE); + UnsignedByteBuffer binaryParser = UnsignedByteBufferImpl.loadBinaryBuffer(this.path, byteOrder, offset, BBFILE_EXTENDED_HEADER_HEADER_SIZE); int extensionSize = binaryParser.getUShort(); int extraIndexCount = binaryParser.getUShort(); @@ -255,7 +261,7 @@ void loadExtendedHeader(long offset) throws IOException { if (extraIndexCount == 0) return; int sz = extraIndexCount * (2 + 2 + 8 + 4 + 10 * (2 + 2)); - binaryParser = UnsignedByteBuffer.loadBinaryBuffer(this.path, byteOrder, extraIndexListOffset, sz); + binaryParser = UnsignedByteBufferImpl.loadBinaryBuffer(this.path, byteOrder, extraIndexListOffset, sz); // const type = [] // const fieldCount = [] @@ -303,9 +309,9 @@ List getLeafChunks(String chr1, int bpStart, String chr2, int bpEnd, lon // Load the R Tree and fine leaf items RPTree rpTree = rTreeCache.get(treeOffset); - if(rpTree == null) { - rpTree = RPTree.loadTree(this.path, treeOffset); - rTreeCache.put(treeOffset, rpTree); + if (rpTree == null) { + rpTree = RPTree.loadTree(this.path, treeOffset); + rTreeCache.put(treeOffset, rpTree); } List leafChunks = new ArrayList<>(); @@ -383,16 +389,21 @@ String getChrForId(int chrIdx) { * @param bpPerPixel -- the resolution in bp per pixel. * @return A zoom header, or null if no appropriate zoom data is available for the resolution. */ + BBZoomHeader zoomLevelForScale(double bpPerPixel) { + return zoomLevelForScale(bpPerPixel, 2); + } + + BBZoomHeader zoomLevelForScale(double bpPerPixel, int tolerance) { BBZoomHeader level = null; for (BBZoomHeader zl : this.zoomHeaders) { if (zl.reductionLevel < bpPerPixel) { return zl; } } - // For the highest resolution, allow up to a factor of 2 + // For the lowest resolution, consider a match if within a factor "tolerance" of the requested resolution BBZoomHeader lastLevel = this.zoomHeaders[this.zoomHeaders.length - 1]; - return lastLevel.reductionLevel / 2 < bpPerPixel ? lastLevel : null; + return lastLevel.reductionLevel / tolerance < bpPerPixel ? lastLevel : null; } public boolean isSearchable() { @@ -465,7 +476,7 @@ List decodeFeatures(byte[] buffer, int chrIdx, int start, int end) } - UnsignedByteBuffer bb = UnsignedByteBuffer.wrap(uncompressed, byteOrder); + UnsignedByteBufferImpl bb = UnsignedByteBufferImpl.wrap(uncompressed, byteOrder); while (bb.remaining() > 0) { int chromId = bb.getInt(); @@ -495,7 +506,7 @@ List decodeZoomData(byte[] buffer, int chrIdx, int start, int end, W uncompressed = buffer; // use uncompressed read buffer directly } - UnsignedByteBuffer bb = UnsignedByteBuffer.wrap(uncompressed, byteOrder); + UnsignedByteBufferImpl bb = UnsignedByteBufferImpl.wrap(uncompressed, byteOrder); while (bb.remaining() > 0) { int chromId = bb.getInt(); @@ -545,7 +556,7 @@ List decodeWigData(byte[] buffer, int chrIdx, int start, int end, Li uncompressed = buffer; // use uncompressed read buffer directly } - UnsignedByteBuffer binaryParser = UnsignedByteBuffer.wrap(uncompressed, byteOrder); + UnsignedByteBuffer binaryParser = UnsignedByteBufferImpl.wrap(uncompressed, byteOrder); int chromId = binaryParser.getInt(); int blockStart = binaryParser.getInt(); int chromStart = blockStart; diff --git a/src/main/java/org/broad/igv/ucsc/bb/RPTree.java b/src/main/java/org/broad/igv/ucsc/bb/RPTree.java index a42d3084ea..a4fa3e4fd9 100644 --- a/src/main/java/org/broad/igv/ucsc/bb/RPTree.java +++ b/src/main/java/org/broad/igv/ucsc/bb/RPTree.java @@ -2,6 +2,7 @@ import org.broad.igv.ucsc.twobit.UnsignedByteBuffer; +import org.broad.igv.ucsc.twobit.UnsignedByteBufferImpl; import java.io.IOException; import java.nio.ByteOrder; @@ -36,11 +37,11 @@ private RPTree(String path, long startOffset) { } void init() throws IOException { - UnsignedByteBuffer binaryParser = UnsignedByteBuffer.loadBinaryBuffer(this.path, this.byteOrder, this.startOffset, RPTREE_HEADER_SIZE); + UnsignedByteBuffer binaryParser = UnsignedByteBufferImpl.loadBinaryBuffer(this.path, this.byteOrder, this.startOffset, RPTREE_HEADER_SIZE); int magic = binaryParser.getInt(); if (magic != RPTree.magic) { this.byteOrder = ByteOrder.BIG_ENDIAN; - binaryParser = UnsignedByteBuffer.loadBinaryBuffer(this.path, this.byteOrder, this.startOffset, RPTREE_HEADER_SIZE); + binaryParser = UnsignedByteBufferImpl.loadBinaryBuffer(this.path, this.byteOrder, this.startOffset, RPTREE_HEADER_SIZE); magic = binaryParser.getInt(); if (magic != RPTree.magic) { throw new RuntimeException("Bad magic number " + magic); @@ -102,13 +103,13 @@ Node readNode(long offset) throws IOException { return this.nodeCache.get(nodeKey); } - UnsignedByteBuffer binaryParser = UnsignedByteBuffer.loadBinaryBuffer(this.path, this.byteOrder, offset, 4); + UnsignedByteBuffer binaryParser = UnsignedByteBufferImpl.loadBinaryBuffer(this.path, this.byteOrder, offset, 4); byte type = binaryParser.get(); boolean isLeaf = (type == 1); byte reserved = binaryParser.get(); int count = binaryParser.getUShort(); int bytesRequired = count * (isLeaf ? RPTREE_NODE_LEAF_ITEM_SIZE : RPTREE_NODE_CHILD_ITEM_SIZE); - binaryParser = UnsignedByteBuffer.loadBinaryBuffer(this.path, this.byteOrder, offset + 4, bytesRequired); + binaryParser = UnsignedByteBufferImpl.loadBinaryBuffer(this.path, this.byteOrder, offset + 4, bytesRequired); Item[] items = new Item[count]; for (int i = 0; i < count; i++) { diff --git a/src/main/java/org/broad/igv/ucsc/twobit/TwoBitSequence.java b/src/main/java/org/broad/igv/ucsc/twobit/TwoBitSequence.java index 8c76aa36d0..d491d997f1 100644 --- a/src/main/java/org/broad/igv/ucsc/twobit/TwoBitSequence.java +++ b/src/main/java/org/broad/igv/ucsc/twobit/TwoBitSequence.java @@ -46,8 +46,8 @@ public TwoBitSequence(String path, String indexPath) throws IOException { index = BPTree.loadBPTree(indexPath, 0); } - UnsignedByteBuffer loadBinaryBuffer(long start, int size) throws IOException { - return UnsignedByteBuffer.loadBinaryBuffer(path, byteOrder, start, size); + UnsignedByteBufferImpl loadBinaryBuffer(long start, int size) throws IOException { + return UnsignedByteBufferImpl.loadBinaryBuffer(path, byteOrder, start, size); } private void init(String path) throws IOException { @@ -56,7 +56,7 @@ private void init(String path) throws IOException { this.sequenceRecordCache = new HashMap<>(); long filePosition = 0; - UnsignedByteBuffer buffer = UnsignedByteBuffer.loadBinaryBuffer(path, byteOrder, filePosition, 64); + UnsignedByteBuffer buffer = UnsignedByteBufferImpl.loadBinaryBuffer(path, byteOrder, filePosition, 64); int signature = buffer.getInt(); if (SIGNATURE != signature) { @@ -140,7 +140,7 @@ public byte[] readSequence(String seqName, int regionStart, int regionEnd) { long start = record.packedPos + baseBytesOffset; int size = regionEnd / 4 - baseBytesOffset + 1; - UnsignedByteBuffer buffer = loadBinaryBuffer(start, size); + UnsignedByteBufferImpl buffer = loadBinaryBuffer(start, size); byte[] baseBytes = buffer.array(); //new byte[size]; diff --git a/src/main/java/org/broad/igv/ucsc/twobit/UnsignedByteBuffer.java b/src/main/java/org/broad/igv/ucsc/twobit/UnsignedByteBuffer.java index 95912f3ad4..6110f155ce 100644 --- a/src/main/java/org/broad/igv/ucsc/twobit/UnsignedByteBuffer.java +++ b/src/main/java/org/broad/igv/ucsc/twobit/UnsignedByteBuffer.java @@ -1,113 +1,30 @@ package org.broad.igv.ucsc.twobit; -import htsjdk.samtools.seekablestream.SeekableStream; -import org.broad.igv.util.stream.IGVSeekableStreamFactory; +public interface UnsignedByteBuffer { + byte get(); -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; + short getShort(); -public class UnsignedByteBuffer { + int getUShort(); - ByteBuffer wrappedBuffer; + int getInt(); + long getUInt(); - public static UnsignedByteBuffer loadBinaryBuffer(String path, ByteOrder byteOrder, long start, int size) throws IOException { - try (SeekableStream is = IGVSeekableStreamFactory.getInstance().getStreamFor(path)) { - ByteBuffer bb = ByteBuffer.allocate(size); - bb.order(byteOrder); - byte[] bytes = bb.array(); - is.seek(start); - is.readFully(bytes); - return new UnsignedByteBuffer(bb); - } - } + long getLong(); - public static UnsignedByteBuffer wrap(byte[] bytes, ByteOrder byteOrder) { - ByteBuffer bb = ByteBuffer.wrap(bytes); - bb.order(byteOrder); - return new UnsignedByteBuffer(bb); - } + float getFloat(); - public static UnsignedByteBuffer wrap(byte[] bytes) { - return wrap(bytes, ByteOrder.LITTLE_ENDIAN); - } + double getDouble(); - private UnsignedByteBuffer(ByteBuffer wrappedBuffer) { - this.wrappedBuffer = wrappedBuffer; - } + String getString(); - public byte get() { - return wrappedBuffer.get(); - } + String getFixedLengthString(int length); - public short getShort() { - return wrappedBuffer.getShort(); - } + int position(); - public int getUShort() { - return Short.toUnsignedInt(wrappedBuffer.getShort()); - } + void position(int i); - public int getInt() { - return wrappedBuffer.getInt(); - } - - public long getUInt() { - return Integer.toUnsignedLong(wrappedBuffer.getInt()); - } - - public long getLong() { - return wrappedBuffer.getLong(); - } - - public float getFloat() { - return wrappedBuffer.getFloat(); - } - - public double getDouble() { - return wrappedBuffer.getDouble(); - } - - /** - * Return a null (0) terminated string - * @return - */ - public String getString() { - ByteArrayOutputStream bis = new ByteArrayOutputStream(1000); - int b; - while ((b = wrappedBuffer.get()) != 0) { - bis.write((byte) b); - } - return new String(bis.toByteArray()); - } - - public String getFixedLengthString(int length) { - byte[] bytes = new byte[length]; - int nonPaddedLength = 0; - wrappedBuffer.get(bytes); - for (int i = 0; i < length; i++) { - if (bytes[i] == 0) break; - nonPaddedLength++; - } - return new String(bytes, 0, nonPaddedLength); - } - - public long position() { - return wrappedBuffer.position(); - } - - public void position(int i) { - wrappedBuffer.position(i); - } - - public byte[] array() { - return wrappedBuffer.array(); - } - - public long remaining() { - return wrappedBuffer.remaining(); - } + int remaining(); } diff --git a/src/main/java/org/broad/igv/ucsc/twobit/UnsignedByteBufferDynamic.java b/src/main/java/org/broad/igv/ucsc/twobit/UnsignedByteBufferDynamic.java new file mode 100644 index 0000000000..2146d911cf --- /dev/null +++ b/src/main/java/org/broad/igv/ucsc/twobit/UnsignedByteBufferDynamic.java @@ -0,0 +1,209 @@ +package org.broad.igv.ucsc.twobit; + +import htsjdk.samtools.seekablestream.SeekableStream; +import org.broad.igv.util.stream.IGVSeekableStreamFactory; + +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * A UnsignedByteBuffer that refills its backing buffer as needed from the underlying file resource. This class was + * created specifically to load the chromTree of a BB file, where the start position is known but total size is + * not. + */ +public class UnsignedByteBufferDynamic implements UnsignedByteBuffer { + + /** + * The current wrapped buffer. This is updated from the file as needed. + */ + private ByteBuffer wrappedBuffer; + + /** + * The file offset corresponding to the start of the current wrapped buffer + */ + private long offset; + + /** + * The file offset at object construction. + */ + private long originalOffset; + + int bufferSize; + ByteOrder byteOrder; + String path; + + public static UnsignedByteBufferDynamic loadBinaryBuffer(String path, ByteOrder byteOrder, long start, int size) throws IOException { + UnsignedByteBufferDynamic b = new UnsignedByteBufferDynamic(path, byteOrder, start, size); + b.updateBuffer(); + return b; + } + + private UnsignedByteBufferDynamic(String path, ByteOrder byteOrder, long start, int bufferSize) { + this.path = path; + this.byteOrder = byteOrder; + this.offset = start; + this.originalOffset = start; + this.bufferSize = bufferSize; + } + + private void advanceBuffer() { + offset += (this.wrappedBuffer == null ? 0 : this.wrappedBuffer.position()); + updateBuffer(); + } + + private void updateBuffer() { + try (SeekableStream is = IGVSeekableStreamFactory.getInstance().getStreamFor(path)) { + this.wrappedBuffer = ByteBuffer.allocate(bufferSize); + this.wrappedBuffer.order(byteOrder); + byte[] bytes = this.wrappedBuffer.array(); + is.seek(offset); + try { + is.readFully(bytes); + } catch (EOFException e) { + // This can happen if near the end of the file and is o.k., in fact expected + } + } catch (IOException e) { + // TODO ?? + } + } + + @Override + public byte get() { + if (wrappedBuffer.remaining() < 1) { + advanceBuffer(); + } + return wrappedBuffer.get(); + } + + @Override + public short getShort() { + if (wrappedBuffer.remaining() < 2) { + advanceBuffer(); + } + return wrappedBuffer.getShort(); + } + + @Override + public int getUShort() { + if (wrappedBuffer.remaining() < 2) { + advanceBuffer(); + } + return Short.toUnsignedInt(wrappedBuffer.getShort()); + } + + @Override + public int getInt() { + if (wrappedBuffer.remaining() < 4) { + advanceBuffer(); + } + return wrappedBuffer.getInt(); + } + + @Override + public long getUInt() { + if (wrappedBuffer.remaining() < 4) { + advanceBuffer(); + } + return Integer.toUnsignedLong(wrappedBuffer.getInt()); + } + + @Override + public long getLong() { + if (wrappedBuffer.remaining() < 8) { + advanceBuffer(); + } + return wrappedBuffer.getLong(); + } + + @Override + public float getFloat() { + if (wrappedBuffer.remaining() < 4) { + advanceBuffer(); + } + return wrappedBuffer.getFloat(); + } + + @Override + public double getDouble() { + if (wrappedBuffer.remaining() < 8) { + advanceBuffer(); + } + return wrappedBuffer.getDouble(); + } + + /** + * Return a null (0) terminated string. This method assumes short strings, and will fail if string length is > 1000 + * + * @return + */ + @Override + public String getString() { + if (wrappedBuffer.remaining() < 1000) { + advanceBuffer(); + } + ByteArrayOutputStream bis = new ByteArrayOutputStream(1000); + int b; + while ((b = wrappedBuffer.get()) != 0) { + bis.write((byte) b); + } + return new String(bis.toByteArray()); + } + + @Override + public String getFixedLengthString(int length) { + if (wrappedBuffer.remaining() < length) { + advanceBuffer(); + } + byte[] bytes = new byte[length]; + int nonPaddedLength = 0; + wrappedBuffer.get(bytes); + for (int i = 0; i < length; i++) { + if (bytes[i] == 0) break; + nonPaddedLength++; + } + return new String(bytes, 0, nonPaddedLength); + } + + /** + * Position is interpreted as relative to the original file offset + * + * @return + */ + @Override + public int position() { + return (int) (offset - originalOffset) + wrappedBuffer.position(); + } + + /** + * Position is interpreted as relative to the original file offset + * + * @return + */ + @Override + public void position(int i) { + + /** + * Position is interpreted relative to the original file position for this buffer + * @return + */ + int newBufferPosition = i - (int) (offset - originalOffset); + + if (newBufferPosition < 0 || newBufferPosition > wrappedBuffer.position() + wrappedBuffer.remaining()) { + offset = originalOffset + i; + updateBuffer(); + } else { + wrappedBuffer.position(newBufferPosition); + } + } + + @Override + public int remaining() { + return wrappedBuffer.remaining(); + } + + + +} diff --git a/src/main/java/org/broad/igv/ucsc/twobit/UnsignedByteBufferImpl.java b/src/main/java/org/broad/igv/ucsc/twobit/UnsignedByteBufferImpl.java new file mode 100644 index 0000000000..9adc533704 --- /dev/null +++ b/src/main/java/org/broad/igv/ucsc/twobit/UnsignedByteBufferImpl.java @@ -0,0 +1,124 @@ +package org.broad.igv.ucsc.twobit; + +import htsjdk.samtools.seekablestream.SeekableStream; +import org.broad.igv.util.stream.IGVSeekableStreamFactory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public class UnsignedByteBufferImpl implements UnsignedByteBuffer { + + ByteBuffer wrappedBuffer; + + public static UnsignedByteBufferImpl loadBinaryBuffer(String path, ByteOrder byteOrder, long start, int size) throws IOException { + try (SeekableStream is = IGVSeekableStreamFactory.getInstance().getStreamFor(path)) { + ByteBuffer bb = ByteBuffer.allocate(size); + bb.order(byteOrder); + byte[] bytes = bb.array(); + is.seek(start); + is.readFully(bytes); + return new UnsignedByteBufferImpl(bb); + } + } + + public static UnsignedByteBufferImpl wrap(byte[] bytes, ByteOrder byteOrder) { + ByteBuffer bb = ByteBuffer.wrap(bytes); + bb.order(byteOrder); + return new UnsignedByteBufferImpl(bb); + } + + public static UnsignedByteBuffer wrap(byte[] bytes) { + return wrap(bytes, ByteOrder.LITTLE_ENDIAN); + } + + private UnsignedByteBufferImpl(ByteBuffer wrappedBuffer) { + this.wrappedBuffer = wrappedBuffer; + } + + @Override + public byte get() { + return wrappedBuffer.get(); + } + + @Override + public short getShort() { + return wrappedBuffer.getShort(); + } + + @Override + public int getUShort() { + return Short.toUnsignedInt(wrappedBuffer.getShort()); + } + + @Override + public int getInt() { + return wrappedBuffer.getInt(); + } + + @Override + public long getUInt() { + return Integer.toUnsignedLong(wrappedBuffer.getInt()); + } + + @Override + public long getLong() { + return wrappedBuffer.getLong(); + } + + @Override + public float getFloat() { + return wrappedBuffer.getFloat(); + } + + @Override + public double getDouble() { + return wrappedBuffer.getDouble(); + } + + /** + * Return a null (0) terminated string + * @return + */ + @Override + public String getString() { + ByteArrayOutputStream bis = new ByteArrayOutputStream(1000); + int b; + while ((b = wrappedBuffer.get()) != 0) { + bis.write((byte) b); + } + return new String(bis.toByteArray()); + } + + @Override + public String getFixedLengthString(int length) { + byte[] bytes = new byte[length]; + int nonPaddedLength = 0; + wrappedBuffer.get(bytes); + for (int i = 0; i < length; i++) { + if (bytes[i] == 0) break; + nonPaddedLength++; + } + return new String(bytes, 0, nonPaddedLength); + } + + @Override + public int position() { + return wrappedBuffer.position(); + } + + @Override + public void position(int i) { + wrappedBuffer.position(i); + } + + public byte[] array() { + return wrappedBuffer.array(); + } + + public int remaining() { + return wrappedBuffer.remaining(); + } + +} From 1bb1bd18028837f5eaf95c0c9b86f4cacdcf45aa Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Sat, 7 Sep 2024 17:44:25 -0700 Subject: [PATCH 037/130] Refactor BB reader to conform more closely to spec -- no assumptions on the layout of the file, particularly the relative location of the chromosome B+ tree. --- .../org/broad/igv/ucsc/bb/BBDataSource.java | 4 +- .../broad/igv/ucsc/bb/BBFeatureSource.java | 4 +- .../java/org/broad/igv/ucsc/bb/BBFile.java | 125 +++++++++--------- .../UnsignedByteBufferDynamic.java | 5 +- .../igv/ucsc/bb/BBFeatureSourceTest.java | 31 ++++- .../org/broad/igv/ucsc/bb/BBFileTest.java | 57 +++++++- 6 files changed, 155 insertions(+), 71 deletions(-) rename src/main/java/org/broad/igv/ucsc/{twobit => bb}/UnsignedByteBufferDynamic.java (97%) diff --git a/src/main/java/org/broad/igv/ucsc/bb/BBDataSource.java b/src/main/java/org/broad/igv/ucsc/bb/BBDataSource.java index a6f8f0d93b..873da3da01 100644 --- a/src/main/java/org/broad/igv/ucsc/bb/BBDataSource.java +++ b/src/main/java/org/broad/igv/ucsc/bb/BBDataSource.java @@ -69,7 +69,7 @@ public BBDataSource(BBFile reader, Genome genome) throws IOException { */ private void initMinMax() { - BBTotalSummary totalSummary = reader.totalSummary; + BBTotalSummary totalSummary = reader.getTotalSummary(); if (totalSummary == null) { dataMin = 0; dataMax = 100; @@ -144,7 +144,7 @@ public double getDataMin() { protected DataTile getRawData(String chr, int start, int end) { try { - long rTreeOffset = reader.header.fullIndexOffset; + long rTreeOffset = reader.getHeader().fullIndexOffset; List chunks = this.reader.getLeafChunks(chr, start, chr, end, rTreeOffset); Integer chrIdx = reader.getIdForChr(chr); diff --git a/src/main/java/org/broad/igv/ucsc/bb/BBFeatureSource.java b/src/main/java/org/broad/igv/ucsc/bb/BBFeatureSource.java index d0a53a9df7..9b6cccbb5a 100644 --- a/src/main/java/org/broad/igv/ucsc/bb/BBFeatureSource.java +++ b/src/main/java/org/broad/igv/ucsc/bb/BBFeatureSource.java @@ -65,7 +65,7 @@ public BBFeatureSource(BBFile reader, Genome genome) throws IOException { this.reader = reader; // viz window to load average of 10,000 features per screen - featureVisiblityWindow = reader.featureDensity > 0 ? (int) (10000 / reader.featureDensity) : -1; + featureVisiblityWindow = reader.getFeatureDensity() > 0 ? (int) (10000 / reader.getFeatureDensity()) : -1; } @@ -93,7 +93,7 @@ public void close() { */ public Iterator getFeatures(String chr, int start, int end) throws IOException { - long rTreeOffset = reader.header.fullIndexOffset; + long rTreeOffset = reader.getHeader().fullIndexOffset; Integer chrIdx = reader.getIdForChr(chr); if(chrIdx == null) { return Collections.emptyIterator(); diff --git a/src/main/java/org/broad/igv/ucsc/bb/BBFile.java b/src/main/java/org/broad/igv/ucsc/bb/BBFile.java index 264ab7731e..0267ec0115 100644 --- a/src/main/java/org/broad/igv/ucsc/bb/BBFile.java +++ b/src/main/java/org/broad/igv/ucsc/bb/BBFile.java @@ -12,7 +12,6 @@ import org.broad.igv.ucsc.twobit.UnsignedByteBuffer; import org.broad.igv.ucsc.bb.codecs.BBCodec; import org.broad.igv.ucsc.bb.codecs.BBCodecFactory; -import org.broad.igv.ucsc.twobit.UnsignedByteBufferDynamic; import org.broad.igv.ucsc.twobit.UnsignedByteBufferImpl; import org.broad.igv.util.CompressionUtils; import org.broad.igv.util.stream.IGVSeekableStreamFactory; @@ -83,71 +82,83 @@ */ public class BBFile { + enum Type {BIGWIG, BIGBED} + static public final int BBFILE_HEADER_SIZE = 64; static public final long BIGWIG_MAGIC = 2291137574l; // BigWig Magic static public final long BIGBED_MAGIC = 2273964779l; // BigBed Magic static public final int BBFILE_EXTENDED_HEADER_HEADER_SIZE = 64; private Trix trix; - String autosql; + private String autosql; private ChromTree chromTree; private String[] chrNames; - double featureDensity; - - Map chrAliasTable; - public BBTotalSummary totalSummary; + private double featureDensity; + private Map chrAliasTable; + private BBTotalSummary totalSummary; private BPTree[] _searchTrees; private Map rTreeCache; - BBCodec bedCodec; + private BBCodec bedCodec; + private String path; + private Type type; + private BBHeader header = null; + private BBZoomHeader[] zoomHeaders; + private ByteOrder byteOrder; + private Genome genome; - public boolean isBigWigFile() { - return type == Type.BIGWIG; + public BBFile(String path, Genome genome) throws IOException { + this.path = path; + this.genome = genome; + this.chrAliasTable = new HashMap<>(); + this.rTreeCache = new HashMap<>(); + init(); } - public boolean isBigBedFile() { - return type == Type.BIGBED; + public BBFile(String path, Genome genome, String trixPath) throws IOException { + this(path, genome); + this.trix = new Trix(trixPath + "x", trixPath); } - public String getAutoSql() { - return autosql; + void init() throws IOException { + this.header = readHeader(); } - public String[] getChromosomeNames() { - return chrNames; + public BBTotalSummary getTotalSummary() { + return totalSummary; } - public void setTrix(Trix trix) { + public Type getType() { + return type; } + public BBHeader getHeader() { + return header; + } - enum Type {BIGWIG, BIGBED} - - String path; - Type type; - BBHeader header = null; - - BBZoomHeader[] zoomHeaders; - ByteOrder byteOrder; - Genome genome; + public Genome getGenome() { + return genome; + } + public boolean isBigWigFile() { + return type == Type.BIGWIG; + } - public BBFile(String path, Genome genome) throws IOException { - this.path = path; - this.genome = genome; - this.chrAliasTable = new HashMap<>(); - this.rTreeCache = new HashMap<>(); - init(); + public boolean isBigBedFile() { + return type == Type.BIGBED; } - public BBFile(String path, Genome genome, String trixPath) throws IOException { - this(path, genome); - this.trix = new Trix(trixPath + "x", trixPath); + public double getFeatureDensity() { + return featureDensity; + } + public String getAutoSQL() { + return autosql; } - void init() throws IOException { - this.header = readHeader(); + public String[] getChromosomeNames() { + return chrNames; } BBHeader readHeader() throws IOException { + // The common header ByteOrder order = ByteOrder.LITTLE_ENDIAN; UnsignedByteBuffer buffer = UnsignedByteBufferImpl.loadBinaryBuffer(this.path, order, 0, BBFILE_HEADER_SIZE); long magic = buffer.getUInt(); @@ -183,8 +194,12 @@ BBHeader readHeader() throws IOException { header.uncompressBuffSize = buffer.getInt(); header.extensionOffset = buffer.getLong(); - // Read rest of fields up to full data offset - buffer = UnsignedByteBufferImpl.loadBinaryBuffer(this.path, order, BBFILE_HEADER_SIZE, (int) (header.fullDataOffset - BBFILE_HEADER_SIZE + 4)); + // Zoom headers, autosql, and total summary if present + int size = (int) (header.totalSummaryOffset > 0 ? + header.totalSummaryOffset - BBFILE_HEADER_SIZE + 40 : + Math.min(header.fullDataOffset, header.chromTreeOffset) - BBFILE_HEADER_SIZE); + + buffer = UnsignedByteBufferImpl.loadBinaryBuffer(this.path, order, BBFILE_HEADER_SIZE, size); // Zoom headers -- immediately follows the common header this.zoomHeaders = new BBZoomHeader[header.nZoomLevels]; @@ -212,38 +227,28 @@ BBHeader readHeader() throws IOException { this.totalSummary = BBTotalSummary.parseSummary(buffer); } - //Total data count -- for bigbed this is the number of features, for bigwig it is number of sections - buffer.position((int) (header.fullDataOffset - BBFILE_HEADER_SIZE)); - header.dataCount = buffer.getInt(); - // Chromosome tree -- this normally preceeds fullDataOffset so will be within the buffer. However, this // isn't guaranteed, we have to check - int chromtreeBufferPosition = (int) (header.chromTreeOffset - startOffset); - if(chromtreeBufferPosition > 0 && chromtreeBufferPosition < buffer.position() + buffer.remaining()) { - buffer.position((int) (header.chromTreeOffset - startOffset)); - } else { - buffer = UnsignedByteBufferDynamic.loadBinaryBuffer(this.path, order, header.chromTreeOffset, 1000); - } + int chromtreeBufferSize = (int) Math.min(1000000, Math.max(10000, header.fullDataOffset - header.chromTreeOffset)); + buffer = UnsignedByteBufferDynamic.loadBinaryBuffer(this.path, order, header.chromTreeOffset, chromtreeBufferSize); this.chromTree = ChromTree.parseTree(buffer, startOffset, this.genome); this.chrNames = this.chromTree.names(); - this.header = header; - //extension - if (header.extensionOffset > 0) { - this.loadExtendedHeader(header.extensionOffset); - } + if (type == Type.BIGBED) { + //Total data count -- for bigbed this is the number of features, for bigwig it is number of sections + buffer = UnsignedByteBufferImpl.loadBinaryBuffer(this.path, order, header.fullDataOffset, 4); + header.dataCount = buffer.getInt(); + this.featureDensity = ((double) header.dataCount) / chromTree.sumLengths; - // total summary stats - if (header.version > 1) { - buffer = UnsignedByteBufferImpl.loadBinaryBuffer(this.path, order, header.totalSummaryOffset, 40); - this.totalSummary = BBTotalSummary.parseSummary(buffer); + bedCodec = BBCodecFactory.getCodec(autosql, header.definedFieldCount); } - this.featureDensity = ((double) header.dataCount) / chromTree.sumLengths; + this.header = header; - if (type == Type.BIGBED) { - bedCodec = BBCodecFactory.getCodec(autosql, header.definedFieldCount); + //extension + if (header.extensionOffset > 0) { + this.loadExtendedHeader(header.extensionOffset); } diff --git a/src/main/java/org/broad/igv/ucsc/twobit/UnsignedByteBufferDynamic.java b/src/main/java/org/broad/igv/ucsc/bb/UnsignedByteBufferDynamic.java similarity index 97% rename from src/main/java/org/broad/igv/ucsc/twobit/UnsignedByteBufferDynamic.java rename to src/main/java/org/broad/igv/ucsc/bb/UnsignedByteBufferDynamic.java index 2146d911cf..e6e5c4bc45 100644 --- a/src/main/java/org/broad/igv/ucsc/twobit/UnsignedByteBufferDynamic.java +++ b/src/main/java/org/broad/igv/ucsc/bb/UnsignedByteBufferDynamic.java @@ -1,6 +1,7 @@ -package org.broad.igv.ucsc.twobit; +package org.broad.igv.ucsc.bb; import htsjdk.samtools.seekablestream.SeekableStream; +import org.broad.igv.ucsc.twobit.UnsignedByteBuffer; import org.broad.igv.util.stream.IGVSeekableStreamFactory; import java.io.ByteArrayOutputStream; @@ -12,7 +13,7 @@ /** * A UnsignedByteBuffer that refills its backing buffer as needed from the underlying file resource. This class was * created specifically to load the chromTree of a BB file, where the start position is known but total size is - * not. + * not. It might have general utility but has not been tested with any other use case. */ public class UnsignedByteBufferDynamic implements UnsignedByteBuffer { diff --git a/src/test/java/org/broad/igv/ucsc/bb/BBFeatureSourceTest.java b/src/test/java/org/broad/igv/ucsc/bb/BBFeatureSourceTest.java index 12d58aa9b1..a95e8d26d1 100644 --- a/src/test/java/org/broad/igv/ucsc/bb/BBFeatureSourceTest.java +++ b/src/test/java/org/broad/igv/ucsc/bb/BBFeatureSourceTest.java @@ -3,7 +3,6 @@ import htsjdk.tribble.Feature; import org.broad.igv.feature.BasicFeature; import org.broad.igv.feature.genome.Genome; -import org.broad.igv.ucsc.Trix; import org.broad.igv.util.TestUtils; import org.junit.Ignore; import org.junit.Test; @@ -76,7 +75,7 @@ public void testBigBed() throws IOException { String path = TestUtils.DATA_DIR + "bb/chr21.refseq.bb"; BBFile bbReader = new BBFile(path, null); - assertTrue(bbReader.type == BBFile.Type.BIGBED); + assertTrue(bbReader.getType() == BBFile.Type.BIGBED); BBFeatureSource bbSource = new BBFeatureSource(bbReader, null); @@ -97,6 +96,34 @@ public void testBigBed() throws IOException { } + /** + * Test the 'dataCount' value, which should equal the total count of feature records in the file. + * + * @throws IOException + */ + @Test + public void testDataCount() throws IOException { + + String path = TestUtils.DATA_DIR + "bb/chr21.refseq.bb"; + BBFile bbReader = new BBFile(path, null); + + assertTrue(bbReader.getType() == BBFile.Type.BIGBED); + + BBFeatureSource bbSource = new BBFeatureSource(bbReader, null); + String [] chrNames = bbReader.getChromosomeNames(); + + int count = 0; + for(String chr : chrNames) { + Iterator iter = bbSource.getFeatures(chr, 0, Integer.MAX_VALUE); + while (iter.hasNext()) { + iter.next(); + count++; + } + } + assertEquals("Feature count", bbReader.getHeader().dataCount, count); + + } + @Test @Ignore diff --git a/src/test/java/org/broad/igv/ucsc/bb/BBFileTest.java b/src/test/java/org/broad/igv/ucsc/bb/BBFileTest.java index 6928f172e5..4ae89067e6 100644 --- a/src/test/java/org/broad/igv/ucsc/bb/BBFileTest.java +++ b/src/test/java/org/broad/igv/ucsc/bb/BBFileTest.java @@ -1,25 +1,76 @@ package org.broad.igv.ucsc.bb; -import htsjdk.tribble.Feature; import org.broad.igv.feature.BasicFeature; import org.broad.igv.util.TestUtils; import org.junit.Test; import java.io.IOException; -import java.util.Iterator; import static org.junit.Assert.*; import static org.junit.Assert.assertTrue; public class BBFileTest { + /** + * Test a BW file with an unusual layout (chromTree after full data). + */ @Test - public void testGene() throws IOException { + public void testChromTree() throws IOException { + String bbFile = "https://data.broadinstitute.org/igvdata/test/data/bb/chromTreeTest.bigwig"; + BBFile reader = new BBFile(bbFile, null); + reader.readHeader(); + String [] chrNames = reader.getChromosomeNames(); + assertEquals(6, chrNames.length); + } + + /** + * Test a BW file with an typical layout (chromTree before full data). + */ + @Test + public void testChromTree2() throws IOException { String bbFile = TestUtils.DATA_DIR + "bb/GCF_000009045.1_ASM904v1.ncbiGene.bb"; BBFile reader = new BBFile(bbFile, null); BBHeader header = reader.readHeader(); assertNotNull(header); + String [] chrNames = reader.getChromosomeNames(); + assertEquals(1, chrNames.length); + } + + @Test + public void testAutoSQL() throws IOException { + + String bbFile = TestUtils.DATA_DIR + "bb/GCF_000009045.1_ASM904v1.ncbiGene.bb"; + BBFile reader = new BBFile(bbFile, null); + reader.readHeader(); + + String autoSql = "table bigGenePred\n" + + "\"bigGenePred gene models\"\n" + + " (\n" + + " string chrom; \"Reference sequence chromosome or scaffold\"\n" + + " uint chromStart; \"Start position in chromosome\"\n" + + " uint chromEnd; \"End position in chromosome\"\n" + + " string name; \"Name or ID of item, ideally both human readable and unique\"\n" + + " uint score; \"Score (0-1000)\"\n" + + " char[1] strand; \"+ or - for strand\"\n" + + " uint thickStart; \"Start of where display should be thick (start codon)\"\n" + + " uint thickEnd; \"End of where display should be thick (stop codon)\"\n" + + " uint reserved; \"RGB value (use R,G,B string in input file)\"\n" + + " int blockCount; \"Number of blocks\"\n" + + " int[blockCount] blockSizes; \"Comma separated list of block sizes\"\n" + + " int[blockCount] chromStarts; \"Start positions relative to chromStart\"\n" + + " string name2; \"Alternative/human readable name\"\n" + + " string cdsStartStat; \"Status of CDS start annotation (none, unknown, incomplete, or complete)\"\n" + + " string cdsEndStat; \"Status of CDS end annotation (none, unknown, incomplete, or complete)\"\n" + + " int[blockCount] exonFrames; \"Exon frame {0,1,2}, or -1 if no frame for exon\"\n" + + " string type; \"Transcript type\"\n" + + " string geneName; \"Primary identifier for gene\"\n" + + " string geneName2; \"Alternative/human readable gene name\"\n" + + " string geneType; \"Gene type\"\n" + + " )\n" + + "\n"; + + assertEquals(autoSql.trim(), reader.getAutoSQL().trim()); } From ab89ab95ff04812fe0548d80fb65d4bdf1eadba9 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Mon, 9 Sep 2024 13:01:29 -0700 Subject: [PATCH 038/130] Remove "snappy" dependency --- build.gradle | 2 -- src/main/java/org/broad/igv/tools/sort/AsciiSorter.java | 1 - src/main/java/org/broad/igv/ui/IGV.java | 3 +++ 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 6141ae4ea1..307045090d 100644 --- a/build.gradle +++ b/build.gradle @@ -111,7 +111,6 @@ dependencies { [group: 'org.apache.commons', name: 'commons-math3', version: '3.6.1'], [group: 'commons-io', name: 'commons-io', version: '2.7'], [group: 'org.apache.commons', name: 'commons-compress', version: '1.26.0'], - [group: 'org.xerial.snappy', name: 'snappy-java', version: '1.1.10.4'], [group: 'org.apache.commons', name: 'commons-jexl', version: '2.1.1'], [group: 'org.apache.commons', name: 'commons-lang3', version: '3.9'], [group: 'com.github.samtools', name: 'htsjdk', version: '4.1.1'], @@ -186,7 +185,6 @@ tasks.withType(Test) { systemProperties['make.fail'] = 'false' systemProperties['include.longrunning'] = 'false' systemProperties['ignore.ioexceptions'] = 'false' - systemProperties['org.xerial.snappy.tempdir'] = 'build/tmp' maxHeapSize = '2g' maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 useJUnitPlatform() diff --git a/src/main/java/org/broad/igv/tools/sort/AsciiSorter.java b/src/main/java/org/broad/igv/tools/sort/AsciiSorter.java index c5558b39ad..6a2202268b 100644 --- a/src/main/java/org/broad/igv/tools/sort/AsciiSorter.java +++ b/src/main/java/org/broad/igv/tools/sort/AsciiSorter.java @@ -68,7 +68,6 @@ public AsciiSorter(File inputFile, File outputFile) { this.writeStdOut = outputFile == null; this.tmpDir = new File(System.getProperty("java.io.tmpdir"), System.getProperty("user.name")); - System.setProperty("snappy.disable", "true"); if (!tmpDir.exists()) { tmpDir.mkdir(); } diff --git a/src/main/java/org/broad/igv/ui/IGV.java b/src/main/java/org/broad/igv/ui/IGV.java index 63344ddc6b..a0de4ccd06 100644 --- a/src/main/java/org/broad/igv/ui/IGV.java +++ b/src/main/java/org/broad/igv/ui/IGV.java @@ -1884,6 +1884,9 @@ public void run() { final IGVPreferences preferences = PreferencesManager.getPreferences(); + // Disable "snappy", used by htsjdk for sorting but not supported by IGV + System.setProperty("snappy.disable", "true"); + // Start CommandsServer **before** loading the initial genome, as credentials might need to be set for // privately hosted genomes. startCommandsServer(igvArgs, preferences); From c265c9d4dcaaafce5fc3ab7208a148b41bd9aedc Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Mon, 9 Sep 2024 14:32:13 -0700 Subject: [PATCH 039/130] Disable "snappy" for command line igvtools --- src/main/java/org/broad/igv/tools/IgvTools.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/broad/igv/tools/IgvTools.java b/src/main/java/org/broad/igv/tools/IgvTools.java index 9c023d60ef..ed9e7653d0 100644 --- a/src/main/java/org/broad/igv/tools/IgvTools.java +++ b/src/main/java/org/broad/igv/tools/IgvTools.java @@ -251,6 +251,9 @@ public static void main(String[] argv) { try { Globals.setHeadless(true); + // Disable "snappy", used by htsjdk for sorting but not supported by IGV + System.setProperty("snappy.disable", "true"); + (new IgvTools()).run(argv); userMessageWriter.println("Done"); From 4521c92408bdc6c74276b861f9bc2c25ff2e5157 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Mon, 9 Sep 2024 15:06:55 -0700 Subject: [PATCH 040/130] disable snappy, second attempt --- scripts/igv.sh | 2 ++ scripts/igvtools | 4 ++-- src/main/java/org/broad/igv/ui/IGV.java | 3 --- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/scripts/igv.sh b/scripts/igv.sh index 22d2d5792d..41ea0ec06c 100755 --- a/scripts/igv.sh +++ b/scripts/igv.sh @@ -26,6 +26,7 @@ java -version if [ -e "$HOME/.igv/java_arguments" ]; then java --module-path="${prefix}/lib" -Xmx8g \ @"${prefix}/igv.args" \ + -Dsamjdk.snappy.disable=true \ -Dapple.laf.useScreenMenuBar=true \ -Djava.net.preferIPv4Stack=true \ -Djava.net.useSystemProxies=true \ @@ -34,6 +35,7 @@ if [ -e "$HOME/.igv/java_arguments" ]; then else java --module-path="${prefix}/lib" -Xmx8g \ @"${prefix}/igv.args" \ + -Dsamjdk.snappy.disable=true \ -Dapple.laf.useScreenMenuBar=true \ -Djava.net.preferIPv4Stack=true \ -Djava.net.useSystemProxies=true \ diff --git a/scripts/igvtools b/scripts/igvtools index 2de1d72471..4c55f1dc5a 100755 --- a/scripts/igvtools +++ b/scripts/igvtools @@ -12,12 +12,12 @@ fi # Check if there is a user-specified Java arguments file if [ -e "$HOME/.igv/java_arguments" ]; then - java -showversion -Djava.awt.headless=true --module-path="${prefix}/lib" -Xmx1500m \ + java -showversion -Djava.awt.headless=true -Dsamjdk.snappy.disable=true --module-path="${prefix}/lib" -Xmx1500m \ @"${prefix}/igv.args" \ @"$HOME/.igv/java_arguments" \ --module=org.igv/org.broad.igv.tools.IgvTools "$@" else - java -showversion -Djava.awt.headless=true --module-path="${prefix}/lib" -Xmx1500m \ + java -showversion -Djava.awt.headless=true -Dsamjdk.snappy.disable=true --module-path="${prefix}/lib" -Xmx1500m \ @"${prefix}/igv.args" \ --module=org.igv/org.broad.igv.tools.IgvTools "$@" fi diff --git a/src/main/java/org/broad/igv/ui/IGV.java b/src/main/java/org/broad/igv/ui/IGV.java index a0de4ccd06..63344ddc6b 100644 --- a/src/main/java/org/broad/igv/ui/IGV.java +++ b/src/main/java/org/broad/igv/ui/IGV.java @@ -1884,9 +1884,6 @@ public void run() { final IGVPreferences preferences = PreferencesManager.getPreferences(); - // Disable "snappy", used by htsjdk for sorting but not supported by IGV - System.setProperty("snappy.disable", "true"); - // Start CommandsServer **before** loading the initial genome, as credentials might need to be set for // privately hosted genomes. startCommandsServer(igvArgs, preferences); From 5b18361f9240cae38121876a7be9e99b5089cf53 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Mon, 9 Sep 2024 20:02:35 -0700 Subject: [PATCH 041/130] disable snappy 3 --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 307045090d..089ce3e033 100644 --- a/build.gradle +++ b/build.gradle @@ -185,6 +185,7 @@ tasks.withType(Test) { systemProperties['make.fail'] = 'false' systemProperties['include.longrunning'] = 'false' systemProperties['ignore.ioexceptions'] = 'false' + systemProperties['samjdk.snappy.disable'] = 'true' maxHeapSize = '2g' maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 useJUnitPlatform() From 97dd56db0c323096dff694369cbf1a0d3287a766 Mon Sep 17 00:00:00 2001 From: Louis Bergelson Date: Tue, 10 Sep 2024 19:19:48 -0400 Subject: [PATCH 042/130] Improve behavior of the Hide small indels menu option (#1564) * If you cancel the popup dialogue or enter an invalid value then no change will be made. Previously cancelling it produced a stacktrace in the log and toggled the state using the old value. --- .gitignore | 3 +++ .../org/broad/igv/sam/AlignmentTrackMenu.java | 15 +++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 9daad7f241..4b782f51d8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ /.gradle/ /build/ /.idea/ + +#JENV local config file +.java-version diff --git a/src/main/java/org/broad/igv/sam/AlignmentTrackMenu.java b/src/main/java/org/broad/igv/sam/AlignmentTrackMenu.java index d767857ae7..eeb2ecda45 100644 --- a/src/main/java/org/broad/igv/sam/AlignmentTrackMenu.java +++ b/src/main/java/org/broad/igv/sam/AlignmentTrackMenu.java @@ -123,10 +123,17 @@ class AlignmentTrackMenu extends IGVPopupMenu { smallIndelsItem.addActionListener(aEvt -> UIUtilities.invokeOnEventThread(() -> { if (smallIndelsItem.isSelected()) { String sith = MessageUtils.showInputDialog("Small indel threshold: ", String.valueOf(renderOptions.getSmallIndelThreshold())); - try { - renderOptions.setSmallIndelThreshold(Integer.parseInt(sith)); - } catch (NumberFormatException exc) { - log.error("Error setting small indel threshold - not an integer", exc); + if (sith == null) { + // dialogue was cancelled so no change should be made + return; + } else { + try { + renderOptions.setSmallIndelThreshold(Integer.parseInt(sith)); + } catch (NumberFormatException exc) { + log.error("Error setting small indel threshold - not an integer", exc); + //error so no change should be made + return; + } } } renderOptions.setHideSmallIndels(smallIndelsItem.isSelected()); From 5a9c3ec689d94311666f7b3a26b192b5cc865b65 Mon Sep 17 00:00:00 2001 From: Jim Robinson <933148+jrobinso@users.noreply.github.com> Date: Tue, 10 Sep 2024 16:20:27 -0700 Subject: [PATCH 043/130] Add basemod colors for 3 chebi codes. See #1563 (#1566) --- src/main/java/org/broad/igv/prefs/Constants.java | 6 +++++- .../java/org/broad/igv/sam/mods/BaseModificationColors.java | 4 ++++ src/main/resources/org/broad/igv/prefs/preferences.tab | 3 +++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/broad/igv/prefs/Constants.java b/src/main/java/org/broad/igv/prefs/Constants.java index aa93e9656f..b8bc1a425f 100644 --- a/src/main/java/org/broad/igv/prefs/Constants.java +++ b/src/main/java/org/broad/igv/prefs/Constants.java @@ -197,6 +197,9 @@ private Constants() { public static final String BASEMOD_B_COLOR = "BASEMOD.B_COLOR"; public static final String BASEMOD_A_COLOR = "BASEMOD.A_COLOR"; public static final String BASEMOD_O_COLOR = "BASEMOD.O_COLOR"; + public static final String BASEMOD_17082_COLOR = "BASEMOD.17082_COLOR"; + public static final String BASEMOD_17596_COLOR = "BASEMOD.17596_COLOR"; + public static final String BASEMOD_21839_COLOR = "BASEMOD.21839_COLOR"; public static final String BASEMOD_OTHER_COLOR = "BASEMOD.OTHER_COLOR"; public static final String BASEMOD_NONE_A_COLOR = "BASEMOD.NONE_A_COLOR"; @@ -204,6 +207,8 @@ private Constants() { public static final String BASEMOD_NONE_T_COLOR = "BASEMOD.NONE_T_COLOR"; public static final String BASEMOD_NONE_G_COLOR = "BASEMOD.NONE_G_COLOR"; public static final String BASEMOD_NONE_N_COLOR = "BASEMOD.NONE_N_COLOR"; + + public static final String BASEMOD_GROUP_BY_STRAND = "BASEMOD.GROUP_BY_STRAND"; public static final String BASEMOD_SKIPPED_BASES = "BASEMOD.SKIPPED_BASES"; public static final String SMRT_KINETICS_SHOW_OPTIONS = "SMRT_KINETICS.SHOW_OPTIONS"; @@ -300,7 +305,6 @@ private Constants() { public static final String CIRC_VIEW_HOST = "CIRC_VIEW_HOST"; - /** * List of keys that affect the alignments loaded. This list is used to trigger a reload, if required. * Not all alignment preferences need trigger a reload, this is a subset. diff --git a/src/main/java/org/broad/igv/sam/mods/BaseModificationColors.java b/src/main/java/org/broad/igv/sam/mods/BaseModificationColors.java index a96874caea..f8c05e78b4 100644 --- a/src/main/java/org/broad/igv/sam/mods/BaseModificationColors.java +++ b/src/main/java/org/broad/igv/sam/mods/BaseModificationColors.java @@ -43,6 +43,10 @@ public static void updateColors() { colors.put("e", preferences.getAsColor(BASEMOD_E_COLOR)); colors.put("b", preferences.getAsColor(BASEMOD_B_COLOR)); colors.put("a", preferences.getAsColor(BASEMOD_A_COLOR)); + colors.put("17082", preferences.getAsColor(BASEMOD_17082_COLOR)); + colors.put("17596", preferences.getAsColor(BASEMOD_17596_COLOR)); + colors.put("21839", preferences.getAsColor(BASEMOD_21839_COLOR)); + colors.put("other", preferences.getAsColor(BASEMOD_OTHER_COLOR)); colors.put("NONE_A", preferences.getAsColor(BASEMOD_NONE_A_COLOR)); colors.put("NONE_C", preferences.getAsColor(BASEMOD_NONE_C_COLOR)); diff --git a/src/main/resources/org/broad/igv/prefs/preferences.tab b/src/main/resources/org/broad/igv/prefs/preferences.tab index e58cd6e1f5..9d7944d8b2 100644 --- a/src/main/resources/org/broad/igv/prefs/preferences.tab +++ b/src/main/resources/org/broad/igv/prefs/preferences.tab @@ -236,6 +236,9 @@ BASEMOD.E_COLOR 5fU color 141,221,208 BASEMOD.B_COLOR 5caU color 0,100,47 BASEMOD.A_COLOR 6mA color 51,0,111 BASEMOD.O_COLOR 8oxoG color 111,78,129 +BASEMOD.17082_COLOR pseU color 51,153,255 +BASEMOD.17596_COLOR inosine color 102,153,0 +BASEMOD.21839_COLOR 4mC color 153,0,153 BASEMOD.OTHER_COLOR Other color 132,178,158 ## Unomdified base colors (2-color option) From e8e38e1d288999472659eed5cefe67611ee4f1bc Mon Sep 17 00:00:00 2001 From: Jim Robinson <933148+jrobinso@users.noreply.github.com> Date: Tue, 10 Sep 2024 16:23:26 -0700 Subject: [PATCH 044/130] Genome loading refactor to recover gracefully on startup errors (#1553) * Genome loading refactor to recover gracefully on startup errors related to genome load --- .../org/broad/igv/feature/genome/Genome.java | 48 +++++----- .../igv/feature/genome/GenomeManager.java | 87 +++++++++++-------- .../feature/genome/GenomeServerException.java | 41 --------- .../feature/genome/load/JsonGenomeLoader.java | 18 ++-- .../org/broad/igv/session/SessionWriter.java | 18 ++-- .../org/broad/igv/track/SequenceTrack.java | 7 +- src/main/java/org/broad/igv/ui/IGV.java | 36 ++++---- .../igv/ui/commandbar/GenomeComboBox.java | 66 ++++++-------- .../igv/ui/commandbar/IGVCommandBar.java | 30 ++++--- .../broad/igv/ui/panel/ReferenceFrame.java | 13 +-- .../broad/igv/ui/panel/ZoomSliderPanel.java | 2 +- .../java/org/broad/igv/util/ParsingUtils.java | 4 - .../org/broad/igv/AbstractHeadlessTest.java | 4 +- 13 files changed, 170 insertions(+), 204 deletions(-) delete mode 100644 src/main/java/org/broad/igv/feature/genome/GenomeServerException.java diff --git a/src/main/java/org/broad/igv/feature/genome/Genome.java b/src/main/java/org/broad/igv/feature/genome/Genome.java index 990bd1ffd6..cf52b6ae01 100644 --- a/src/main/java/org/broad/igv/feature/genome/Genome.java +++ b/src/main/java/org/broad/igv/feature/genome/Genome.java @@ -55,6 +55,7 @@ import org.broad.igv.track.TribbleFeatureSource; import org.broad.igv.ucsc.Hub; import org.broad.igv.ucsc.twobit.TwoBitSequence; +import org.broad.igv.util.ParsingUtils; import org.broad.igv.util.ResourceLocator; import org.broad.igv.util.liftover.Liftover; @@ -142,8 +143,14 @@ public Genome(GenomeConfig config) throws IOException { } else if (sequence != null && sequence.hasChromosomes()) { chromosomeList = sequence.getChromosomes(); } else if (config.indexURL != null) { - FastaIndex index = new FastaIndex(config.indexURL); - chromosomeList = index.getChromosomes(); + try { + // If chromosome info is not otherwise available try to parse the fasta index, if available. This + // situation can occur if a twoBitURL is defined but chromSizes is not. + FastaIndex index = new FastaIndex(config.indexURL); + chromosomeList = index.getChromosomes(); + } catch (IOException e) { + log.error("Error loading fasta index", e); + } } // If list of chromosomes is specified use it for the whole genome view, and to prepopulate the @@ -224,7 +231,7 @@ public Genome(GenomeConfig config) throws IOException { /** * Alternate constructor for defining a minimal genome, usually from parsing a chrom.sizes file. Used to - * create mock genomes for igvtools and testing. + * create mock genomes for igvtools and for testing. * * @param id * @param chromosomes @@ -242,6 +249,7 @@ public Genome(String id, List chromosomes) { chromosomeMap.put(chromosome.getName(), chromosome); } this.longChromosomeNames = computeLongChromosomeNames(); + this.homeChromosome = this.longChromosomeNames.size() > 1 ? Globals.CHR_ALL : chromosomeNames.get(0); this.chromAliasSource = (new ChromAliasDefaults(id, chromosomeNames)); } @@ -623,18 +631,15 @@ public long getWGLength() { } - // TODO A hack (obviously), we need to record a species in the genome definitions to support old style - // blat servers. + // Species mapping to support old style blat servers. This should not be needed for current IGV releases. private static Map ucscSpeciesMap; private static synchronized String getSpeciesForID(String id) { if (ucscSpeciesMap == null) { ucscSpeciesMap = new HashMap<>(); - InputStream is = null; + try (InputStream is = Genome.class.getResourceAsStream("speciesMapping.txt")) { - try { - is = Genome.class.getResourceAsStream("speciesMapping.txt"); BufferedReader br = new BufferedReader(new InputStreamReader(is)); String nextLine; @@ -649,14 +654,7 @@ private static synchronized String getSpeciesForID(String id) { } } catch (IOException e) { log.error("Error reading species mapping table", e); - } finally { - if (is != null) try { - is.close(); - } catch (IOException e) { - log.error("", e); - } } - } for (Map.Entry entry : ucscSpeciesMap.entrySet()) { @@ -685,18 +683,6 @@ public List getAnnotationResources() { return annotationResources; } - /** - * Mock genome for unit tests - */ - - private Genome(String id) { - this.id = id; - } - - public boolean getShowWholeGenomeView() { - return showWholeGenomeView; - } - public Map getLiftoverMap() { return liftoverMap; } @@ -809,4 +795,12 @@ public void setHub(Hub hub) { this.hub = hub; } + public synchronized static Genome nullGenome() { + if(nullGenome == null) { + nullGenome = new Genome("None", Arrays.asList(new Chromosome(0, "", 0))); + } + return nullGenome; + } + + private static Genome nullGenome = null; } diff --git a/src/main/java/org/broad/igv/feature/genome/GenomeManager.java b/src/main/java/org/broad/igv/feature/genome/GenomeManager.java index 3783b7aa74..6d1a13abec 100644 --- a/src/main/java/org/broad/igv/feature/genome/GenomeManager.java +++ b/src/main/java/org/broad/igv/feature/genome/GenomeManager.java @@ -134,48 +134,46 @@ public static File getGenomeFile(String genomePath) throws MalformedURLException return archiveFile; } - public void setCurrentGenome(Genome genome) { - if (genome != null) { - PreferencesManager.getPreferences().setLastGenome(genome.getId()); - } + // Setter provided for unit tests + public void setCurrentGenomeForTest(Genome genome) { this.currentGenome = genome; - if (genome != null) { - if (IGV.hasInstance()) { - IGV.getInstance().getSession().clearHistory(); - FrameManager.getDefaultFrame().setChromosomeName(genome.getHomeChromosome(), true); - IGVEventBus.getInstance().post(new GenomeChangeEvent(genome)); - } - } } + /** + * Load a genome by ID, which might be a file path or URL + * + * @param genomeId - ID for an IGV hosted genome, or file path or url + * @return boolean flag indicating success + * @throws IOException + */ + public boolean loadGenomeById(String genomeId) throws IOException { - public void loadGenomeById(String genomeId) throws IOException { final Genome currentGenome = getCurrentGenome(); if (currentGenome != null && genomeId.equals(currentGenome.getId())) { - return; // Already loaded + return false; } - String genomePath = null; + String genomePath; if (org.broad.igv.util.ParsingUtils.fileExists(genomeId)) { genomePath = genomeId; } else { GenomeListItem item = genomeListManager.getGenomeListItem(genomeId); if (item == null) { MessageUtils.showMessage("Could not locate genome with ID: " + genomeId); - return; + return false; } else { genomePath = item.getPath(); } } - - loadGenome(genomePath); // monitor[0]); - + return loadGenome(genomePath) != null; // monitor[0]); } /** * The main load method -- loads a genome from a file or url path. Note this is a long running operation and * should not be done on the Swing event thread as it will block the UI. + *

+ * NOTE: The member 'currentGenome' is set here as a side effect. * * @param genomePath * @return @@ -214,26 +212,8 @@ public Genome loadGenome(String genomePath) throws IOException { IGV.getInstance().resetSession(null); } - GenomeListItem genomeListItem = new GenomeListItem(newGenome.getDisplayName(), genomePath, newGenome.getId()); - final Set serverGenomeIDs = genomeListManager.getServerGenomeIDs(); - - boolean userDefined = !serverGenomeIDs.contains(newGenome.getId()); - genomeListManager.addGenomeItem(genomeListItem, userDefined); - - setCurrentGenome(newGenome); - - // hasInstance() test needed for unit tests - if (IGV.hasInstance()) { - IGV.getInstance().goToLocus(newGenome.getHomeChromosome()); // newGenome.getDefaultPos()); - loadGenomeAnnotations(newGenome); - IGV.getInstance().resetFrames(); - } + setCurrentGenome(genomePath, newGenome); - if (PreferencesManager.getPreferences().getAsBoolean(Constants.CIRC_VIEW_ENABLED) && CircularViewUtilities.ping()) { - CircularViewUtilities.changeGenome(newGenome); - } - - // log.warn("Genome loaded. id= " + newGenome.getId()); return currentGenome; } catch (SocketException e) { @@ -246,6 +226,37 @@ public Genome loadGenome(String genomePath) throws IOException { } } + public void setCurrentGenome(String genomePath, Genome newGenome) { + + GenomeListItem genomeListItem = new GenomeListItem(newGenome.getDisplayName(), genomePath, newGenome.getId()); + final Set serverGenomeIDs = genomeListManager.getServerGenomeIDs(); + + boolean userDefined = !serverGenomeIDs.contains(newGenome.getId()); + genomeListManager.addGenomeItem(genomeListItem, userDefined); + + this.currentGenome = newGenome; + + // hasInstance() check to filters unit test + if (IGV.hasInstance()) { + IGV.getInstance().goToLocus(newGenome.getHomeChromosome()); // newGenome.getDefaultPos()); + FrameManager.getDefaultFrame().setChromosomeName(newGenome.getHomeChromosome(), true); + loadGenomeAnnotations(newGenome); + IGV.getInstance().resetFrames(); + IGV.getInstance().getSession().clearHistory(); + + if(newGenome != Genome.nullGenome()) { + // This should only occur on startup failure + PreferencesManager.getPreferences().setLastGenome(newGenome.getId()); + } + + if (PreferencesManager.getPreferences().getAsBoolean(Constants.CIRC_VIEW_ENABLED) && CircularViewUtilities.ping()) { + CircularViewUtilities.changeGenome(newGenome); + } + + IGVEventBus.getInstance().post(new GenomeChangeEvent(newGenome)); + } + } + /** * Load and initialize the track objects from the genome's track resource locators. Does not add the tracks * to the IGV instance. @@ -458,7 +469,7 @@ private static void updateSequenceMapFile() { public void refreshHostedGenome(String genomeId) { Map itemMap = GenomeListManager.getInstance().getServerGenomeMap(); - if(itemMap.containsKey(genomeId)) { + if (itemMap.containsKey(genomeId)) { downloadGenome(itemMap.get(genomeId), false); } } diff --git a/src/main/java/org/broad/igv/feature/genome/GenomeServerException.java b/src/main/java/org/broad/igv/feature/genome/GenomeServerException.java deleted file mode 100644 index 30dc81f96d..0000000000 --- a/src/main/java/org/broad/igv/feature/genome/GenomeServerException.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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.feature.genome; - - -/** - * @author eflakes - */ -public class GenomeServerException extends GenomeException { - - public GenomeServerException(String message) { - super(message); - } - - public GenomeServerException(String message, Throwable e) { - super(message, e); - } -} \ No newline at end of file diff --git a/src/main/java/org/broad/igv/feature/genome/load/JsonGenomeLoader.java b/src/main/java/org/broad/igv/feature/genome/load/JsonGenomeLoader.java index bc06d52787..c4c87db379 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/JsonGenomeLoader.java +++ b/src/main/java/org/broad/igv/feature/genome/load/JsonGenomeLoader.java @@ -13,11 +13,13 @@ import org.broad.igv.logging.Logger; import org.broad.igv.track.TribbleFeatureSource; import org.broad.igv.util.FileUtils; +import org.broad.igv.util.HttpUtils; import org.broad.igv.util.ParsingUtils; import org.broad.igv.util.ResourceLocator; import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -35,17 +37,16 @@ public JsonGenomeLoader(String genomePath) { @Override public Genome loadGenome() throws IOException { - BufferedReader reader = null; - try { - reader = ParsingUtils.openBufferedReader(genomePath); - String jsonString = ParsingUtils.readContentsAsString(genomePath); + try (InputStream is = ParsingUtils.openInputStream(genomePath)){ + + String jsonString = ParsingUtils.readContentsFromStream(is); if (jsonString.contains("chromosomeOrder")) { jsonString = fixChromosomeOrder(jsonString); } - GenomeConfig genomeConfig = GenomeConfig.fromJson (jsonString); + GenomeConfig genomeConfig = GenomeConfig.fromJson(jsonString); fixPaths(genomeConfig); @@ -68,8 +69,6 @@ public Genome loadGenome() throws IOException { return genome; - } finally { - reader.close(); } } @@ -168,10 +167,9 @@ private void addToFeatureDB(List locators, Genome genome) { } private String stripQuotes(String str) { - if(str.startsWith("\"")) { + if (str.startsWith("\"")) { return str.substring(1, str.length() - 1); // Assume also ends with - } - else { + } else { return str; } } diff --git a/src/main/java/org/broad/igv/session/SessionWriter.java b/src/main/java/org/broad/igv/session/SessionWriter.java index b86f7c84a3..c60785742d 100644 --- a/src/main/java/org/broad/igv/session/SessionWriter.java +++ b/src/main/java/org/broad/igv/session/SessionWriter.java @@ -25,6 +25,7 @@ package org.broad.igv.session; +import org.broad.igv.feature.genome.Genome; import org.broad.igv.feature.genome.GenomeListItem; import org.broad.igv.logging.*; import org.broad.igv.feature.RegionOfInterest; @@ -426,13 +427,16 @@ public Collection getResourceLocatorSet() { if (currentTrackFileLocators != null) { // Filter data files that are included in genome annotations - List genomeResources = GenomeManager.getInstance().getCurrentGenome().getAnnotationResources(); - Set absoluteGenomeAnnotationPaths = genomeResources == null ? Collections.emptySet() : - genomeResources.stream().map(rl -> rl.getPath()).collect(Collectors.toSet()); - - for (ResourceLocator locator : currentTrackFileLocators) { - if (!absoluteGenomeAnnotationPaths.contains(locator.getPath())) { - locators.add(locator); + final Genome currentGenome = GenomeManager.getInstance().getCurrentGenome(); + if(currentGenome != null) { + List genomeResources = currentGenome.getAnnotationResources(); + Set absoluteGenomeAnnotationPaths = genomeResources == null ? Collections.emptySet() : + genomeResources.stream().map(rl -> rl.getPath()).collect(Collectors.toSet()); + + for (ResourceLocator locator : currentTrackFileLocators) { + if (!absoluteGenomeAnnotationPaths.contains(locator.getPath())) { + locators.add(locator); + } } } } diff --git a/src/main/java/org/broad/igv/track/SequenceTrack.java b/src/main/java/org/broad/igv/track/SequenceTrack.java index 4d11c7d747..55db8d7eb4 100644 --- a/src/main/java/org/broad/igv/track/SequenceTrack.java +++ b/src/main/java/org/broad/igv/track/SequenceTrack.java @@ -236,7 +236,12 @@ public void load(ReferenceFrame referenceFrame) { end = Math.min(end + w / 2 + 2, chromosomeLength); Genome genome = currentGenome; - String sequence = new String(genome.getSequence(chr, start, end)); + byte [] seqBytes = genome.getSequence(chr, start, end); + if(seqBytes == null) { + return; + + } + String sequence = new String(seqBytes); int mod = start % 3; int n1 = normalize3(3 - mod); diff --git a/src/main/java/org/broad/igv/ui/IGV.java b/src/main/java/org/broad/igv/ui/IGV.java index 63344ddc6b..a9ad28bf21 100644 --- a/src/main/java/org/broad/igv/ui/IGV.java +++ b/src/main/java/org/broad/igv/ui/IGV.java @@ -62,7 +62,6 @@ import org.broad.igv.session.autosave.SessionAutosaveManager; import org.broad.igv.track.*; import org.broad.igv.ui.WaitCursorManager.CursorToken; -import org.broad.igv.ui.commandbar.GenomeListManager; import org.broad.igv.ui.dnd.GhostGlassPane; import org.broad.igv.ui.panel.*; import org.broad.igv.ui.util.*; @@ -1926,8 +1925,7 @@ public void run() { if (igvArgs.getGenomeId() != null) { String genomeId = igvArgs.getGenomeId(); try { - GenomeManager.getInstance().loadGenomeById(genomeId); - genomeLoaded = true; + genomeLoaded = GenomeManager.getInstance().loadGenomeById(genomeId); } catch (IOException e) { MessageUtils.showErrorMessage("Error loading genome: " + genomeId, e); log.error("Error loading genome: " + genomeId, e); @@ -1937,26 +1935,30 @@ public void run() { if (igvArgs.getSessionFile() == null && !loadAutosave && !genomeLoaded) { String genomeId = preferences.getDefaultGenome(); try { - GenomeManager.getInstance().loadGenomeById(genomeId); - genomeLoaded = true; + genomeLoaded = GenomeManager.getInstance().loadGenomeById(genomeId); } catch (Exception e) { MessageUtils.showErrorMessage("Error loading genome " + genomeId + "
" + e.getMessage(), e); genomeLoaded = false; - } + } if (!genomeLoaded) { - // If the error is with the default genome try refreshing it. - if(genomeId.equals(GenomeListManager.DEFAULT_GENOME.getId())) { - GenomeManager.getInstance().refreshHostedGenome(genomeId); - } + Genome genome = Genome.nullGenome(); + GenomeManager.getInstance().setCurrentGenome("", genome); - genomeId = GenomeListManager.DEFAULT_GENOME.getId(); - try { - GenomeManager.getInstance().loadGenomeById(genomeId); - } catch (IOException e) { - MessageUtils.showErrorMessage("Error loading genome: " + genomeId, e); - log.error("Error loading genome: " + genomeId, e); - } + + //GenomeManager.getInstance().setCurrentGenome(Genome.NoneGenome()); + // If the error is with the default genome try refreshing it. +// if(genomeId.equals(GenomeListManager.DEFAULT_GENOME.getId())) { +// GenomeManager.getInstance().refreshHostedGenome(genomeId); +// } +// +// genomeId = GenomeListManager.DEFAULT_GENOME.getId(); +// try { +// GenomeManager.getInstance().loadGenomeById(genomeId); +// } catch (IOException e) { +// MessageUtils.showErrorMessage("Error loading genome: " + genomeId, e); +// log.error("Error loading genome: " + genomeId, e); +// } } } diff --git a/src/main/java/org/broad/igv/ui/commandbar/GenomeComboBox.java b/src/main/java/org/broad/igv/ui/commandbar/GenomeComboBox.java index 9bb1fedafe..9a2dc7839e 100644 --- a/src/main/java/org/broad/igv/ui/commandbar/GenomeComboBox.java +++ b/src/main/java/org/broad/igv/ui/commandbar/GenomeComboBox.java @@ -6,7 +6,6 @@ import org.broad.igv.event.IGVEventBus; import org.broad.igv.feature.genome.GenomeListItem; import org.broad.igv.feature.genome.GenomeManager; -import org.broad.igv.feature.genome.GenomeServerException; import org.broad.igv.ui.IGV; import org.broad.igv.ui.UIConstants; import org.broad.igv.ui.util.MessageUtils; @@ -21,7 +20,6 @@ import java.io.File; import java.io.IOException; import java.util.*; -import java.util.List; /** * Created by jrobinso on 7/6/17. @@ -96,29 +94,24 @@ private void loadGenomeListItem(final GenomeListItem genomeListItem) { return; } - final Runnable runnable = new Runnable() { + final Runnable runnable = () -> { - public void run() { + if (genomeListItem != null && genomeListItem.getPath() != null) { - if (genomeListItem != null && genomeListItem.getPath() != null) { - - //log.warn("Loading " + genomeListItem.getId()); - - //User selected "more", pull up dialog and revert combo box - if (genomeListItem == GenomeListItem.DOWNLOAD_ITEM) { - loadGenomeFromServer(); - return; - } + if (genomeListItem == GenomeListItem.DOWNLOAD_ITEM) { + loadGenomeFromServer(); + } else { + boolean success = false; + Exception error = null; try { - GenomeManager.getInstance().loadGenomeById(genomeListItem.getId()); - } catch (GenomeServerException e) { - log.error("Error loading genome: " + genomeListItem.getId() + " " + genomeListItem.getPath(), e); - JOptionPane.showMessageDialog( - IGV.getInstance().getMainFrame(), - "Error loading genome: " + genomeListItem.getDisplayableName()); + success = GenomeManager.getInstance().loadGenomeById(genomeListItem.getId()); } catch (Exception e) { log.error(e); + error = e; + } + + if (!success) { int choice = JOptionPane.showConfirmDialog( IGV.getInstance().getMainFrame(), "The genome [" + genomeListItem.getId() + "] could not be read. Would you like to remove the selected entry?", @@ -127,12 +120,9 @@ public void run() { if (choice == JOptionPane.OK_OPTION) { GenomeListManager.getInstance().removeGenomeListItem(genomeListItem); refreshGenomeListComboBox(); - log.error("Error initializing genome", e); + log.error("Error initializing genome", error); } - } finally { - } - } } }; @@ -231,27 +221,27 @@ public static void loadGenomeFromServer() { IGVEventBus.getInstance().post(new GenomeResetEvent()); } else { GenomeListItem selectedValue = dialog.getSelectedValue(); - if (selectedValue != null) { - - try { - GenomeManager.getInstance().loadGenome(selectedValue.getPath()); + if (selectedValue != null) { - GenomeListManager.getInstance().addServerGenomeItem(selectedValue); + try { + GenomeManager.getInstance().loadGenome(selectedValue.getPath()); - GenomeListManager.getInstance().removeUserDefinedGenome(selectedValue.getId()); + GenomeListManager.getInstance().addServerGenomeItem(selectedValue); - // If this is a .json genome, attempt to remove existing .genome files - if(selectedValue.getPath().endsWith(".json")) { - removeDotGenomeFile(selectedValue.getId()); - } + GenomeListManager.getInstance().removeUserDefinedGenome(selectedValue.getId()); - } catch (IOException e) { - GenomeListManager.getInstance().removeGenomeListItem(selectedValue); - MessageUtils.showErrorMessage("Error loading genome " + selectedValue.getDisplayableName(), e); - log.error("Error loading genome " + selectedValue.getDisplayableName(), e); + // If this is a .json genome, attempt to remove existing .genome files + if (selectedValue.getPath().endsWith(".json")) { + removeDotGenomeFile(selectedValue.getId()); } + } catch (IOException e) { + GenomeListManager.getInstance().removeGenomeListItem(selectedValue); + MessageUtils.showErrorMessage("Error loading genome " + selectedValue.getDisplayableName(), e); + log.error("Error loading genome " + selectedValue.getDisplayableName(), e); } + + } } }; @@ -265,7 +255,7 @@ public static void loadGenomeFromServer() { public static void removeDotGenomeFile(String id) { try { File dotGenomeFile = new File(DirectoryManager.getGenomeCacheDirectory(), id + ".genome"); - if(dotGenomeFile.exists()) { + if (dotGenomeFile.exists()) { dotGenomeFile.delete(); } } catch (Exception e) { diff --git a/src/main/java/org/broad/igv/ui/commandbar/IGVCommandBar.java b/src/main/java/org/broad/igv/ui/commandbar/IGVCommandBar.java index 771c3cb89b..8c988de069 100644 --- a/src/main/java/org/broad/igv/ui/commandbar/IGVCommandBar.java +++ b/src/main/java/org/broad/igv/ui/commandbar/IGVCommandBar.java @@ -180,24 +180,30 @@ public void selectGenome(String genomeId) { public void updateCurrentCoordinates() { if (IGV.hasInstance()) { - String p = ""; - ReferenceFrame defaultFrame = FrameManager.getDefaultFrame(); - final String chrName = defaultFrame.getChrName(); - if (!Globals.CHR_ALL.equals(chrName) && !FrameManager.isGeneListMode()) { - p = defaultFrame.getFormattedLocusString(); - } - final String position = p; - final History history = IGV.getInstance().getSession().getHistory(); - UIUtilities.invokeOnEventThread(new Runnable() { - public void run() { + if(GenomeManager.getInstance().getCurrentGenome() == Genome.nullGenome()) { + UIUtilities.invokeOnEventThread(() -> { + searchTextField.setText(""); + }); + + } else { + String p = ""; + ReferenceFrame defaultFrame = FrameManager.getDefaultFrame(); + final String chrName = defaultFrame.getChrName(); + if (!Globals.CHR_ALL.equals(chrName) && !FrameManager.isGeneListMode()) { + p = defaultFrame.getFormattedLocusString(); + } + final String position = p; + final History history = IGV.getInstance().getSession().getHistory(); + + UIUtilities.invokeOnEventThread(() -> { searchTextField.setText(position); forwardButton.setEnabled(history.canGoForward()); backButton.setEnabled(history.canGoBack()); roiToggleButton.setEnabled(!Globals.CHR_ALL.equals(chrName)); zoomControl.setEnabled(!Globals.CHR_ALL.equals(chrName) && !FrameManager.isGeneListMode()); - } - }); + }); + } } } diff --git a/src/main/java/org/broad/igv/ui/panel/ReferenceFrame.java b/src/main/java/org/broad/igv/ui/panel/ReferenceFrame.java index 8e315fdd9f..2b070b06a1 100644 --- a/src/main/java/org/broad/igv/ui/panel/ReferenceFrame.java +++ b/src/main/java/org/broad/igv/ui/panel/ReferenceFrame.java @@ -641,13 +641,14 @@ public int getMidpoint() { */ public String getFormattedLocusString() { -// if (zoom == 0) { -// return getGenome().getChromosomeDisplayName(getChrName()); -// } else { Range range = getCurrentRange(); - String c = getGenome().getChromosomeDisplayName(range.getChr()); - return Locus.getFormattedLocusString(c, range.getStart(), range.getEnd()); - // } + final Genome genome = getGenome(); + if(genome != null) { + String c = genome.getChromosomeDisplayName(range.getChr()); + return Locus.getFormattedLocusString(c, range.getStart(), range.getEnd()); + } else { + return ""; + } } public Range getCurrentRange() { diff --git a/src/main/java/org/broad/igv/ui/panel/ZoomSliderPanel.java b/src/main/java/org/broad/igv/ui/panel/ZoomSliderPanel.java index d75964f294..52ff494393 100644 --- a/src/main/java/org/broad/igv/ui/panel/ZoomSliderPanel.java +++ b/src/main/java/org/broad/igv/ui/panel/ZoomSliderPanel.java @@ -106,7 +106,7 @@ public ZoomSliderPanel(ReferenceFrame referenceFrame) { } private void updateTickCount() { - int tmp = getReferenceFrame().getMaxZoom() + 1; + int tmp = Math.max(0, getReferenceFrame().getMaxZoom() + 1); if (tmp != numZoomLevels) { numZoomLevels = tmp; zoomLevelRects = new Rectangle[numZoomLevels]; diff --git a/src/main/java/org/broad/igv/util/ParsingUtils.java b/src/main/java/org/broad/igv/util/ParsingUtils.java index 93bd4f6c34..06e4685aa3 100644 --- a/src/main/java/org/broad/igv/util/ParsingUtils.java +++ b/src/main/java/org/broad/igv/util/ParsingUtils.java @@ -147,10 +147,6 @@ public static String readContentsFromStream(InputStream is) throws IOException { return new String(bytes, "UTF-8"); } - public static String readContentsAsString(String path) throws IOException { - return readContentsFromStream(openInputStream(path)); - } - /** * Parse the string and return the result as an integer. This method supports scientific notation for integers, * which Integer.parseInt() does not. diff --git a/src/test/java/org/broad/igv/AbstractHeadlessTest.java b/src/test/java/org/broad/igv/AbstractHeadlessTest.java index 12356a83e6..cf93f3ca90 100644 --- a/src/test/java/org/broad/igv/AbstractHeadlessTest.java +++ b/src/test/java/org/broad/igv/AbstractHeadlessTest.java @@ -58,14 +58,14 @@ public class AbstractHeadlessTest { @BeforeClass public static void setUpClass() throws Exception { setUpHeadless(); - GenomeManager.getInstance().setCurrentGenome(null); + GenomeManager.getInstance().setCurrentGenomeForTest(null); genome = TestUtils.loadGenome(); } @AfterClass public static void tearDownClass() throws Exception { TestUtils.clearOutputDir(); - GenomeManager.getInstance().setCurrentGenome(null); + GenomeManager.getInstance().setCurrentGenomeForTest(null); } @Before From e658784e7dd4516cc4493bb481e6cbc3a5fdaf6c Mon Sep 17 00:00:00 2001 From: Louis Bergelson Date: Fri, 13 Sep 2024 13:24:58 -0400 Subject: [PATCH 045/130] Update to Java 21 and gradle 8.10.1 (#1568) * Update to Java 21 and gradle 8.10.1 * Move from Java 17 -> Java 21 * Update from gradle 8.0.2 -> 8.10.1 * Replace some deprecated gradle code with newer styles * Something changed in the module resolution with this update which caused us to not be able to resolve automatic modules. * Introduced a new gradle plugin which fixes the problem and removed the manual module path manipulation we were doing. * update github actions --- .github/workflows/gradle.yml | 18 +--- README.md | 2 +- build.gradle | 109 +++++++++++++---------- gradle/wrapper/gradle-wrapper.jar | Bin 61574 -> 61608 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 4 +- 6 files changed, 71 insertions(+), 64 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index be85ad7178..767efaa8fb 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -22,10 +22,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v4 with: - java-version: '17' + java-version: '21' distribution: 'temurin' # Configure Gradle for optimal use in GitHub Actions, including caching of downloaded dependencies. @@ -36,16 +36,6 @@ jobs: - name: Test with Gradle Wrapper run: ./gradlew test - # NOTE: The Gradle Wrapper is the default and recommended way to run Gradle (https://docs.gradle.org/current/userguide/gradle_wrapper.html). - # If your project does not have the Gradle Wrapper configured, you can use the following configuration to run Gradle with a specified version. - # - # - name: Setup Gradle - # uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0 - # with: - # gradle-version: '8.5' - # - # - name: Build with Gradle 8.5 - # run: gradle build dependency-submission: @@ -55,10 +45,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v4 with: - java-version: '17' + java-version: '21' distribution: 'temurin' # Generates and submits a dependency graph, enabling Dependabot Alerts for all project dependencies. diff --git a/README.md b/README.md index 7352385162..ebb0415838 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ at [http://software.broadinstitute.org/software/igv/download](http://software.br Builds are executed from the IGV project directory. Files will be created in the 'build' subdirectory. -IGV requires **Java 17** to build and run. Later versions of Java should work but we build and test on **Java 17**. +IGV requires **Java 21** to build and run. Later versions of Java should work but we build and test on **Java 21**. NOTE: If on a Windows platform use ```./gradlew.bat'``` in the instructions below diff --git a/build.gradle b/build.gradle index 089ce3e033..1508dffb43 100644 --- a/build.gradle +++ b/build.gradle @@ -21,29 +21,34 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -apply plugin: 'java' -apply plugin: 'maven-publish' -apply plugin: 'application' - -import org.apache.tools.ant.filters.ReplaceTokens - -mainClassName = 'org.broad.igv.ui.Main' -ext.moduleName = 'org.igv' - buildscript { repositories { mavenCentral() } } +plugins { + id('java') + id('maven-publish') + id('application') + id("org.gradlex.extra-java-module-info") version("1.8") +} + repositories { mavenCentral() mavenLocal() } +import org.apache.tools.ant.filters.ReplaceTokens + +application { + mainClass = 'org.broad.igv.ui.Main' + mainModule = 'org.igv' +} + java { toolchain { - languageVersion = JavaLanguageVersion.of(17) + languageVersion = JavaLanguageVersion.of(21) } } @@ -56,6 +61,10 @@ sourceSets { } } +extraJavaModuleInfo { + deriveAutomaticModuleNamesFromFileNames.set(true) +} + configurations { implementation { exclude group: 'com.google.code.findbugs', module: 'annotations' @@ -130,12 +139,16 @@ dependencies { ) testImplementation( - [group: 'junit', name: 'junit', version: '4.13.1'], + [group: 'junit', name: 'junit', version: '4.13.2'], [group: 'com.sparkjava', name: 'spark-core', version: '2.9.4'], [group: 'org.glassfish.jersey.core', name: 'jersey-common', version: '2.34'] ) testRuntimeOnly( - [group: 'org.junit.vintage', name:'junit-vintage-engine', version:'5.8.2'] + [group: 'org.junit.vintage', name:'junit-vintage-engine', version:'5.8.2'] , + //required by gradle 9+ + //see https://docs.gradle.org/8.10.1/userguide/upgrading_version_8.html#test_framework_implementation_dependencies + [group: 'org.junit.platform', name:'junit-platform-launcher'] + ) } @@ -160,7 +173,7 @@ jar { "Permissions": "all-permissions", "Application-Name": "IGV", "Built-By": System.getProperty('user.name'), - "Main-Class": mainClassName, + "Main-Class": application.mainClass, ) } } @@ -169,18 +182,9 @@ tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' } -compileJava { - inputs.property("moduleName", moduleName) - doFirst { - options.compilerArgs = [ - '--module-path', classpath.asPath, - ] - classpath = files() - } -} - -tasks.withType(Test) { - systemProperties = System.getProperties() +tasks.withType(Test).configureEach { + systemProperties.clear() + systemProperties.putAll( System.getProperties() ) systemProperties['java.awt.headless'] = 'true' systemProperties['make.fail'] = 'false' systemProperties['include.longrunning'] = 'false' @@ -191,7 +195,8 @@ tasks.withType(Test) { useJUnitPlatform() } -task createDist(type: Copy, dependsOn: jar) { +tasks.register('createDist', Copy) { + dependsOn jar from("web/IGV_64.png") from("scripts") { include '*.bat' @@ -204,14 +209,14 @@ task createDist(type: Copy, dependsOn: jar) { with copySpec { from("${buildDir}/libs") from("lib") { - include '*.jar' + include '*.jar' } into "lib" } // Copies all Maven-fetched dependency jars with copySpec { from configurations.runtimeClasspath { - exclude '**/*log4j*.jar' + exclude '**/*log4j*.jar' } into "lib" duplicatesStrategy DuplicatesStrategy.EXCLUDE @@ -226,14 +231,16 @@ tasks.distTar.enabled = false tasks.startScripts.enabled = false // Create the platform agnostic zip distribution -task createDistZip(type: Zip, dependsOn: createDist) { +tasks.register('createDistZip', Zip) { + dependsOn createDist archiveFileName = "IGV_${version}.zip" from("${buildDir}/IGV-dist") into "IGV_${version}" } // Create a linux distribution for IGV , excluding igvtools. Basically identical to generic dist without window and mac scripts -task createLinuxDistZip(type: Zip, dependsOn: createDist) { +tasks.register('createLinuxDistZip', Zip) { + dependsOn createDist archiveFileName = "IGV_Linux_${version}.zip" from("${buildDir}/IGV-dist") { exclude "*.bat" @@ -245,9 +252,10 @@ task createLinuxDistZip(type: Zip, dependsOn: createDist) { } // Create a linux distrubtion with bundled Java -task createLinuxWithJavaDistZip(type: Zip, dependsOn: createDist) { +tasks.register('createLinuxWithJavaDistZip', Zip) { + dependsOn createDist archiveFileName = "IGV_Linux_${version}_WithJava.zip" - with copySpec { from jdkBundleLinux into "jdk-17" } + with copySpec { from jdkBundleLinux into "jdk-21" } from("${buildDir}/IGV-dist") { exclude "*.bat" exclude "*.command" @@ -262,7 +270,8 @@ task createLinuxWithJavaDistZip(type: Zip, dependsOn: createDist) { } } -task createMacDistZip(type: Zip, dependsOn: createDist) { +tasks.register('createMacDistZip', Zip) { + dependsOn createDist archiveFileName = "IGV_MacApp_${version}.zip" from("${buildDir}/IGV-dist") { exclude "*.bat" @@ -274,9 +283,10 @@ task createMacDistZip(type: Zip, dependsOn: createDist) { into "IGV_MacApp_${version}" } -task createMacWithJavaDistZip(type: Zip, dependsOn: createDist) { +tasks.register('createMacWithJavaDistZip', Zip) { + dependsOn createDist archiveFileName = "IGV_MacApp_${version}_WithJava.zip" - with copySpec { from jdkBundleMac into "jdk-17" } + with copySpec { from jdkBundleMac into "jdk-21" } from("${buildDir}/IGV-dist") { exclude "*.bat" exclude "*_hidpi*" @@ -291,7 +301,8 @@ task createMacWithJavaDistZip(type: Zip, dependsOn: createDist) { } } -task createMacAppDist(type: Copy, dependsOn: createDist) { +tasks.register('createMacAppDist', Copy) { + dependsOn createDist with copySpec { from("scripts/mac.app") { exclude "Contents/Info.plist.template" @@ -324,8 +335,9 @@ task createMacAppDist(type: Copy, dependsOn: createDist) { } } -task createMacAppDistZip(type: Zip, dependsOn: createMacAppDist) { - archiveFileName = "IGV_MacApp_${version}.zip" +tasks.register('createMacAppDistZip', Zip) { + dependsOn createMacAppDist + archiveFileName = "IGV_MacApp_${version}.zip" from("${buildDir}/IGV-MacApp-dist") doLast { @@ -333,9 +345,10 @@ task createMacAppDistZip(type: Zip, dependsOn: createMacAppDist) { } } -task createMacAppWithJavaDistZip(type: Zip, dependsOn: createMacAppDist) { +tasks.register('createMacAppWithJavaDistZip', Zip) { + dependsOn createMacAppDist archiveFileName = "IGV_MacApp_${version}_WithJava.zip" - with copySpec { from jdkBundleMac into "IGV_${version}.app/Contents/jdk-17" } + with copySpec { from jdkBundleMac into "IGV_${version}.app/Contents/jdk-21" } from("${buildDir}/IGV-MacApp-dist") doLast { @@ -368,8 +381,9 @@ task createWinDist(type: Copy, dependsOn: createDist) { into "${buildDir}/IGV-WinExe-dist" } -task createWinWithJavaDist(type: Copy, dependsOn: createWinDist) { - with copySpec { from jdkBundleWindows into "IGV_${version}/jdk-17" } +tasks.register('createWinWithJavaDist', Copy) { + dependsOn createWinDist + with copySpec { from jdkBundleWindows into "IGV_${version}/jdk-21" } with copySpec { from("${buildDir}/IGV-WinExe-dist/IGV_${version}") { exclude 'installer.nsi' } into "IGV_${version}" @@ -388,7 +402,8 @@ task createWinWithJavaDist(type: Copy, dependsOn: createWinDist) { } } -task createWinExeDist(type: Exec, dependsOn: createWinDist) { +tasks.register('createWinExeDist', Exec) { + dependsOn createWinDist commandLine(makensisCommand, "-O${buildDir}/tmp/nsis-WithJava-build.log", "${buildDir}/IGV-WinExe-dist/installer.nsi") doLast { @@ -398,7 +413,8 @@ task createWinExeDist(type: Exec, dependsOn: createWinDist) { } } -task createWinWithJavaExeDist(type: Exec, dependsOn: createWinWithJavaDist) { +tasks.register('createWinWithJavaExeDist', Exec) { + dependsOn createWinWithJavaDist commandLine(makensisCommand, "-O${buildDir}/tmp/nsis-build.log", "${buildDir}/IGV-WinExe-WithJava-dist/installer.nsi") doLast { @@ -411,7 +427,8 @@ task createWinWithJavaExeDist(type: Exec, dependsOn: createWinWithJavaDist) { } } -task signWinExeDist(type: Exec, dependsOn: createWinExeDist) { +tasks.register('signWinExeDist', Exec) { + dependsOn createWinExeDist standardInput = new ByteArrayInputStream(keyPassword.getBytes()); commandLine(signcodeCommand, "-spc", spcFile, "-v", pvkFile, "-a", "sha512", "-\$", "commercial", "-n", "IGV ${version}", "-i", "http://www.igv.org/", @@ -450,7 +467,7 @@ task fullJar(type: Jar, dependsOn: jar) { "Permissions": "all-permissions", "Application-Name": "IGV", "Built-By": System.getProperty('user.name'), - "Main-Class": mainClassName, + "Main-Class": application.mainClass, "Class-Path": configurations.runtimeClasspath.collect { it.getName() }.join(' ') ) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 943f0cbfa754578e88a3dae77fce6e3dea56edbf..ccebba7710deaf9f98673a68957ea02138b60d0a 100644 GIT binary patch delta 5094 zcmZu#c|6qH|DG9RA4`noBZNWrC2N)tSqjO%%aX0^O4dPAB*iC6_9R<`apl^#h-_oY z)(k_0v8Fxp{fyi9-uwN%e)GpU&v~BrS>~KG^PF=MNmQjIDr&QHR7f-kM{%U_u*1=5 zGC}ae5(^Rrg9QY8$x^}oiJ0d2O9YW{J~$dD1ovlvh&0B4L)!4S=z;Hac>K{#9q9cKq;>>BtKo1!+gw`yqE zSK8x^jC|B!qmSW#uyb@T^CkB9qRd{N3V-rEi}AEgoU_J27lw_0X`}c0&m9JhxM;RK z54_gdZ(u?R5`B3}NeVal2NTHqlktM`2eTF28%6BZCWW$-shf0l-BOVSm)hU58MTPy zDcY-5777j;ccU!Yba8wH=X6OdPJ8O5Kp^3gUNo>!b=xb6T2F&LiC2eBJj8KuLPW!4 zw3V^NnAKZm^D?tmliCvzi>UtoDH%V#%SM0d*NS+m%4}qO<)M1E{OpQ(v&ZNc`vdi| zEGlVi$Dgxy1p6+k0qGLQt(JwxZxLCZ4>wJ=sb0v%Ki?*+!ic_2exumn{%Co|| z-axdK#RUC;P|vqbe?L`K!j;sUo=uuR_#ZkRvBf%Txo6{OL&I(?dz?47Z(DcX3KTw> zGY%A=kX;fBkq$F^sX|-)1Qkg##+n-Ci{qJVPj@P?l_1Y`nD^v>fZ3HMX%(4p-TlD(>yWwJij!6Jw}l7h>CIm@Ou5B@$Wy`Ky*814%Mdi1GfG1zDG9NogaoVHHr4gannv4?w6g&10!j=lKM zFW;@=Z0}vAPAxA=R4)|`J??*$|Fh`5=ks*V7TapX`+=4n*{aXxRhh-EGX_Xrzjb4r zn0vO7Cc~wtyeM_8{**~9y7>+}1JV8Buhg%*hy|PUc#!vw#W(HFTL|BpM)U0>JxG6S zLnqn1!0++RyyJ>5VU<4mDv8>Q#{EtgS3mj7Hx}Zkr0tz1}h8Kn6q`MiwC z{Y#;D!-ndlImST(C@(*i5f0U(jD29G7g#nkiPX zki6M$QYX_fNH=E4_eg9*FFZ3wF9YAKC}CP89Kl(GNS(Ag994)0$OL4-fj_1EdR}ARB#-vP_$bWF`Qk58+ z4Jq*-YkcmCuo9U%oxGeYe7Be=?n}pX+x>ob(8oPLDUPiIryT8v*N4@0{s_VYALi;lzj19ivLJKaXt7~UfU|mu9zjbhPnIhG2`uI34urWWA9IO{ z_1zJ)lwSs{qt3*UnD}3qB^kcRZ?``>IDn>qp8L96bRaZH)Zl`!neewt(wjSk1i#zf zb8_{x_{WRBm9+0CF4+nE)NRe6K8d|wOWN)&-3jCDiK5mj>77=s+TonlH5j`nb@rB5 z5NX?Z1dk`E#$BF{`(D>zISrMo4&}^wmUIyYL-$PWmEEfEn-U0tx_vy$H6|+ zi{ytv2@JXBsot|%I5s74>W1K{-cvj0BYdNiRJz*&jrV9>ZXYZhEMULcM=fCmxkN&l zEoi=)b)Vazc5TQC&Q$oEZETy@!`Gnj`qoXl7mcwdY@3a-!SpS2Mau|uK#++@>H8QC zr2ld8;<_8We%@E?S=E?=e9c$BL^9X?bj*4W;<+B&OOe+3{<`6~*fC(=`TO>o^A(Y! zA`Qc1ky?*6xjVfR?ugE~oY`Gtzhw^{Z@E6vZ`mMRAp>Odpa!m zzWmtjT|Lj^qiZMfj%%un-o$Eu>*v12qF{$kCKai^?DF=$^tfyV%m9;W@pm-BZn_6b z{jsXY3!U`%9hzk6n7YyHY%48NhjI6jjuUn?Xfxe0`ARD_Q+T_QBZ{ zUK@!63_Wr`%9q_rh`N4=J=m;v>T{Y=ZLKN^m?(KZQ2J%|3`hV0iogMHJ} zY6&-nXirq$Yhh*CHY&Qf*b@@>LPTMf z(cMorwW?M11RN{H#~ApKT)F!;R#fBHahZGhmy>Sox`rk>>q&Y)RG$-QwH$_TWk^hS zTq2TC+D-cB21|$g4D=@T`-ATtJ?C=aXS4Q}^`~XjiIRszCB^cvW0OHe5;e~9D%D10 zl4yP4O=s-~HbL7*4>#W52eiG7*^Hi)?@-#*7C^X5@kGwK+paI>_a2qxtW zU=xV7>QQROWQqVfPcJ$4GSx`Y23Z&qnS?N;%mjHL*EVg3pBT{V7bQUI60jtBTS?i~ zycZ4xqJ<*3FSC6_^*6f)N|sgB5Bep(^%)$=0cczl>j&n~KR!7WC|3;Zoh_^GuOzRP zo2Hxf50w9?_4Qe368fZ0=J|fR*jO_EwFB1I^g~i)roB|KWKf49-)!N%Ggb%w=kB8)(+_%kE~G!(73aF=yCmM3Cfb9lV$G!b zoDIxqY{dH>`SILGHEJwq%rwh46_i`wkZS-NY95qdNE)O*y^+k#JlTEij8NT(Y_J!W zFd+YFoZB|auOz~A@A{V*c)o7E(a=wHvb@8g5PnVJ&7D+Fp8ABV z5`&LD-<$jPy{-y*V^SqM)9!#_Pj2-x{m$z+9Z*o|JTBGgXYYVM;g|VbitDUfnVn$o zO)6?CZcDklDoODzj+ti@i#WcqPoZ!|IPB98LW!$-p+a4xBVM@%GEGZKmNjQMhh)zv z7D){Gpe-Dv=~>c9f|1vANF&boD=Nb1Dv>4~eD636Lldh?#zD5{6JlcR_b*C_Enw&~ z5l2(w(`{+01xb1FCRfD2ap$u(h1U1B6e&8tQrnC}Cy0GR=i^Uue26Rc6Dx}!4#K*0 zaxt`a+px7-Z!^(U1WN2#kdN#OeR|2z+C@b@w+L67VEi&ZpAdg+8`HJT=wIMJqibhT ztb3PFzsq&7jzQuod3xp7uL?h-7rYao&0MiT_Bux;U*N#ebGv92o(jM2?`1!N2W_M* zeo9$%hEtIy;=`8z1c|kL&ZPn0y`N)i$Y1R9>K!el{moiy)014448YC#9=K zwO3weN|8!`5bU_#f(+ZrVd*9`7Uw?!q?yo&7sk&DJ;#-^tcCtqt5*A(V;&LdHq7Hg zI6sC@!ly9p$^@v&XDsgIuv;9#w^!C1n5+10-tEw~ZdO1kqMDYyDl!5__o}f3hYe2M zCeO)~m&&=JZn%cVH3HzPlcE`9^@``2u+!Y}Remn)DLMHc-h5A9ATgs;7F7=u2=vBlDRbjeYvyNby=TvpI{5nb2@J_YTEEEj4q<@zaGSC_i&xxD!6)d zG{1??({Ma<=Wd4JL%bnEXoBOU_0bbNy3p%mFrMW>#c zzPEvryBevZVUvT^2P&Zobk#9j>vSIW_t?AHy>(^x-Bx~(mvNYb_%$ZFg(s5~oka+Kp(GU68I$h(Vq|fZ zC_u1FM|S)=ldt#5q>&p4r%%p)*7|Rf0}B#-FwHDTo*|P6HB_rz%R;{==hpl#xTt@VLdSrrf~g^ z`IA8ZV1b`UazYpnkn28h&U)$(gdZ*f{n`&kH%Oy54&Z;ebjlh4x?JmnjFAALu}EG} zfGmQ$5vEMJMH`a=+*src#dWK&N1^LFxK9Sa#q_rja$JWra09we<2oL9Q9Sx)?kZFW z$jhOFGE~VcihYlkaZv8?uA7v$*}?2h6i%Qmgc4n~3E(O_`YCRGy~}`NFaj@(?Wz;GS_?T+RqU{S)eD1j$1Gr;C^m z7zDK=xaJ^6``=#Y-2ssNfdRqh0ntJrutGV5Nv&WI%3k1wmD5n+0aRe{0k^!>LFReN zx1g*E>nbyx03KU~UT6->+rG%(owLF=beJxK&a0F;ie1GZ^eKg-VEZb&=s&ajKS#6w zjvC6J#?b|U_(%@uq$c#Q@V_me0S1%)pKz9--{EKwyM}_gOj*Og-NEWLDF_oFtPjG; zXCZ7%#=s}RKr&_5RFN@=H(015AGl4XRN9Bc51`;WWt%vzQvzexDI2BZ@xP~^2$I&7 zA(ndsgLsmA*su8p-~IS q+ZJUZM}`4#Zi@l2F-#HCw*??ha2ta#9s8?H3%YId(*zJG6aF78h1yF1 delta 5107 zcmY*d1zc0@|J{HQlai7V5+f#EN-H%&UP4MFm6QgFfuJK4DG4u#ARsbQL4i>MB1q|w zmWd#pqd~BR-yN@ieE-|$^W1aKIZtf&-p_fyw{(Uwc7_sWYDh^12cY!qXvcPQ!qF;q@b0nYU7 zP&ht}K7j%}P%%|ffm;4F0^i3P0R`a!2wm89L5P3Kfu;tTZJre<{N5}AzsH+E3DS`Q zJLIl`LRMf`JOTBLf(;IV(9(h{(}dXK!cPoSLm(o@fz8vRz}6fOw%3}3VYOsCczLF` za2RTsCWa2sS-uw(6|HLJg)Xf@S8#|+(Z5Y)ER+v+8;btfB3&9sWH6<=U}0)o-jIts zsi?Nko;No&JyZI%@1G&zsG5kKo^Zd7rk_9VIUao9;fC~nv(T0F&Af0&Rp`?x94EIS zUBPyBe5R5#okNiB1Xe--q4|hPyGzhJ?Lurt#Ci09BQ+}rlHpBhm;EmfLw{EbCz)sg zgseAE#f$met1jo;`Z6ihk?O1be3aa$IGV69{nzagziA!M*~E5lMc(Sp+NGm2IUjmn zql((DU9QP~Tn1pt6L`}|$Na-v(P+Zg&?6bAN@2u%KiB*Gmf}Z)R zMENRJgjKMqVbMpzPO{`!J~2Jyu7&xXnTDW?V?IJgy+-35q1)-J8T**?@_-2H`%X+6f5 zIRv`uLp&*?g7L~6+3O*saXT~gWsmhF*FNKw4X$29ePKi02G*)ysenhHv{u9-y?_do ztT(Cu04pk>51n}zu~=wgToY5Cx|MTlNw}GR>+`|6CAhQn=bh@S<7N)`w};;KTywDU z=QWO@RBj$WKOXSgCWg{BD`xl&DS!G}`Mm3$)=%3jzO_C+s+mfTFH5JL>}*(JKs@MqX|o2b#ZBX5P;p7;c)$F1y4HwvJ?KA938$rd)gn_U^CcUtmdaBW57 zlPph>Fz&L`cSScFjcj+7Jif3vxb20Ag~FPstm?9#OrD$e?Y~#1osDB0CFZ9Mu&%iE zSj~wZpFqu6!k%BT)}$F@Z%(d-Pqy07`N8ch2F7z^=S-!r-@j{#&{SM@a8O$P#SySx zZLD_z=I300OCA1YmKV0^lo@>^)THfZvW}s<$^w^#^Ce=kO5ymAnk>H7pK!+NJ-+F7 z1Bb6Y=r)0nZ+hRXUyD+BKAyecZxb+$JTHK5k(nWv*5%2a+u*GDt|rpReYQ}vft zXrIt#!kGO85o^~|9Oc-M5A!S@9Q)O$$&g8u>1=ew?T35h8B{-Z_S78oe=E(-YZhBPe@Y1sUt63A-Cdv>D1nIT~=Rub6$?8g>meFb7Ic@w^%@RN2z72oPZ#Ta%b(P1|&6I z61iO<8hT*)p19Bgd0JgXP{^c{P2~K@^DIXv=dF(u|DFfqD^dMIl8-x)xKIpJRZru@ zDxicyYJG}mh}=1Dfg%B$#H`CiAxPTj^;f4KRMZHUz-_x6)lEq!^mu%72*PI=t$6{Uql#dqm4 zClgaN63!&?v*enz4k1sbaM+yCqUf+i9rw$(YrY%ir1+%cWRB<;r}$8si!6QcNAk~J zk3?dejBaC`>=T<=y=>QVt*4kL>SwYwn$(4ES793qaH)>n(axyV3R5jdXDh#e-N0K- zuUgk|N^|3*D1!Wlz-!M*b}Zc5=;K6I+>1N$&Q%)&8LWUiTYi&aQIj(luA< zN5R<8Y8L#*i0xBio$jWcaiZ4S2w3#R@CGemesy~akKP)2GojQF6!$}!_RdUJPBevX zG#~uz%Yirb0@1wgQ;ayb=qD}6{=QXxjuZQ@@kxbN!QWhtEvuhS2yAZe8fZy6*4Inr zdSyR9Dec4HrE|I=z-U;IlH;_h#7e^Hq}gaJ<-z^}{*s!m^66wu2=(*EM0UaV*&u1q zJrq!K23TO8a(ecSQFdD$y+`xu)Xk36Z*;1i{hS=H2E<8<5yHuHG~22-S+Jq|3HMAw z%qBz3auT=M!=5F|Wqke|I^E8pmJ-}>_DwX5w%d3MSdC>xW%$ocm8w8HRdZ|^#cEt1 zM*I7S6sLQq;;Mecet(Q()+?s+&MeVLOvx}(MkvytkvLHl7h*N0AT1#AqC&(he(^%przH`KqA$z_dAvJJb409@F)fYwD$JW_{_Oie8!@VdJE zU>D$@B?LawAf5$;`AZ1E!krn=aAC%4+YQrzL!59yl1;|T2)u=RBYA8lk0Ek&gS!Rb zt0&hVuyhSa0}rpZGjTA>Gz}>Uv*4)F zf7S%D2nfA7x?gPEXZWk8DZimQs#xi0?So_k`2zb!UVQEAcbvjPLK9v>J~!awnxGpq zEh$EPOc4q&jywmglnC&D)1-P0DH!@)x;uJwMHdhPh>ZLWDw+p1pf52{X2dk{_|UOmakJa4MHu?CY`6Hhv!!d7=aNwiB5z zb*Wlq1zf^3iDlPf)b_SzI*{JCx2jN;*s~ra8NeB!PghqP!0po-ZL?0Jk;2~*~sCQ<%wU`mRImd)~!23RS?XJu|{u( ztFPy3*F=ZhJmBugTv48WX)4U*pNmm~4oD4}$*-92&<)n=R)5lT z-VpbEDk>(C1hoo#-H_u0`#%L6L$ zln(}h2*Cl(5(JtVM{YZ26@Fwmp;?Qt}9$_F%`?+-JHbC;bPZj8PLq9 zWo-KFw!i&r8WuA-!3F_m9!24Z(RhalAUR~_H#Ln=$%b5GY z)oB)zO%J5TY}&BXq^7#M>euVL%01Tzj4$6^ZOjT*7@zr~q@6GEjGi)nbwzSL`TiLN z{DVG~I$w@%^#tD{>1Ap@%=XogG_^Hvy_xiRn4yy?LKsC+ zU!S79X8orh&D%>1S`x2iyi&(iG&r#YT{}~iy(FIOo8?MZU#eo*c*(RjAGj@uDi zARJur)-*{n0PgW~&mFeg`MJ?(Kr;NUom)jh?ozZtyywN9bea6ikQlh}953Oul~N%4 z@Sx!@>?l1e7V*@HZMJx!gMo0TeXdU~#W6^n?YVQJ$)nuFRkvKbfwv_s*2g(!wPO|@ zvuXF=2MiPIX)A7x!|BthSa$GB%ECnuZe_Scx&AlnC z!~6C_SF24#@^VMIw)a-7{00}}Cr5NImPbW8OTIHoo6@NcxLVTna8<<;uy~YaaeMnd z;k_ynYc_8jQn9vW_W8QLkgaHtmwGC}wRcgZ^I^GPbz{lW)p#YYoinez1MjkY%6LBd z+Vr>j&^!?b-*Vk>8I!28o`r3w&^Lal8@=50zV4&9V9oXI{^r8;JmVeos&wf?O!;_o zk))^k*1fvYw9?WrS!sG2TcX`hH@Y3mF&@{i05;_AV{>Umi8{uZP_0W5_1V2yHU<)E z+qviK*7SJtnL;76{WK!?Pv$-!w$08<%8Qy|sB|P%GiV1<+dHw*sj!C~SjsB6+1L@so+Q~n# z+Uc5+Uz+mGmkR@>H7D*c?mm8WQz;3VOpktU_DeBi>3#@z zmLe;3gP<7KPy>~k47nEeT?G?7e2g6316Xdb_y+ja5C9Ayg6QTNr~&Kbs(1>7zp|f@le;9B z1e(+Ga%jPWR7oc}=XcB4$z?YD)l;%#U;}~gZzGViI=fwu9OAPCCK!0w>Ay^#$b49k zT&|M?JaIyRT<;@*t_jp1ifWPvL;{maf6o0T#X!#9YX;0Q;LTQ0}0tg^_Ru4pkSr4#P zmnW|D0`A#Ie6pEfBDv39=jN2;kiUoT6I&kChsbI!jMuY6zuZql5!&i%5!c zjsHlXtjT;NV?jAb`%vy)JOK_j1rponLqc>(2qgYlLPEs>|0QV<=Pw~C`fLFKJJitt zyC6003{rxCsmtGKjhB%W2W~*%vKH8l$pZoOFT*K@uL9%CD^3rh=ZtuTU1 zJpf4|%n^yjh#dKSSCJI8;YU*CD!8Wv20*e5`-fya^75@ADLU^RdHDg3Bk3k6)dGi7 z!!z;|O1h$8q!vO*w6 I6Xdi10eY*&F8}}l diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index bdc9a83b1e..c44c2304cf 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 65dcd68d65..79a61d421c 100755 --- a/gradlew +++ b/gradlew @@ -144,7 +144,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +152,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac From b3689ee773d839b28ba880a3d778e1bf4cd89730 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Wed, 11 Sep 2024 11:10:06 -0700 Subject: [PATCH 046/130] remove test generated file --- test/data/fasta/out_order.fa.fai | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 test/data/fasta/out_order.fa.fai diff --git a/test/data/fasta/out_order.fa.fai b/test/data/fasta/out_order.fa.fai deleted file mode 100644 index 10ac8e6d4a..0000000000 --- a/test/data/fasta/out_order.fa.fai +++ /dev/null @@ -1,2 +0,0 @@ -chr5 142 6 56 57 -chr1 600 157 60 61 From bfd072fef49095a45c2bfd36c4f146f280e8035a Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Fri, 13 Sep 2024 21:11:09 -0700 Subject: [PATCH 047/130] Update jdk-17 references to jdk-21 --- scripts/igv-launcher.bat | 6 +++--- scripts/igv.bat | 6 +++--- scripts/igv.command | 4 ++-- scripts/igv.sh | 4 ++-- scripts/igv_hidpi.sh | 4 ++-- scripts/igvtools | 4 ++-- scripts/igvtools.bat | 6 +++--- scripts/igvtools_gui | 4 ++-- scripts/igvtools_gui.bat | 6 +++--- scripts/igvtools_gui.command | 4 ++-- scripts/igvtools_gui_hidpi | 4 ++-- scripts/readme.txt | 6 +++--- 12 files changed, 29 insertions(+), 29 deletions(-) diff --git a/scripts/igv-launcher.bat b/scripts/igv-launcher.bat index 31bff24297..3e4fdd3644 100755 --- a/scripts/igv-launcher.bat +++ b/scripts/igv-launcher.bat @@ -1,9 +1,9 @@ setlocal -if exist jdk-17 ( +if exist jdk-21 ( echo "Using bundled JDK." - set JAVA_HOME=jdk-17 - set JAVA_CMD=jdk-17\bin\javaw + set JAVA_HOME=jdk-21 + set JAVA_CMD=jdk-21\bin\javaw ) else ( echo "Using system JDK." set JAVA_CMD=java diff --git a/scripts/igv.bat b/scripts/igv.bat index 1d6a56141b..a40bccd121 100755 --- a/scripts/igv.bat +++ b/scripts/igv.bat @@ -3,10 +3,10 @@ setlocal for %%x in (%0) do set BatchPath=%%~dpsx for %%x in (%BatchPath%) do set BatchPath=%%~dpsx -if exist %BatchPath%\jdk-17 ( +if exist %BatchPath%\jdk-21 ( echo "Using bundled JDK." - set JAVA_HOME=%BatchPath%\jdk-17 - set JAVA_CMD=%BatchPath%\jdk-17\bin\javaw + set JAVA_HOME=%BatchPath%\jdk-21 + set JAVA_CMD=%BatchPath%\jdk-21\bin\javaw ) else ( echo "Using system JDK." set JAVA_CMD=java diff --git a/scripts/igv.command b/scripts/igv.command index 42b06562f5..14d0aff07e 100755 --- a/scripts/igv.command +++ b/scripts/igv.command @@ -13,9 +13,9 @@ prefix=`dirname $(readlink -f $0 || echo $0)` # Check whether or not to use the bundled JDK -if [ -d "${prefix}/jdk-17" ]; then +if [ -d "${prefix}/jdk-21" ]; then echo echo "Using bundled JDK." - JAVA_HOME="${prefix}/jdk-17" + JAVA_HOME="${prefix}/jdk-21" PATH=$JAVA_HOME/bin:$PATH else echo "Using system JDK. IGV requires Java 17." diff --git a/scripts/igv.sh b/scripts/igv.sh index 41ea0ec06c..400457f09e 100755 --- a/scripts/igv.sh +++ b/scripts/igv.sh @@ -11,9 +11,9 @@ prefix=`dirname $(readlink -f $0 || echo $0)` # Check whether or not to use the bundled JDK -if [ -d "${prefix}/jdk-17" ]; then +if [ -d "${prefix}/jdk-21" ]; then echo echo "Using bundled JDK." - JAVA_HOME="${prefix}/jdk-17" + JAVA_HOME="${prefix}/jdk-21" PATH=$JAVA_HOME/bin:$PATH else echo "Using system JDK. IGV requires Java 17." diff --git a/scripts/igv_hidpi.sh b/scripts/igv_hidpi.sh index 8d8c2cd55b..e6d3d69265 100755 --- a/scripts/igv_hidpi.sh +++ b/scripts/igv_hidpi.sh @@ -11,9 +11,9 @@ prefix=`dirname $(readlink -f $0 || echo $0)` # Check whether or not to use the bundled JDK -if [ -d "${prefix}/jdk-17" ]; then +if [ -d "${prefix}/jdk-21" ]; then echo echo "Using bundled JDK." - JAVA_HOME="${prefix}/jdk-17" + JAVA_HOME="${prefix}/jdk-21" PATH=$JAVA_HOME/bin:$PATH else echo "Using system JDK." diff --git a/scripts/igvtools b/scripts/igvtools index 4c55f1dc5a..c9e7a06f94 100755 --- a/scripts/igvtools +++ b/scripts/igvtools @@ -2,9 +2,9 @@ prefix=`dirname $(readlink -f $0 || echo $0)` # Check whether or not to use the bundled JDK -if [ -d "${prefix}/jdk-17" ]; then +if [ -d "${prefix}/jdk-21" ]; then echo echo "Using bundled JDK." - JAVA_HOME="${prefix}/jdk-17" + JAVA_HOME="${prefix}/jdk-21" PATH=$JAVA_HOME/bin:$PATH else echo "Using system JDK." diff --git a/scripts/igvtools.bat b/scripts/igvtools.bat index fee81c77be..5e90d8c87f 100755 --- a/scripts/igvtools.bat +++ b/scripts/igvtools.bat @@ -3,10 +3,10 @@ setlocal for %%x in (%0) do set BatchPath=%%~dpsx for %%x in (%BatchPath%) do set BatchPath=%%~dpsx -if exist %BatchPath%\jdk-17 ( +if exist %BatchPath%\jdk-21 ( echo "Using bundled JDK." - set JAVA_HOME=%BatchPath%\jdk-17 - set JAVA_CMD=%BatchPath%\jdk-17\bin\java + set JAVA_HOME=%BatchPath%\jdk-21 + set JAVA_CMD=%BatchPath%\jdk-21\bin\java ) else ( echo "Using system JDK." set JAVA_CMD=java diff --git a/scripts/igvtools_gui b/scripts/igvtools_gui index e12df3cc3d..843e033fca 100755 --- a/scripts/igvtools_gui +++ b/scripts/igvtools_gui @@ -2,9 +2,9 @@ prefix=`dirname $(readlink -f $0 || echo $0)` # Check whether or not to use the bundled JDK -if [ -d "${prefix}/jdk-17" ]; then +if [ -d "${prefix}/jdk-21" ]; then echo echo "Using bundled JDK." - JAVA_HOME="${prefix}/jdk-17" + JAVA_HOME="${prefix}/jdk-21" PATH=$JAVA_HOME/bin:$PATH else echo "Using system JDK." diff --git a/scripts/igvtools_gui.bat b/scripts/igvtools_gui.bat index a66493a9ea..1a7dcbc27c 100755 --- a/scripts/igvtools_gui.bat +++ b/scripts/igvtools_gui.bat @@ -3,10 +3,10 @@ setlocal for %%x in (%0) do set BatchPath=%%~dpsx for %%x in (%BatchPath%) do set BatchPath=%%~dpsx -if exist %BatchPath%\jdk-17 ( +if exist %BatchPath%\jdk-21 ( echo "Using bundled JDK." - set JAVA_HOME=%BatchPath%\jdk-17 - set JAVA_CMD=%BatchPath%\jdk-17\bin\javaw + set JAVA_HOME=%BatchPath%\jdk-21 + set JAVA_CMD=%BatchPath%\jdk-21\bin\javaw ) else ( echo "Using system JDK." set JAVA_CMD=java diff --git a/scripts/igvtools_gui.command b/scripts/igvtools_gui.command index 05913030c3..0331d3668e 100755 --- a/scripts/igvtools_gui.command +++ b/scripts/igvtools_gui.command @@ -3,9 +3,9 @@ cd `dirname $0` prefix=`dirname $(readlink -f $0 || echo $0)` # Check whether or not to use the bundled JDK -if [ -d "${prefix}/jdk-17" ]; then +if [ -d "${prefix}/jdk-21" ]; then echo echo "Using bundled JDK." - JAVA_HOME="${prefix}/jdk-17" + JAVA_HOME="${prefix}/jdk-21" PATH=$JAVA_HOME/bin:$PATH else echo "Using system JDK." diff --git a/scripts/igvtools_gui_hidpi b/scripts/igvtools_gui_hidpi index 37e5e62364..e1c2399759 100755 --- a/scripts/igvtools_gui_hidpi +++ b/scripts/igvtools_gui_hidpi @@ -2,9 +2,9 @@ prefix=`dirname $(readlink -f $0 || echo $0)` # Check whether or not to use the bundled JDK -if [ -d "${prefix}/jdk-17" ]; then +if [ -d "${prefix}/jdk-21" ]; then echo echo "Using bundled JDK." - JAVA_HOME="${prefix}/jdk-17" + JAVA_HOME="${prefix}/jdk-21" PATH=$JAVA_HOME/bin:$PATH else echo "Using system JDK." diff --git a/scripts/readme.txt b/scripts/readme.txt index 6d6ded2537..30dfbab3a0 100644 --- a/scripts/readme.txt +++ b/scripts/readme.txt @@ -75,14 +75,14 @@ command-line. To use the default Java, independently installed (java 11 require java --module-path=lib -Xmx4g @igv.args --module=org.igv/org.broad.igv.ui.Main -To use the java included with our packaged bundles substitute "./jdk-17/bin/java" for "java", as follows. +To use the java included with our packaged bundles substitute "./jdk-21/bin/java" for "java", as follows. - ./jdk-17/bin/java --module-path=lib -Xmx4g @igv.args --module=org.igv/org.broad.igv.ui.Main + ./jdk-21/bin/java --module-path=lib -Xmx4g @igv.args --module=org.igv/org.broad.igv.ui.Main The above commands assume that you are launching IGV from the directory where it was unpacked. Note that this lists the memory specification directly, and that the java_arguments file will be skipped. -If you wish to use the java_arguments file (assuming one exists), modify the above to (substituting ./jdk-17/bin/java for java as required): +If you wish to use the java_arguments file (assuming one exists), modify the above to (substituting ./jdk-21/bin/java for java as required): java --module-path=lib @igv.args @"$HOME/.igv/java_arguments" --module=org.igv/org.broad.igv.ui.Main From e16fd785a9231fa04b0a32c839612136870ccefa Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Sat, 14 Sep 2024 19:32:27 -0700 Subject: [PATCH 048/130] Update mac startup script for jdk 21 --- scripts/mac.app/Contents/MacOS/IGV.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/mac.app/Contents/MacOS/IGV.sh b/scripts/mac.app/Contents/MacOS/IGV.sh index 59b8f56b53..27e23b09cd 100755 --- a/scripts/mac.app/Contents/MacOS/IGV.sh +++ b/scripts/mac.app/Contents/MacOS/IGV.sh @@ -18,9 +18,9 @@ prefix=`dirname $(readlink $0 || echo $0)` # Check whether or not to use the bundled JDK echo ${prefix} -if [ -d "${prefix}/../jdk-17" ]; then +if [ -d "${prefix}/../jdk-21" ]; then echo echo "Using bundled JDK." - JAVA_HOME="${prefix}/../jdk-17" + JAVA_HOME="${prefix}/../jdk-21" PATH=$JAVA_HOME/bin:$PATH else echo "Using system JDK." From 068e20a7a9df69bf31094364dfb61706fbe49079 Mon Sep 17 00:00:00 2001 From: Jim Robinson <933148+jrobinso@users.noreply.github.com> Date: Sun, 15 Sep 2024 22:41:45 -0700 Subject: [PATCH 049/130] Replace compiled shell script MacOS launcher --- scripts/mac.app/Contents/MacOS/IGV | Bin 50704 -> 102736 bytes scripts/mac.app/Contents/MacOS/IGV.sh | 41 -------------------------- 2 files changed, 41 deletions(-) delete mode 100755 scripts/mac.app/Contents/MacOS/IGV.sh diff --git a/scripts/mac.app/Contents/MacOS/IGV b/scripts/mac.app/Contents/MacOS/IGV index 36ea038177338f1b8073a50d326b2260bfe3ba1a..daf1d8ecdf87e878399d5eb539cd1473a6fca348 100755 GIT binary patch literal 102736 zcmeI5e{dYdmB)Ma1N=i+0t^Q8!((9xA=w?-mW^a2TefzwWXrN5$-zk-8D@7z(%QST z>zP?;6%(?KF|iPEQj$uzToNk>A$EYo$3X%FjKL(3T#lTnTyjY$PLQK&36Ma-QIdnv zec$}BRtv_dO8&Uiy{>xG{kmVj>HhTeXm&=^^W%3OJ|u)F2?%jI;!+`+5T(bknTNaz z#0ZBWM?0{Y2jRd3m;e)C0!)AjFaajO1egF5U;<2l2`~XBzyz286JP>NfC(@GCcp%k z025#WOn?b60Vco%m;e)C0!)AjFaajO1egF5U;<2l2`~XBzyz286JP>NfC(@GCcp%k z025#WOn?del?nXx*lX|L#s2_a{1+vVGa>0RA@&FfDIv~>v_KT4W9|CL$0MDaDR*YT zIsvB)g>{L_8{Mg9!~f zg$p3Gk4-j&D;{-gUxtYM_bnD;7Gh#VI`N`FeImFojj$3@g%HtABB=KH^$kH*K}_t^5I$ z?Vb>od)#L2p@zVfYg8_|RkpfSMx1EHk8ths@K(Zh9XTF;)GdC1it{UwHRO!Ru(KW7 z(@nL8%bQV)hH`k*L`(hCm3!zSY}hT_^&}1VwMX*f$g7;&gAkf=JnVi|YQd2+lzFq| zsR&&?i{Z$dLyzx%d}?UZo7DQ7aM=XcpPD!%Ba#0aX+x2G3zgkS>#DU3NAf=e{3oZV zF8-LmV+F=SkOWa`p$0CMWm1r;(h zU5aH)Q#b5_@@?Ao;IgG+i-jLcQZ!>|30;#mgg4IdvfQglH>YLurihS3@!o!Uo7%4` zs@a=K!kr}~K^Y=w^`!>-b<;u{L@=04X_CaP;(+s`sY;yb#ml_7+ly@{_Puh<1egF5U;<2l2`~XBzyz286JP>N zfC&^6kdV;<8G+Ex=)#)>?(ctOBG9uxKRD?s+zp_IcV3`+0o*_cgdnRSQ3xLC267NM z4}2GbyI_HzL&hPvB^#&+h`>U~72aMIrM+vtZb!0uEMu$Dgx-OhI6}dFCf(@AV`e8hriVs?`55}(<1}}N~h^Lc{=Al7SOps0q zFSMboQ161?Q#xINQXy>o|BE;m`jP^j%9lWDAQVV%FDj?{etXYFf2qE&lRee1A`S?o zQ~knns_*xY`b+hFo$RST&^Uo~s$W=6^?e@1hwA$}*;D;35DKJI{lapMGQB{}tPE5+ z8f6oYN+f(fbe|3Ci5m3$yigAp>DLtLdXb(i(lH%o_!}(JKV77Mwn+c`BK=E6`o1Fl zn??EqMLPXI!X*&;{{gy*y%=&Cq#AN5M1s&WdSbi?axvrskdxITey6Gn6Zlo$W4b8L zbUDc>;N4TdFvpwYlr=q_{rT%nXPhDY(}aI6@XsqaCpZ(|KR5WNkr^|8`dnkhleh=m zre=&-AEvot>d92Ut|U|oCz4b$oicEDS}1WV?B5TMWaAd@SIb*0+@+ScnW<>puu?K^ z_y%oVO-f-im5!#e@&??9wxmwC*pSm^Y8!5-TaIDE+uuw@5VTP2U9Q{WQBySs<*?qb zCsJwMl()uGnhxEJ_v>0%wbdDpf_nczHXBnd)9B4whB_EcMf(O4JsGpNCo8YZ#1op_ zYS_A&)U~*3>sH7};Xjk~>QVa*}9duu`WAZNZ~{~ zldBNNp07BJ6YRHf$duD@gFt6sOyd%o%E&93hZwh`OIvATA-)TBP!@Pbl$22hr47XN z$Ov}yuP>L4NhSWbZIJW6!97>NBtT>34?%DRLT!C*ZLL-tQ|q+Y^7{Ik{oX_x+t&CzrTI(=;tc#d3NK$d3E=$+1vf<`|cct z_m%3gmY45XQ#b3-&PT2;eOlb`+xync8^4rypNaqL+X4-paS?zwu<%2OECy`Gb+) z?ETNtA}|YU`!QzyHS_Z(MTgs#iW8yE*vD)1-YD z?j6iVtdB$|I=}zrv9`_|)Q{c$$Ah~b*>`bd=1+$TMl&}3-I9hjrKX%Du+Sr8RM znypo1H?H>?%|WgfkJ+o|%x>7O53Ys_&so94vS3`pSt8yO*Ubi*tYGZ^^Wf?R*-N~u zzJ==|Gj0!taOMiysjmbbgBb}%b5TZU;<2l2`~XBzyz286JP>NfC(@G zCcp%k025#WOn?b60Vco%m;e)C0!)AjFaajO1egF5U;<2l2`~XBzyz286JP>NfC(@G zCcp%k025#WOn?b60Vco%m;e)C0!)AjFaajO1egF5U;<2l2`~XBzyz286JP>NfC(@G zCcp%k025#W|1Svq^w?|fU};@vv;lGFd`@2GdIT_5>)q;s>AJ!9ik zo{{rzh5YP=Ve$9t$seYv-3!J3Mpn80%G?ha3eu@x zCqkLXok$}=>xE~c)`_fQe_v>H{q6TGNH6p^%k$@c=aBrx+?9Jh`(l3$^j!p$MLlSw z7luOLl&>fmV*smcD{-SI<>=j!t=-c##@4w0uJf`;FZMBQEd-529-=7WwcTr#=FRP0 zuD2%Fk3j5%qL^@!kZ)QMTTyyb1;cPse_7Z%p~|_$V#lgu_ph1UtG?9oz12sbqwg0;kN`xK!wZDc4!eqr%C12A z#0uv~;~M}u@NfC(@GCcp%k025#WOn?b6 z0Vco%m;e)C0!)AjFaajO1egF5U;<2l2`~XBzyz286JP>NfC(@GCcp%k025#WOyK__ zfvMf%=<%I_1IL@i=wn9~JvcsBJoG}Tm>i$mgt%!E?}2{d_|CNlU^jZWRQdVasY3^R z`>E39M`sCfaMndlld}R%llymHJTb1I{)fcm3(EtevvRr4{ATgc)JE~p_;0F5?}9J9 zzqvGj`o4uv3vnghi<>WrTqoj@#jn?mei!9rw-$NoDQy4Av%T;A?Cko{ucI8?N5q@; zqiM1gVn=?#^!}Z)s#T+-D8mbO^Bw)~R*l{Yo!U44o08G-v&CdU?0WWvN->%B{oT20 zl+rI&HcjSB#ka=KMtlkSeM!^gXT5yN!~RBQJ+x{xzX)ZEnkI87JCT3JRipU}$ak(2 zuOYf^1DpLGn>_aImW%n0pL1Zp0AsT|FgkuZ)e{F7j|WCe=H{MFqW%k~H%*Q&XqqH; z<6xfO*z^$WA3Cu66BDR^Vw`3LX1_uV`n>(LlKq0FhcKR_G}dpYp4d-gJ#~bRjgkY$ zG4^A-7fpP>Z1Sjs z{l7u`-0$}dZCw-)^~CtKsi_5d=!1Qs~~-DL3&|9y1F2JSwZ@Wf^?`Ly{sU; zvLL<6O*gBCk+P*;-ImmJIuVblc06TBTHMrQcFG(Souvvs|q$;p7L5WHB()l#wTs|+=%H@2?- zsISNIMsj1PZlyA2Ot)l|C@X72X`@#J!_;3rlu&y@8B6bqnYwOl(v8eI)oirQj1CKG zQ^?TmP}N zfC(@GCcp%k025#WOn?b60Vco%m;e)C0!)AjFaajO1egF5U;<2l2`~XBzyz286JP>N zfC(@GCcp%k025#WOn?b60Vco%n84pO0ST?|fQ&%s+vM(A07xKs3j{c6`u4XUoOBab z18DMd+zR^Gj7;D zGYxw)s;R*MuKUxDp<8ZZ;R^C3l%$@Fr3VEREMg%PHHL*BEb^_|em4V(IE&J>P(rB~ z$sna9`y8hkTUYvYRntu|J4wqoV9|q-OwFsMBLVRM%;%wthp}D*_4Egxeh+a%`Z-T; zz#0yulW4T@!tW7lzJjr~0o_0)Zl`Usz7{{r*vZslKn1J=LdUjsodazp$L@`#gvb)%SI>r}|qV z6iBD~h2jQU?J(lNDWlw)n98Tvg%`a`z?ptl>>(oEiMngMSY2&nN!G z_fH$njGsQISn(v*dudZMMywCBTQT)ys$W+Us)Z9rDw$3hScyg`aVzYveG|#XEv!u= zZ?Ui&vH&YSB zDinK{>$Z5*RLwy-toQ4QR9ZLXt+AA*LpS67x)xS#b%vv$-anAd##GBRdb5_H4n|Ya zzJWwf#_a9M%Ih-mgeJEdwr(bMEw0+S6*5wBL({V}yo92TVOWo5df%&HbJXci&`6m{ zH6g2Ja%HXT^Z;k`BCK?L9DBavF#hToNS2e1Pz%BIEqMYl@=BgTjOAy_X43(VxCeEp z{5DZiMj1#4s6L$@(TRDvaN{;bgCTLoya{4ofwK&Y*+t*zB+V``liTV7vZ zvwTHey{ax>wxXt|r$$>I)3rKPyfb>$jdKIfl>O??M;GPQ`S-UE9{pU!JC@td-`=-=$8*}iufKis+5PrgC#(18=HEUj zjkMpb&&y-5u(N$z?y;v9-VvO5_R6~!G*8lr=Y7M6kG^~){Jp8gZ8b~(@rh?X z6r8L+|F&B`Fh23im4`NN+#f%F@2Bs%Z)@s@FZUcS`^C|t+ez361^F_bhw)6O{r}F=omN)J=_g^3G`_Awk2i~!- zT=u{hwEG4wzWvV1);IURb8gv=t{ZSP(sAh=ZO5M9u>(irRSj#h$%N!BPSsewG*nY9 z=?11J7QSk%-qL+_u&#Q|>NyRI!<(DCKh_bEu$q=-OC4L*wY4@&)j?TaOAAiPa=1Gz zb+om1bxZIe%aQhKsk+a$)2n28U|=BRE=z?o5*b=@2Tn4&X%Du+Sr8RMnypo1H?H>? z%|WgfkJ+o|%x>7O53Ys_&so9BUctDA(_FkKuA2=qS-}`*UG!mfgX|^VRo}vOoEf(V zLpaq2ZPnTyR1=BRzzGMmT$YTz%kJ(QWT$bf=K!4vjMt{SN&NfC(@GCcp%k025#WOn?b60Vco%m;e)C0!)AjFaajO1egF5U;<2l l2`~XBzyz286JP>NfC(@GCcp%k025#WOn?b6fxis`{|kaWWv>7L literal 50704 zcmeI5cT`hJxWI3aAW{q{B8nmwY}il)RFERk;L=vaRap$ZNN*}W3kp#}jH0W6owY0K zA|hggV8bqAV=W8zph!_x5pj9nH#aC|eeb{b-g)QD-81(qGxNrtqPhii@(+R8E58upIs zngvBDbhMvtFP!A&>cOtA9h+C%Ix0)tZ&+}xaL);a!BOp&^}mc~3~k9}$M#oP4D#1+ zGeBZqR%E|nUhOK9l=1?bTz0Hz%L^r5LSeYCFfK8|FDhIZ9UrU6i>)K~BsN}dn+xo= zzUYEyi|L7q;t~o2eB*uFtTt}n?A#v&hygNI&{=|25Vpo%4WQsU#8y#%CbQCtmX&%fvh;suAe68?ojY*8v`Y_Z_gF$+g}FIpN!w$33_KJ z#!$4Nj7*4&9T^_#H!>hGJOFIG4545io4#mIJytOOz{)oXcb3ODrHuxkF%%q|VK6IA zqO)u5t1=WkI#{K+2E+U@PQ7)Thm|<46%?$)IY1_Gym7we0{M6(#>EFljBpqMd4>9s zM$AAE7~c-~VBaAM8+p--5}*Vq0ZM=ppaduZN`Mle1SkPYfD)htC;>`<5}*Vq0ZM=p zpaduZN`Mle1SkPYfD)htC;>`<5}*Vq0ZM=ppaduZN`Mmh|3bi1AbxR?+if=Fdy&gy zMJE(#+zudk1==mM7)E?WARWsAJ5^Objr$rrs+?5rQ;^mSPjsubwblDv5U-CT!(cpt zcyW_Bp-~{=HwnaidGg{$hPN8OXIqU_cm>@37_vkl?RFRxyEJnRyVQjDyaGNH8wt2L zrFN+{%Rv!HT;$L`5mm8g)y9vD8=d{nI`9hgIF68-5d8L>TaKyPo}lpxy19Z7-*T4l ztL)Rv)xlv)9QLW^>UL>3M7LpI9ckQtpyNDxg2Z|F!8{;oxqypbO~%wEE{)5o(Ga^d zR-3ca9Kq|8g7aYh`}rP%7HQn#nd}h9K|;d0APCE2pronlr{a`daM~Nd!MO?3Ud6T) zk(ST|dMzY6qQ~aibK6WLKy+S#mOk*{zqjXHa#X|=UqP=x;wgt34STc8j;R`G36`D3 z?_dK?oLzEBD^AUcOpVvaj$OZHHSP#Ndd!Cq;dX}~g9s!@1Qzs+>>tELgxm|h$(`K9 z@T5G5St_~)Rd`iz#UZ4kGO*1)2w<(nH&3RMVMT@Dlj}l1fkYGnZ3SXc1-K++F!3?$ zB007ID#XX+M2{wV7185~{v32zC>eAeg%KqDbWj;q%?4G6sAMRCxU>jImiv8aZDlhA zi;<9A1rqHE@QreTq!XZN5{$bg7Ue^1o(QKSA-Z7Md4%D8PTm)OQoz#;`tkY;#0m9g zGC}gB44tRAK^wBazh}#FXyyi zoy~!t7^cV=Sjp>2W6}}UzX<0efAj-iA#{XYB0W`jTNX7{yQjBU!rjRV>2>i!v6BCAr2&ytomzyc{+a6x=Yl_hkSae`Eo>zz^ms?+QWMRShDO<=#S$5-Xr& z3!2&G)JyW`@GF>NJeI`#)ACzjNESEBeIX$+|B-ywC$cWnoS@sXk~J6vb`ze;^3`mW zC0k{rsImts_irY>da+d-aGOFe978UEDqaycV=8-grSP$zl-nQx!DU;P%Ev(#W|v`O zi21qlRbapgkt-N?mM6uZNkQ|oq&Yh#Jf!8S#CwoqyNjMGgM$ zMPAI-EFd+96*YMD$ak|fb4bl=MGZ+_!qzyFnmR=drX-JHYli;2Cb$V%aCJycCrAo& zBYkX0ADb6HWAbC0K%6B0FgTF#Tu)I<_U;sDz-5Pn$-aQ}Az(4_o>L^z6_coep^_Ir zf=>iR&&}=HK+hcLDd9IJ%QOW3P3)0y7s-kFmtX^3f(?W}JQS4jn?Nwti=a6PmmkEfa!p7x#WKhT!7SU)renKW$f-L!n}*5ImD>#i z03HI{hy{0_)WqP41T_NK402Vr<_ue-=tBs+{zzc&{C7>&2T~&@H4uzMBYg@;ADb$y zk&c}U;w14u$LY6Q2G@2$A@Mm7q*wr;<;QReVty4ctWsuZg?URBR}~q=bOoG~YHwu<&Jz&?2==u;g5k&?F;9X7DcJ_)(;E+59fvVLZBpfX=oKiLDOey~kinilM z;&Msic}ILN`65T0a7WyO=}Ts<->S==9XPO_Me|q{a{ziJ<0Q>T{8(>%SzZM2VP$AeI@z4kM}uVpCoLq zIRD*C`7LO|n?AnzNI*yu>g9joaU#*KBRkHsULbaWw-5LL<3oBn@XrD{zQxOB(9u!C z*u(ljYQfl8s_l=02d9A8r9!?9THt$43NDx&U*9BF^3Nb(LzGpsA2D1r?147;5lR`X zN4+39Q60UPcqzK98-|8=Aozf4>(@*U53m*eWbkmv;jt@6Kkk2k3)}LN6WNbW(8~TK zRN-fnqqq!cxWju>tBHyO!?c>HlDitYtCPD1xoeSoXL8pe_b%kFNACLMZb0rm$-Ni3 z8PM*Iey%P@ z%@Vass5zrngjxV<`KZO9b^x`dsAZ!jMJ)rhji{|cZ69jOQ7b@g0cz(_i$tv)wE)zr zP@9cfEo$zl;g+eMjG89oTWyV+F>0ex8;ROr)a+0*Ld^{|JPPy&PPy&PPy&>K?D@N=eqN~gG+j?Nf!v)$K`frqV4$pISJg9OY>&otPqH@vA{E<#K-Y&epca2@^ z@UoM=i7ta=IrAH-ZWlw;Qg5B zPCw^cbm`5TJ7A_}^PHh!E^9)cpAPFJyD?R~?4!q;=VSHP4RabP8*|0rwf${{dVMHB(2lap|iyX@PS|a&J>{8S`t#&%I2-RX>#tov4?5v1F9t&iPplm8AuT?>#P739J{)8>Nx& z5PJH>y1q^;s=KJHS#Ro7-uLa{6=i)VSP#!%IX}KR-7|W1>z7AoYrC~*{^PbxW|Vc= z>u`Uamy1IFQCt4dr2D!-p8BC`Pv3j{%GE?8j(2}GyyRiI)-3ZA{c~%AR&Lxfp>Lnw z!|H>dwyxOzliA>InSL8oH|Otaoze7UqjY4=)Z4jCSm4==$UTGKY+Y1x+SVnzv$#Cb zYl5Z8fcl~4J)(Ad+>!Sz;SM>m=uXm%8RZTB`h~|+9Q(a$)W5a==e*sY?cP7J8aqx> zGIms%T&3n_mfhXCI)i2u>z-&e4KC5|P=346V!di*Vq%3|&Xh(^ulmK-{a2}}&KSR? zsbaUEWajoJ(UE)aM#~ng&R35KaTr$He8Xk){fYhiU5uM8se0*GxqH<@ef~l9tg&vF z4|RJR*Z0h%DNdjD^jf`QQ{9`5nLD#A4rV_HQrmK+DN1;=qtv)zT~cK165WctiL>L^ zEKeBmPoGVjs%u3ut(?5|Ms>S==9oXHGymb}M=6uI8&0k84bFUJ z5qntYZ}G1mzhwXR+l%bkmA`8)*NJiO{CnP$U!wJP+HYbC)GSoW4ND##ZMDqgOtUNQ z)S$yJU4AkA*fjfdW44)&?=fx1tWkL_Lzh(TnYF#w&V|c980U076&ST4;qOT&Yj5vS z+nc>a{&>XjVp%66x1RUZ1^skfj5LO8x6B!$`J&_GJz0+u-SU~mG!_b6Pmk* zZoXl+d4rXy>(!h6-??7fHc#7SY4fKHCV!8~f^oUyC(e7hJLjERY|UBMtL0rJxARX= zEmT=icJ;yW`_YHG$4z^dXyCN%ER*ZvFwpX{t%Jppk?w!(45)K*_HbL~V?5T9CtmKE(#m`_S*Pfe{9*2TQFVHrP*z|y|4GcN<%TP z&`tl^=~Zq6w_IJawbPg(*Hh-miT#m#rud$i8o5?Hiqmy1!3@roJV!LKVXT+CL@?YQjyy-Ll677b&?-WrP*A8oLj z=2#e!RycfP!;|I!v&Y)@(>YpekGf7Rxxc!0>9e|{qiu?{ZC^xYbe@^b##lFra4#|?V89qR6Sf9Z($him_=dF%Tn#YmdERClSkv)Ao{g658a z?|5dpTGw9mj=vn?^YNp+iX5$_Dv~ zH800?TDNzdS47O4xjl3CYTx%e^MWa>TkPEV=*GLVK0Q5eQEig#-#g=(=aD7aYvxqW z3J%@ZCu6d`WY5)&pT5i=e`w=hd&R**>f~i5aY| z(e!z?-V3`m(atCK|MP6#q&HvEoj$Z?HfE;Il3jdOb8Cpp)~JG<_Y0%%^zj;V!llO{ zZtdlDeN;|m+$;+}Qr7dZ-o!=y)X#0#-CUFS=|xW06*b8t?Qi$ygtrRCg;}Okb?s#L zyCm(L>?vqHHSgY`HHoLHR1-TNt2|d2e)ndn(ctvgs<8vdI6f_MYdKvoS!L<(wHX5v zGxFd1-pO+M?QPF{ynauon|OS<`>g!T^%;hrEq3V4%bk8wHPx%nBk|S;=Gilx{U_dj z;9@zu<6vz=3R|h5XAEdW$E29+rOG?Pyakj*fYLPI`~4*Lqsa zonf{i>6#$f^UXZ&E1A4EH)+#HFU|c2o8P|IF+Fbe$!SKZb#`F)A>CrT9d*x?&ov#O zlK67e>K+~+U*12xt^Z1`hlQ^#?se|8=INovs#%q{_SEe-HN|%O>47O{D!SxrPd#_j`q#Sr?&8pl ztI3rg-BJo3|FI;f#d)CP?-Qz{r+K~EdT?2Z%`@}lMOJRT$35y||NAc*4s=WvvR*FC zI^we5HgrYs1*=TMnK5rQ5;rzirX=;^j?#czF9hTXnWrfyK&>0@HFb75wuDSGZn z@851Z=i!|eHg)Ag(FcP!$!8r8#%iDVCBPu%+^#>KXB%$pv5i}6o4=!I=;K3fyuH54 zuZz^C1SkPYfD)htC;>`<5}*Vq0ZM=ppaduZN`Mle1SkPYfD)htC;>`<5}*Vq0ZM=p zpaduZN`Mle1SkPYfD)htC;>`<5}*Vq0ZM=ppaduZN`Mle1SkPY;QtK)-TsV^K4hzF zClm_f;(h(Yg#IC6!r%no*Z}6|SSFqI2L#5(MxzlJIWIIeDiYLy#P9&Drv3yp+& zCLJdo$3}wYQRsHi78a(8&0pHm9*tRzbd@;TT{r z_H^S%*!XC=gBctc5A#85CTm59 z2B75wR>aqkA!xw_B1p(+)`HZrt4?(y_*+O7;#VJ4eYrs$`#1vd<~mSC#DBO7=q~`hvNn{?PeLalYjv Date: Fri, 20 Sep 2024 13:56:18 -0400 Subject: [PATCH 050/130] Update CRAM reference test to include a genbank (gbk) referece with chromosome aliasing --- .../igv/sam/cram/IGVReferenceSourceTest.java | 54 +++++++++---------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/src/test/java/org/broad/igv/sam/cram/IGVReferenceSourceTest.java b/src/test/java/org/broad/igv/sam/cram/IGVReferenceSourceTest.java index 6021445bf9..6e0eacd22b 100644 --- a/src/test/java/org/broad/igv/sam/cram/IGVReferenceSourceTest.java +++ b/src/test/java/org/broad/igv/sam/cram/IGVReferenceSourceTest.java @@ -30,9 +30,7 @@ import org.junit.Before; import org.junit.Test; -import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.Arrays; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; @@ -43,53 +41,51 @@ public class IGVReferenceSourceTest { - public static final String FASTA_URL = "https://igv-genepattern-org.s3.amazonaws.com/genomes/seq/hg38/hg38.fa"; - public static final String COMPRESSED_FASTA_URL = "https://igv-genepattern-org.s3.amazonaws.com/genomes/seq/hg38/hg38.fa.gz"; + public static final String FASTA_URL = "https://igv.org/genomes/data/hg38/hg38.fa"; public static final String EXPECTED_REFERENCE_BASES = "AAACCCAGGGCAAAGAATCTGGCCCTA"; //bases at 22:27198875-271988902 + public static final String GBK_URL = "https://s3.amazonaws.com/igv.broadinstitute.org/genomes/NC_012920.1.gbk"; + public static final String GBK_EXPECTED_BASES = "tcatttctctaacagcagtaatattaataattttcatgat"; @Before public void setUp() throws Exception { } - private void assertReferenceReqionRequestsWork(final String fastaUrl) throws IOException { - GenomeManager.getInstance().loadGenome(fastaUrl); - + @Test + public void testGetReferenceBasesByRegion() throws Exception { + GenomeManager.getInstance().loadGenome(IGVReferenceSourceTest.FASTA_URL); IGVReferenceSource refSource = new IGVReferenceSource(); String expected = EXPECTED_REFERENCE_BASES; - SAMSequenceRecord rec = new SAMSequenceRecord("22", 50818468); + SAMSequenceRecord rec = new SAMSequenceRecord("chr22", 50818468); byte[] bases = refSource.getReferenceBasesByRegion(rec, 27198874, expected.length()); assertEquals(expected.length(), bases.length); assertEquals(expected, new String(bases, StandardCharsets.US_ASCII)); - } - - @Test - public void testGetReferenceBasesByRegion() throws Exception { - assertReferenceReqionRequestsWork(IGVReferenceSourceTest.FASTA_URL); - } - @Test - public void testGetReferenceBasesByRegionCompressed() throws Exception { - assertReferenceReqionRequestsWork(COMPRESSED_FASTA_URL); + // Test with a chr alias + rec = new SAMSequenceRecord("22", 50818468); + bases = refSource.getReferenceBasesByRegion(rec, 27198874, expected.length()); + assertEquals(expected.length(), bases.length); + assertEquals(expected, new String(bases, StandardCharsets.US_ASCII)); } @Test - public void testGetReferenceBasesCompressed() throws Exception { - - GenomeManager.getInstance().loadGenome(COMPRESSED_FASTA_URL); - + public void testGenbankReference() throws Exception { + GenomeManager.getInstance().loadGenome(GBK_URL); + String expected = GBK_EXPECTED_BASES.toUpperCase(); // Seq at NC_012920:7,279-7,318 IGVReferenceSource refSource = new IGVReferenceSource(); - SAMSequenceRecord rec = new SAMSequenceRecord("22", 50818468); - byte[] bases = refSource.getReferenceBases(rec, false); - - assertEquals(50818468, bases.length); + SAMSequenceRecord rec = new SAMSequenceRecord("NC_012920", 16569); + byte[] bases = refSource.getReferenceBasesByRegion(rec, 7278, expected.length()); + assertEquals(expected.length(), bases.length); + assertEquals(expected, new String(bases, StandardCharsets.US_ASCII)); - assertEquals('N', bases[0]); - String expected = EXPECTED_REFERENCE_BASES; - String actual = new String(Arrays.copyOfRange(bases, 27198874, 27198874 + expected.length()), StandardCharsets.US_ASCII); - assertEquals(expected, actual); + // Test with a chr alias + rec = new SAMSequenceRecord("MT", 16569); + bases = refSource.getReferenceBasesByRegion(rec, 7278, expected.length()); + assertEquals(expected.length(), bases.length); + assertEquals(expected, new String(bases, StandardCharsets.US_ASCII)); } + // @Test // public void testCompressedTiming() throws Exception { // From 9170e309b5707c62e3afbe5bd0874aa97e947cb9 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Tue, 24 Sep 2024 21:30:45 -0700 Subject: [PATCH 051/130] Clarify sequence name mismatch error message --- src/main/java/org/broad/igv/track/TrackLoader.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/broad/igv/track/TrackLoader.java b/src/main/java/org/broad/igv/track/TrackLoader.java index c967156e36..66e02b15ec 100644 --- a/src/main/java/org/broad/igv/track/TrackLoader.java +++ b/src/main/java/org/broad/igv/track/TrackLoader.java @@ -956,7 +956,7 @@ private void showMismatchSequenceNameMessage(String filename, Genome genome, Lis StringBuffer message = new StringBuffer(); message.append("File: " + filename + "
does not contain any sequence names which match the current genome."); - message.append("

File:      "); + message.append("

Sequence names in 'filename':      "); int n = 0; for (String sn : seqNames) { message.append(sn + ", "); @@ -967,7 +967,7 @@ private void showMismatchSequenceNameMessage(String filename, Genome genome, Lis } } if (genome != null && genome.getChromosomeNames() != null && genome.getChromosomeNames().size() > 0) { - message.append("
Genome: "); + message.append("
Sequence names in genome reference sequence: "); n = 0; for (String cn : genome.getChromosomeNames()) { message.append(cn + ", "); From 907726c2985cdf9e44217b4da1f7cb2af57cf27d Mon Sep 17 00:00:00 2001 From: Louis Bergelson Date: Wed, 25 Sep 2024 14:24:44 -0400 Subject: [PATCH 052/130] Fix for 2bit reference crash when reading the end of the reference (#1586) * Clip requests for sequence to not go over the length of the chromosome. * fixes https://github.com/igvteam/igv/issues/1583 --- src/main/java/org/broad/igv/ucsc/twobit/TwoBitSequence.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/broad/igv/ucsc/twobit/TwoBitSequence.java b/src/main/java/org/broad/igv/ucsc/twobit/TwoBitSequence.java index d491d997f1..27368ad839 100644 --- a/src/main/java/org/broad/igv/ucsc/twobit/TwoBitSequence.java +++ b/src/main/java/org/broad/igv/ucsc/twobit/TwoBitSequence.java @@ -133,6 +133,9 @@ public byte[] readSequence(String seqName, int regionStart, int regionEnd) { throw new RuntimeException("regionStart cannot be less than 0"); } + //don't run off the end of the genome + regionEnd = Math.min(record.getDnaSize(), regionEnd); + Queue nBlocks = _getOverlappingBlocks(regionStart, regionEnd, record.nBlocks); Queue maskBlocks = _getOverlappingBlocks(regionStart, regionEnd, record.maskBlocks); From 6203b4537a1ef9dfac33581d86bb4375a87b7115 Mon Sep 17 00:00:00 2001 From: Chris Saunders Date: Thu, 26 Sep 2024 07:38:42 -0700 Subject: [PATCH 053/130] Correct rendered range of symbolic VCF alleles (#1580) * Adjust the start position of non-ref symbolic alleles to include a padding base as described in the vcf spec. Co-authored-by: Louis Bergelson --- src/main/java/org/broad/igv/variant/vcf/VCFVariant.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/org/broad/igv/variant/vcf/VCFVariant.java b/src/main/java/org/broad/igv/variant/vcf/VCFVariant.java index 182c033067..acc15bc66b 100644 --- a/src/main/java/org/broad/igv/variant/vcf/VCFVariant.java +++ b/src/main/java/org/broad/igv/variant/vcf/VCFVariant.java @@ -385,6 +385,15 @@ private void calcStart() { if (variantContext.getType() == VariantContext.Type.INDEL || variantContext.getType() == VariantContext.Type.MIXED) { prefixLength = findCommonPrefixLength(); } + + /** + * The VCF spec defines POS as the base preceding the polymorphism for non-ref symbolic alleles. + * + */ + if (variantContext.getType(true) == VariantContext.Type.SYMBOLIC) { + prefixLength = 1; + } + this.start = (variantContext.getStart() - 1) + prefixLength; } From 40869ade416c77d37d665f2daa843e5b9692efe1 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:43:31 -0700 Subject: [PATCH 054/130] Restore hosted genome selection behavior -- json is downloaded to "genomes" folder. --- .../java/org/broad/igv/ui/IGVMenuBar.java | 2 +- .../igv/ui/commandbar/GenomeComboBox.java | 46 ++++++++++--------- .../ui/commandbar/GenomeSelectionDialog.java | 44 ++++++++++-------- 3 files changed, 51 insertions(+), 41 deletions(-) diff --git a/src/main/java/org/broad/igv/ui/IGVMenuBar.java b/src/main/java/org/broad/igv/ui/IGVMenuBar.java index 8f72951c98..67dae72c5b 100644 --- a/src/main/java/org/broad/igv/ui/IGVMenuBar.java +++ b/src/main/java/org/broad/igv/ui/IGVMenuBar.java @@ -431,7 +431,7 @@ private JMenu createGenomesMenu() { JMenu menu = new JMenu("Genomes"); - loadGenomeFromServerMenuItem = new JMenuItem("Select Hosted Genome..."); + loadGenomeFromServerMenuItem = new JMenuItem("Download Hosted Genome..."); loadGenomeFromServerMenuItem.addActionListener(e -> GenomeComboBox.loadGenomeFromServer()); loadGenomeFromServerMenuItem.setToolTipText(LOAD_GENOME_SERVER_TOOLTIP); menu.add(loadGenomeFromServerMenuItem); diff --git a/src/main/java/org/broad/igv/ui/commandbar/GenomeComboBox.java b/src/main/java/org/broad/igv/ui/commandbar/GenomeComboBox.java index 9a2dc7839e..f9cc1fbf80 100644 --- a/src/main/java/org/broad/igv/ui/commandbar/GenomeComboBox.java +++ b/src/main/java/org/broad/igv/ui/commandbar/GenomeComboBox.java @@ -20,6 +20,7 @@ import java.io.File; import java.io.IOException; import java.util.*; +import java.util.List; /** * Created by jrobinso on 7/6/17. @@ -102,16 +103,10 @@ private void loadGenomeListItem(final GenomeListItem genomeListItem) { loadGenomeFromServer(); } else { - boolean success = false; - Exception error = null; try { - success = GenomeManager.getInstance().loadGenomeById(genomeListItem.getId()); + GenomeManager.getInstance().loadGenomeById(genomeListItem.getId()); } catch (Exception e) { log.error(e); - error = e; - } - - if (!success) { int choice = JOptionPane.showConfirmDialog( IGV.getInstance().getMainFrame(), "The genome [" + genomeListItem.getId() + "] could not be read. Would you like to remove the selected entry?", @@ -120,7 +115,7 @@ private void loadGenomeListItem(final GenomeListItem genomeListItem) { if (choice == JOptionPane.OK_OPTION) { GenomeListManager.getInstance().removeGenomeListItem(genomeListItem); refreshGenomeListComboBox(); - log.error("Error initializing genome", error); + log.error("Error initializing genome", e); } } } @@ -220,27 +215,36 @@ public static void loadGenomeFromServer() { if (dialog.isCanceled()) { IGVEventBus.getInstance().post(new GenomeResetEvent()); } else { - GenomeListItem selectedValue = dialog.getSelectedValue(); - if (selectedValue != null) { - + List selectedValueList = dialog.getSelectedValues(); + GenomeListItem firstItem = null; + for (GenomeListItem selectedValue : selectedValueList) { + if (selectedValue != null) { + boolean downloadSequence = false; + boolean success = GenomeManager.getInstance().downloadGenome(selectedValue, downloadSequence); + if (success) { + GenomeListManager.getInstance().addServerGenomeItem(selectedValue); + firstItem = selectedValue; + } + } + } + if (firstItem != null && selectedValueList.size() == 1) { try { - GenomeManager.getInstance().loadGenome(selectedValue.getPath()); - - GenomeListManager.getInstance().addServerGenomeItem(selectedValue); - GenomeListManager.getInstance().removeUserDefinedGenome(selectedValue.getId()); + GenomeManager.getInstance().loadGenome(firstItem.getPath()); + // If the user has previously defined this genome, remove it. + GenomeListManager.getInstance().removeUserDefinedGenome(firstItem.getId()); // If this is a .json genome, attempt to remove existing .genome files - if (selectedValue.getPath().endsWith(".json")) { - removeDotGenomeFile(selectedValue.getId()); + if (firstItem.getPath().endsWith(".json")) { + removeDotGenomeFile(firstItem.getId()); } + } catch (IOException e) { - GenomeListManager.getInstance().removeGenomeListItem(selectedValue); - MessageUtils.showErrorMessage("Error loading genome " + selectedValue.getDisplayableName(), e); - log.error("Error loading genome " + selectedValue.getDisplayableName(), e); + GenomeListManager.getInstance().removeGenomeListItem(firstItem); + MessageUtils.showErrorMessage("Error loading genome " + firstItem.getDisplayableName(), e); + log.error("Error loading genome " + firstItem.getDisplayableName(), e); } - } } }; diff --git a/src/main/java/org/broad/igv/ui/commandbar/GenomeSelectionDialog.java b/src/main/java/org/broad/igv/ui/commandbar/GenomeSelectionDialog.java index 4b4d9cbdf0..096566b4d5 100644 --- a/src/main/java/org/broad/igv/ui/commandbar/GenomeSelectionDialog.java +++ b/src/main/java/org/broad/igv/ui/commandbar/GenomeSelectionDialog.java @@ -51,6 +51,7 @@ public class GenomeSelectionDialog extends org.broad.igv.ui.IGVDialog { private JPanel dialogPane; private JPanel contentPanel; + private JTextArea textArea1; private JPanel filterPanel; private JLabel label1; private JTextField genomeFilter; @@ -61,7 +62,7 @@ public class GenomeSelectionDialog extends org.broad.igv.ui.IGVDialog { private JButton cancelButton; private boolean isCanceled = true; - private GenomeListItem selectedValue = null; + private List selectedValues = null; private List allListItems; private DefaultListModel genomeListModel; @@ -105,7 +106,11 @@ private void rebuildGenomeList(String filterText) { * @param e */ private void genomeListMouseClicked(MouseEvent e) { - if (e.getClickCount() == 2) { + switch (e.getClickCount()) { + case 1: + List selValues = genomeList.getSelectedValuesList(); + break; + case 2: okButtonActionPerformed(null); } } @@ -114,8 +119,8 @@ private void genomeEntryKeyReleased(KeyEvent e) { rebuildGenomeList(genomeFilter.getText()); } - public GenomeListItem getSelectedValue() { - return selectedValue; + public List getSelectedValues() { + return selectedValues; } public boolean isCanceled() { @@ -124,14 +129,14 @@ public boolean isCanceled() { private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cancelButtonActionPerformed isCanceled = true; - selectedValue = null; + selectedValues = null; setVisible(false); dispose(); } private void okButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_okButtonActionPerformed isCanceled = false; - selectedValue = genomeList.getSelectedValue(); + selectedValues = genomeList.getSelectedValuesList(); setVisible(false); dispose(); } @@ -140,6 +145,7 @@ private void okButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRS private void initComponents() { dialogPane = new JPanel(); contentPanel = new JPanel(); + textArea1 = new JTextArea(); filterPanel = new JPanel(); label1 = new JLabel(); genomeFilter = new JTextField(); @@ -165,6 +171,16 @@ private void initComponents() { { contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS)); + //---- textArea1 ---- + textArea1.setText("Selected genomes will be downloaded and added to the genome dropdown list."); + textArea1.setLineWrap(true); + textArea1.setWrapStyleWord(true); + textArea1.setBackground(UIManager.getColor("Button.background")); + textArea1.setRows(2); + textArea1.setMaximumSize(new Dimension(2147483647, 60)); + textArea1.setRequestFocusEnabled(false); + textArea1.setEditable(false); + contentPanel.add(textArea1); //======== filterPanel ======== { @@ -204,7 +220,7 @@ public void keyReleased(KeyEvent e) { { //---- genomeList ---- - genomeList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + //genomeList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); genomeList.addMouseListener(new IGVMouseInputAdapter() { @Override public void igvMouseClicked(MouseEvent e) { @@ -227,24 +243,14 @@ public void igvMouseClicked(MouseEvent e) { //---- okButton ---- okButton.setText("OK"); - okButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - okButtonActionPerformed(e); - } - }); + okButton.addActionListener(e -> okButtonActionPerformed(e)); buttonBar.add(okButton, new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 5, 5), 0, 0)); //---- cancelButton ---- cancelButton.setText("Cancel"); - cancelButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - cancelButtonActionPerformed(e); - } - }); + cancelButton.addActionListener(e -> cancelButtonActionPerformed(e)); buttonBar.add(cancelButton, new GridBagConstraints(2, 0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 5, 0), 0, 0)); From dee67089fc76f2ac3e6a1e0fe178ac27f166ea6f Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Fri, 27 Sep 2024 11:53:03 -0700 Subject: [PATCH 055/130] BB parsing bug - wrong offset for chromTree --- src/main/java/org/broad/igv/ucsc/bb/BBFile.java | 10 ++++++---- src/main/java/org/broad/igv/ucsc/bb/ChromTree.java | 6 +++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/broad/igv/ucsc/bb/BBFile.java b/src/main/java/org/broad/igv/ucsc/bb/BBFile.java index 0267ec0115..f13756c8c2 100644 --- a/src/main/java/org/broad/igv/ucsc/bb/BBFile.java +++ b/src/main/java/org/broad/igv/ucsc/bb/BBFile.java @@ -227,11 +227,13 @@ BBHeader readHeader() throws IOException { this.totalSummary = BBTotalSummary.parseSummary(buffer); } - // Chromosome tree -- this normally preceeds fullDataOffset so will be within the buffer. However, this - // isn't guaranteed, we have to check - int chromtreeBufferSize = (int) Math.min(1000000, Math.max(10000, header.fullDataOffset - header.chromTreeOffset)); + // Chromosome tree -- we know the start offset but not the size. But we can try to estimate it. + int chromtreeBufferSize = header.fullDataOffset > header.chromTreeOffset ? + (int) Math.min(10000, header.fullDataOffset - header.chromTreeOffset) : + 10000; + buffer = UnsignedByteBufferDynamic.loadBinaryBuffer(this.path, order, header.chromTreeOffset, chromtreeBufferSize); - this.chromTree = ChromTree.parseTree(buffer, startOffset, this.genome); + this.chromTree = ChromTree.parseTree(buffer, header.chromTreeOffset, this.genome); this.chrNames = this.chromTree.names(); diff --git a/src/main/java/org/broad/igv/ucsc/bb/ChromTree.java b/src/main/java/org/broad/igv/ucsc/bb/ChromTree.java index 028f7905fa..009ba08909 100644 --- a/src/main/java/org/broad/igv/ucsc/bb/ChromTree.java +++ b/src/main/java/org/broad/igv/ucsc/bb/ChromTree.java @@ -25,7 +25,7 @@ public class ChromTree { public long sumLengths = 0; - public static ChromTree parseTree(UnsignedByteBuffer buffer, int startOffset, Genome genome) { + public static ChromTree parseTree(UnsignedByteBuffer buffer, long startOffset, Genome genome) { return (new ChromTree().parse(buffer, startOffset, genome)); } @@ -50,7 +50,7 @@ public String[] names() { return idToName; } - public ChromTree parse(UnsignedByteBuffer buffer, int startOffset, Genome genome) { + public ChromTree parse(UnsignedByteBuffer buffer, long startOffset, Genome genome) { { Header header = new Header(); header.magic = buffer.getInt(); @@ -75,7 +75,7 @@ public ChromTree parse(UnsignedByteBuffer buffer, int startOffset, Genome genome } } - void readTreeNode(long offset, final int startOffset, UnsignedByteBuffer buffer, Genome genome) { + void readTreeNode(long offset, final long startOffset, UnsignedByteBuffer buffer, Genome genome) { if (offset >= 0) buffer.position((int) offset); byte type = buffer.get(); byte reserved = buffer.get(); From 1347745dcec0afbfa24bbb7f13ac303074f789d3 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Fri, 27 Sep 2024 11:53:32 -0700 Subject: [PATCH 056/130] Don't check for "load from server" files in advance -- it can take up to 500 ms delaying startup. --- src/main/java/org/broad/igv/ui/IGVMenuBar.java | 18 ++++++++---------- .../igv/ui/action/LoadFromServerAction.java | 4 +--- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/broad/igv/ui/IGVMenuBar.java b/src/main/java/org/broad/igv/ui/IGVMenuBar.java index 67dae72c5b..93f1e808b6 100644 --- a/src/main/java/org/broad/igv/ui/IGVMenuBar.java +++ b/src/main/java/org/broad/igv/ui/IGVMenuBar.java @@ -36,10 +36,13 @@ import org.broad.igv.event.IGVEventBus; import org.broad.igv.event.IGVEventObserver; import org.broad.igv.feature.genome.Genome; +import org.broad.igv.feature.genome.GenomeListItem; import org.broad.igv.feature.genome.GenomeManager; import org.broad.igv.feature.genome.GenomeUtils; import org.broad.igv.track.AttributeManager; import org.broad.igv.track.Track; +import org.broad.igv.ui.commandbar.GenomeListManager; +import org.broad.igv.ui.commandbar.GenomeSelectionDialog; import org.broad.igv.util.GoogleUtils; import org.broad.igv.oauth.OAuthProvider; import org.broad.igv.oauth.OAuthUtils; @@ -51,7 +54,6 @@ import org.broad.igv.tools.motiffinder.MotifFinderPlugin; import org.broad.igv.track.CombinedDataSourceDialog; import org.broad.igv.ui.action.*; -import org.broad.igv.ui.commandbar.GenomeComboBox; import org.broad.igv.ui.commandbar.RemoveGenomesDialog; import org.broad.igv.ui.legend.LegendDialog; import org.broad.igv.ui.panel.FrameManager; @@ -294,7 +296,7 @@ JMenu createFileMenu() { menuAction = new LoadFromServerAction("Load From Server...", KeyEvent.VK_S, igv); menuAction.setToolTipText(UIConstants.LOAD_SERVER_DATA_TOOLTIP); loadTracksFromServerMenuItem = MenuAndToolbarUtils.createMenuItem(menuAction); - loadTracksFromServerMenuItem.setVisible(genomeId != null && LoadFromServerAction.getNodeURLs(genomeId) != null); + // loadTracksFromServerMenuItem.setVisible(genomeId != null && LoadFromServerAction.getNodeURLs(genomeId) != null); menuItems.add(loadTracksFromServerMenuItem); // Track hubs -- moved to Genomes menu @@ -432,7 +434,7 @@ private JMenu createGenomesMenu() { JMenu menu = new JMenu("Genomes"); loadGenomeFromServerMenuItem = new JMenuItem("Download Hosted Genome..."); - loadGenomeFromServerMenuItem.addActionListener(e -> GenomeComboBox.loadGenomeFromServer()); + loadGenomeFromServerMenuItem.addActionListener(e -> GenomeSelectionDialog.selectGenomesFromServer()); loadGenomeFromServerMenuItem.setToolTipText(LOAD_GENOME_SERVER_TOOLTIP); menu.add(loadGenomeFromServerMenuItem); @@ -450,7 +452,7 @@ private JMenu createGenomesMenu() { // If a file selection was made if (file != null) { - GenomeManager.getInstance().loadGenome(file.getAbsolutePath()); + Genome newGenome = GenomeManager.getInstance().loadGenome(file.getAbsolutePath()); } } catch (Exception ex) { MessageUtils.showErrorMessage(ex.getMessage(), ex); @@ -473,11 +475,9 @@ private JMenu createGenomesMenu() { MenuAction menuAction = new SelectGenomeAnnotationTracksAction("Select GenArk Tracks...", igv); selectGenomeAnnotationsItem = MenuAndToolbarUtils.createMenuItem(menuAction); - Genome genome = GenomeManager.getInstance().getCurrentGenome(); - selectGenomeAnnotationsItem.setEnabled(genome != null && genome.getHub() != null); + Genome newGenome = GenomeManager.getInstance().getCurrentGenome(); + selectGenomeAnnotationsItem.setEnabled(newGenome != null && newGenome.getHub() != null); menu.add(selectGenomeAnnotationsItem); - - menu.add(new JSeparator()); // Add genome to combo box from server @@ -1202,8 +1202,6 @@ public void receiveEvent(final IGVEvent event) { UIUtilities.invokeOnEventThread(() -> { final Genome genome = ((GenomeChangeEvent) event).genome(); encodeMenuItem.setVisible(EncodeFileBrowser.genomeSupported(genome.getId())); - loadTracksFromServerMenuItem.setVisible(LoadFromServerAction.getNodeURLs(genome.getId()) != null); - //selectGenomeAnnotationsItem .setEnabled(genome != null && genome.getHub() != null); }); } } diff --git a/src/main/java/org/broad/igv/ui/action/LoadFromServerAction.java b/src/main/java/org/broad/igv/ui/action/LoadFromServerAction.java index 2885866e7d..9403742b9a 100644 --- a/src/main/java/org/broad/igv/ui/action/LoadFromServerAction.java +++ b/src/main/java/org/broad/igv/ui/action/LoadFromServerAction.java @@ -113,9 +113,7 @@ public static LinkedHashSet getNodeURLs(String genomeId) { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is)); nodeURLs = getResourceUrls(bufferedReader); } catch (IOException e) { - //This is pretty common, if there is no data registry file for the genome the file won't exist - //log.error("Error loading genome registry file", e); - log.warn("No data found for current genome."); + //This is common and not an error, a load-from-server "data registry" file is optional } finally { if (is != null) { try { From ab536a5685612de6f4ba81e0ddc4420c72cc991d Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Fri, 27 Sep 2024 12:06:30 -0700 Subject: [PATCH 057/130] bug... --- src/main/java/org/broad/igv/ui/IGVMenuBar.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/broad/igv/ui/IGVMenuBar.java b/src/main/java/org/broad/igv/ui/IGVMenuBar.java index 93f1e808b6..62093a6df5 100644 --- a/src/main/java/org/broad/igv/ui/IGVMenuBar.java +++ b/src/main/java/org/broad/igv/ui/IGVMenuBar.java @@ -41,6 +41,7 @@ import org.broad.igv.feature.genome.GenomeUtils; import org.broad.igv.track.AttributeManager; import org.broad.igv.track.Track; +import org.broad.igv.ui.commandbar.GenomeComboBox; import org.broad.igv.ui.commandbar.GenomeListManager; import org.broad.igv.ui.commandbar.GenomeSelectionDialog; import org.broad.igv.util.GoogleUtils; @@ -434,7 +435,7 @@ private JMenu createGenomesMenu() { JMenu menu = new JMenu("Genomes"); loadGenomeFromServerMenuItem = new JMenuItem("Download Hosted Genome..."); - loadGenomeFromServerMenuItem.addActionListener(e -> GenomeSelectionDialog.selectGenomesFromServer()); + loadGenomeFromServerMenuItem.addActionListener(e -> GenomeComboBox.loadGenomeFromServer()); loadGenomeFromServerMenuItem.setToolTipText(LOAD_GENOME_SERVER_TOOLTIP); menu.add(loadGenomeFromServerMenuItem); From c7ef831eb4ee66e0e1beecb48fe5298e4caef8d0 Mon Sep 17 00:00:00 2001 From: Jim Robinson <933148+jrobinso@users.noreply.github.com> Date: Fri, 27 Sep 2024 12:36:21 -0700 Subject: [PATCH 058/130] Genome improvements (#1589) Refactoring genome load management to simplify code. --- .../org/broad/igv/feature/genome/Genome.java | 2 +- .../igv/feature/genome/GenomeManager.java | 61 ++++----- .../feature/genome/load/JsonGenomeLoader.java | 16 +++ src/main/java/org/broad/igv/ui/IGV.java | 2 +- .../java/org/broad/igv/ui/IGVMenuBar.java | 3 +- .../igv/ui/action/LoadFromURLMenuAction.java | 4 +- .../broad/igv/ui/action/UCSCGenArkAction.java | 2 + .../igv/ui/commandbar/GenomeComboBox.java | 80 +----------- .../igv/ui/commandbar/GenomeListManager.java | 50 ++++---- .../ui/commandbar/GenomeSelectionDialog.java | 119 ++++++++++++++++-- .../java/org/broad/igv/util/FileUtils.java | 2 + 11 files changed, 189 insertions(+), 152 deletions(-) diff --git a/src/main/java/org/broad/igv/feature/genome/Genome.java b/src/main/java/org/broad/igv/feature/genome/Genome.java index cf52b6ae01..00ee49f90f 100644 --- a/src/main/java/org/broad/igv/feature/genome/Genome.java +++ b/src/main/java/org/broad/igv/feature/genome/Genome.java @@ -125,7 +125,7 @@ public Genome(GenomeConfig config) throws IOException { } else if (config.fastaURL != null) { String fastaPath = config.fastaURL; String indexPath = config.indexURL; - String gziIndexPath = config.gziIndexURL; + String gziIndexPath = config.gziIndexURL != null ? config.gziIndexURL : config.compressedIndexURL; // Synonyms uncachedSequence = fastaPath.endsWith(".gz") ? new FastaBlockCompressedSequence(fastaPath, gziIndexPath, indexPath) : new FastaIndexedSequence(fastaPath, indexPath); diff --git a/src/main/java/org/broad/igv/feature/genome/GenomeManager.java b/src/main/java/org/broad/igv/feature/genome/GenomeManager.java index 6d1a13abec..2654131572 100644 --- a/src/main/java/org/broad/igv/feature/genome/GenomeManager.java +++ b/src/main/java/org/broad/igv/feature/genome/GenomeManager.java @@ -116,7 +116,9 @@ private GenomeManager() { * @throws UnsupportedEncodingException */ public static File getGenomeFile(String genomePath) throws MalformedURLException, UnsupportedEncodingException { + File archiveFile; + if (HttpUtils.isRemoteURL(genomePath.toLowerCase())) { // We need a local copy, as there is no http zip file reader URL genomeArchiveURL = HttpUtils.createURL(genomePath); @@ -195,10 +197,11 @@ public Genome loadGenome(String genomePath) throws IOException { Genome newGenome = GenomeLoader.getLoader(genomePath).loadGenome(); // Load user-defined chr aliases, if any. This is done last so they have priority + final File genomeCacheDirectory = DirectoryManager.getGenomeCacheDirectory(); try { - String aliasPath = (new File(DirectoryManager.getGenomeCacheDirectory(), newGenome.getId() + "_alias.tab")).getAbsolutePath(); + String aliasPath = (new File(genomeCacheDirectory, newGenome.getId() + "_alias.tab")).getAbsolutePath(); if (!(new File(aliasPath)).exists()) { - aliasPath = (new File(DirectoryManager.getGenomeCacheDirectory(), newGenome.getId() + "_alias.tab.txt")).getAbsolutePath(); + aliasPath = (new File(genomeCacheDirectory, newGenome.getId() + "_alias.tab.txt")).getAbsolutePath(); } if ((new File(aliasPath)).exists()) { newGenome.addChrAliases(ChromAliasParser.loadChrAliases(aliasPath)); @@ -212,7 +215,13 @@ public Genome loadGenome(String genomePath) throws IOException { IGV.getInstance().resetSession(null); } - setCurrentGenome(genomePath, newGenome); + // If the genome is loaded from anywhere other than the cache directory add an entry to the pulldown + if(newGenome != null && !(new File(genomePath).getParentFile().equals(genomeCacheDirectory))) { + GenomeListItem genomeListItem = new GenomeListItem(newGenome.getDisplayName(), genomePath, newGenome.getId()); + GenomeListManager.getInstance().addGenomeItem(genomeListItem, true); + } + + setCurrentGenome(newGenome); return currentGenome; @@ -226,13 +235,7 @@ public Genome loadGenome(String genomePath) throws IOException { } } - public void setCurrentGenome(String genomePath, Genome newGenome) { - - GenomeListItem genomeListItem = new GenomeListItem(newGenome.getDisplayName(), genomePath, newGenome.getId()); - final Set serverGenomeIDs = genomeListManager.getServerGenomeIDs(); - - boolean userDefined = !serverGenomeIDs.contains(newGenome.getId()); - genomeListManager.addGenomeItem(genomeListItem, userDefined); + public void setCurrentGenome(Genome newGenome) { this.currentGenome = newGenome; @@ -354,26 +357,32 @@ public Genome getCurrentGenome() { } - public boolean downloadGenome(GenomeListItem item, boolean downloadSequence) { + public boolean downloadGenome(GenomeListItem item, boolean downloadDataFiles) { boolean success; try { - File genomeFile = getGenomeFile(item.getPath()); // Has side affect of downloading .genome file - if (downloadSequence) { - String fastaPath = null; + File genomeFile = getGenomeFile(item.getPath()); // Has side affect of downloading .genome file + + if (downloadDataFiles) { + + if (item.getPath().endsWith(".genome")) { GenomeDescriptor genomeDescriptor = GenomeDescriptor.parseGenomeArchiveFile(genomeFile); - fastaPath = genomeDescriptor.getSequencePath(); + String fastaPath = genomeDescriptor.getSequencePath(); + if (fastaPath != null && FileUtils.isRemote(fastaPath)) { + File localFile = downloadFasta(fastaPath); + if (localFile != null) { + addLocalFasta(item.getId(), localFile); + } + } + } else if (item.getPath().endsWith(".json")) { + JsonGenomeLoader.GenomeDescriptor desc = (new JsonGenomeLoader(item.getPath())).loadDescriptor(); - fastaPath = desc.getFastaURL(); - } - if (fastaPath != null && FileUtils.isRemote(fastaPath)) { - File localFile = downloadFasta(fastaPath); - if (localFile != null) { - addLocalFasta(item.getId(), localFile); - } + + } + } success = true; @@ -391,7 +400,6 @@ public boolean downloadGenome(GenomeListItem item, boolean downloadSequence) { } return success; - } @@ -466,11 +474,4 @@ private static void updateSequenceMapFile() { } } - public void refreshHostedGenome(String genomeId) { - - Map itemMap = GenomeListManager.getInstance().getServerGenomeMap(); - if (itemMap.containsKey(genomeId)) { - downloadGenome(itemMap.get(genomeId), false); - } - } } diff --git a/src/main/java/org/broad/igv/feature/genome/load/JsonGenomeLoader.java b/src/main/java/org/broad/igv/feature/genome/load/JsonGenomeLoader.java index c4c87db379..d2c8499dba 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/JsonGenomeLoader.java +++ b/src/main/java/org/broad/igv/feature/genome/load/JsonGenomeLoader.java @@ -98,6 +98,19 @@ private String fixChromosomeOrder(String jsonString) { * @return */ private GenomeConfig fixPaths(GenomeConfig config) { + + if (config.chromAliasBbURL != null) { + config.chromAliasBbURL = FileUtils.getAbsolutePath(config.chromAliasBbURL, genomePath); + } + if (config.twoBitURL != null) { + config.twoBitURL = FileUtils.getAbsolutePath(config.twoBitURL, genomePath); + } + if (config.cytobandBbURL != null) { + config.cytobandBbURL = FileUtils.getAbsolutePath(config.cytobandBbURL, genomePath); + } + if (config.chromSizesURL != null) { + config.chromSizesURL = FileUtils.getAbsolutePath(config.chromSizesURL, genomePath); + } if (config.fastaURL != null) { config.fastaURL = FileUtils.getAbsolutePath(config.fastaURL, genomePath); } @@ -107,6 +120,9 @@ private GenomeConfig fixPaths(GenomeConfig config) { if (config.gziIndexURL != null) { config.gziIndexURL = FileUtils.getAbsolutePath(config.gziIndexURL, genomePath); } + if (config.compressedIndexURL != null) { + config.compressedIndexURL = FileUtils.getAbsolutePath(config.compressedIndexURL, genomePath); + } if (config.cytobandURL != null) { config.cytobandURL = FileUtils.getAbsolutePath(config.cytobandURL, genomePath); } diff --git a/src/main/java/org/broad/igv/ui/IGV.java b/src/main/java/org/broad/igv/ui/IGV.java index a9ad28bf21..3c1f3e3a3e 100644 --- a/src/main/java/org/broad/igv/ui/IGV.java +++ b/src/main/java/org/broad/igv/ui/IGV.java @@ -1943,7 +1943,7 @@ public void run() { if (!genomeLoaded) { Genome genome = Genome.nullGenome(); - GenomeManager.getInstance().setCurrentGenome("", genome); + GenomeManager.getInstance().setCurrentGenome(genome); //GenomeManager.getInstance().setCurrentGenome(Genome.NoneGenome()); diff --git a/src/main/java/org/broad/igv/ui/IGVMenuBar.java b/src/main/java/org/broad/igv/ui/IGVMenuBar.java index 62093a6df5..93f1e808b6 100644 --- a/src/main/java/org/broad/igv/ui/IGVMenuBar.java +++ b/src/main/java/org/broad/igv/ui/IGVMenuBar.java @@ -41,7 +41,6 @@ import org.broad.igv.feature.genome.GenomeUtils; import org.broad.igv.track.AttributeManager; import org.broad.igv.track.Track; -import org.broad.igv.ui.commandbar.GenomeComboBox; import org.broad.igv.ui.commandbar.GenomeListManager; import org.broad.igv.ui.commandbar.GenomeSelectionDialog; import org.broad.igv.util.GoogleUtils; @@ -435,7 +434,7 @@ private JMenu createGenomesMenu() { JMenu menu = new JMenu("Genomes"); loadGenomeFromServerMenuItem = new JMenuItem("Download Hosted Genome..."); - loadGenomeFromServerMenuItem.addActionListener(e -> GenomeComboBox.loadGenomeFromServer()); + loadGenomeFromServerMenuItem.addActionListener(e -> GenomeSelectionDialog.selectGenomesFromServer()); loadGenomeFromServerMenuItem.setToolTipText(LOAD_GENOME_SERVER_TOOLTIP); menu.add(loadGenomeFromServerMenuItem); diff --git a/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java b/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java index 0a07edb7a8..355bd8ff59 100644 --- a/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java +++ b/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java @@ -30,6 +30,7 @@ package org.broad.igv.ui.action; import org.broad.igv.Globals; +import org.broad.igv.feature.genome.Genome; import org.broad.igv.feature.genome.load.HubGenomeLoader; import org.broad.igv.logging.*; import org.broad.igv.feature.genome.GenomeManager; @@ -81,6 +82,7 @@ public void actionPerformed(ActionEvent e) { if (!dlg.isCanceled()) { + String inputURLs = dlg.getFileURL(); if (inputURLs != null && inputURLs.trim().length() > 0) { @@ -89,7 +91,7 @@ public void actionPerformed(ActionEvent e) { if (inputs.length == 1 && HubGenomeLoader.isHubURL(inputs[0])) { LongRunningTask.submit(() -> { try { - GenomeManager.getInstance().loadGenome(inputs[0]); + Genome newGenome = GenomeManager.getInstance().loadGenome(inputs[0]); } catch (IOException ex) { log.error("Error loading tack hub", ex); MessageUtils.showMessage("Error loading track hub: " + ex.getMessage()); diff --git a/src/main/java/org/broad/igv/ui/action/UCSCGenArkAction.java b/src/main/java/org/broad/igv/ui/action/UCSCGenArkAction.java index c159bc00cc..785d23a307 100644 --- a/src/main/java/org/broad/igv/ui/action/UCSCGenArkAction.java +++ b/src/main/java/org/broad/igv/ui/action/UCSCGenArkAction.java @@ -27,12 +27,14 @@ import org.broad.igv.Globals; import org.broad.igv.feature.genome.Genome; +import org.broad.igv.feature.genome.GenomeListItem; import org.broad.igv.feature.genome.GenomeManager; import org.broad.igv.feature.genome.load.HubGenomeLoader; import org.broad.igv.logging.LogManager; import org.broad.igv.logging.Logger; import org.broad.igv.track.AttributeManager; import org.broad.igv.ui.IGV; +import org.broad.igv.ui.commandbar.GenomeListManager; import org.broad.igv.ui.table.SearchableTableDialog; import org.broad.igv.ui.table.SearchableTableModel; import org.broad.igv.ui.table.SearchableTableRecord; diff --git a/src/main/java/org/broad/igv/ui/commandbar/GenomeComboBox.java b/src/main/java/org/broad/igv/ui/commandbar/GenomeComboBox.java index f9cc1fbf80..1b5fe42332 100644 --- a/src/main/java/org/broad/igv/ui/commandbar/GenomeComboBox.java +++ b/src/main/java/org/broad/igv/ui/commandbar/GenomeComboBox.java @@ -1,15 +1,11 @@ package org.broad.igv.ui.commandbar; import org.broad.igv.logging.*; -import org.broad.igv.DirectoryManager; -import org.broad.igv.event.GenomeResetEvent; -import org.broad.igv.event.IGVEventBus; import org.broad.igv.feature.genome.GenomeListItem; import org.broad.igv.feature.genome.GenomeManager; import org.broad.igv.ui.IGV; import org.broad.igv.ui.UIConstants; import org.broad.igv.ui.util.MessageUtils; -import org.broad.igv.ui.util.UIUtilities; import org.broad.igv.util.LongRunningTask; import javax.swing.*; @@ -17,10 +13,8 @@ import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import java.io.File; import java.io.IOException; import java.util.*; -import java.util.List; /** * Created by jrobinso on 7/6/17. @@ -100,7 +94,7 @@ private void loadGenomeListItem(final GenomeListItem genomeListItem) { if (genomeListItem != null && genomeListItem.getPath() != null) { if (genomeListItem == GenomeListItem.DOWNLOAD_ITEM) { - loadGenomeFromServer(); + GenomeSelectionDialog.selectGenomesFromServer(); } else { try { @@ -197,76 +191,4 @@ public Component getListCellRendererComponent(JList list, Object value, int inde } - /** - * Open a selection list to load a genome from the server. This method is static because its used by multiple - * UI elements (menu bar and genome selection pulldown). - */ - public static void loadGenomeFromServer() { - - Runnable showDialog = () -> { - - Collection inputListItems = GenomeListManager.getInstance().getServerGenomeList(); - if (inputListItems == null) { - return; - } - GenomeSelectionDialog dialog = new GenomeSelectionDialog(IGV.getInstance().getMainFrame(), inputListItems); - UIUtilities.invokeAndWaitOnEventThread(() -> dialog.setVisible(true)); - - if (dialog.isCanceled()) { - IGVEventBus.getInstance().post(new GenomeResetEvent()); - } else { - List selectedValueList = dialog.getSelectedValues(); - GenomeListItem firstItem = null; - for (GenomeListItem selectedValue : selectedValueList) { - if (selectedValue != null) { - boolean downloadSequence = false; - boolean success = GenomeManager.getInstance().downloadGenome(selectedValue, downloadSequence); - if (success) { - GenomeListManager.getInstance().addServerGenomeItem(selectedValue); - firstItem = selectedValue; - } - } - } - if (firstItem != null && selectedValueList.size() == 1) { - try { - - GenomeManager.getInstance().loadGenome(firstItem.getPath()); - // If the user has previously defined this genome, remove it. - GenomeListManager.getInstance().removeUserDefinedGenome(firstItem.getId()); - - // If this is a .json genome, attempt to remove existing .genome files - if (firstItem.getPath().endsWith(".json")) { - removeDotGenomeFile(firstItem.getId()); - } - - - } catch (IOException e) { - GenomeListManager.getInstance().removeGenomeListItem(firstItem); - MessageUtils.showErrorMessage("Error loading genome " + firstItem.getDisplayableName(), e); - log.error("Error loading genome " + firstItem.getDisplayableName(), e); - } - } - } - }; - - if (SwingUtilities.isEventDispatchThread()) { - LongRunningTask.submit(showDialog); - } else { - showDialog.run(); - } - } - - public static void removeDotGenomeFile(String id) { - try { - File dotGenomeFile = new File(DirectoryManager.getGenomeCacheDirectory(), id + ".genome"); - if (dotGenomeFile.exists()) { - dotGenomeFile.delete(); - } - } catch (Exception e) { - // If anything goes wrong, just log it, this cleanup is not essential - log.error("Error deleting .genome file", e); - } - } - - } diff --git a/src/main/java/org/broad/igv/ui/commandbar/GenomeListManager.java b/src/main/java/org/broad/igv/ui/commandbar/GenomeListManager.java index 89501936a5..b6bab49160 100644 --- a/src/main/java/org/broad/igv/ui/commandbar/GenomeListManager.java +++ b/src/main/java/org/broad/igv/ui/commandbar/GenomeListManager.java @@ -31,7 +31,6 @@ */ public class GenomeListManager { - public static final String BACKUP_GENOME_SERVER_URL = "https://s3.amazonaws.com/igv.org.genomes/genomes.tsv"; private static Logger log = LogManager.getLogger(GenomeListManager.class); private static GenomeListManager theInstance; @@ -40,7 +39,7 @@ public class GenomeListManager { public static final GenomeListItem DEFAULT_GENOME = new GenomeListItem( "Human (hg38)", - "https://igv-genepattern-org.s3.amazonaws.com/genomes/hg38/hg38.json", + "https://igv.org/genomes/hg38/hg38.json", "hg38"); private Map genomeItemMap; @@ -114,13 +113,6 @@ public void addGenomeItem(GenomeListItem genomeListItem, boolean userDefined) { } } - /** - * Add a server-hosted genome list to the selectables map - */ - public void addServerGenomeItem(GenomeListItem genomeListItem) { - addGenomeItem(genomeListItem, false); - } - /** * Return genome list item from currently selectable set. To search through * all server and user defined genomes, use {@link #getGenomeListItem(String)} @@ -258,15 +250,28 @@ private static Map getCachedGenomeList() { JsonObject json = rootElement.getAsJsonObject(); JsonElement id = json.get("id"); JsonElement name = json.get("name"); - JsonElement fastaURL = json.get("fastaURL"); - if (id != null && name != null && fastaURL != null) { - if (cachedGenomes.containsKey(id.getAsString())) { - File prevFile = new File(cachedGenomes.get(id.getAsString()).getPath()); - prevFile.delete(); - } - GenomeListItem item = new GenomeListItem(name.getAsString(), file.getAbsolutePath(), id.getAsString()); - cachedGenomes.put(item.getId(), item); + JsonElement fasta = json.get("fastaURL"); + JsonElement twobit = json.get("twoBitURL"); + if (id == null) { + log.error("Error parsing " + file.getName() + ". \"id\" is required"); + continue; + } + if (name == null) { + log.error("Error parsing " + file.getName() + ". \"name\" is required"); + continue; + } + if (id == null) { + log.error("Error parsing " + file.getName() + ". \"id\" is required"); + continue; + } + if (fasta == null && twobit == null) { + log.error("Error parsing " + file.getName() + ". One of either \"fastaURL\" or \"twoBitURL\" is required"); + continue; } + + GenomeListItem item = new GenomeListItem(name.getAsString(), file.getAbsolutePath(), id.getAsString()); + cachedGenomes.put(item.getId(), item); + } } catch (Exception e) { log.error("Error parsing genome json: " + file.getAbsolutePath(), e); @@ -316,7 +321,7 @@ public void removeGenomeListItem(GenomeListItem genomeListItem) { // If this is a cached genome remove it from cache Map cachedItems = getCachedGenomeList(); - if(cachedItems.containsKey(id)){ + if (cachedItems.containsKey(id)) { try { (new File(genomeListItem.getPath())).delete(); } catch (Exception e) { @@ -364,14 +369,7 @@ public Map getServerGenomeMap() { genomeListURLString = PreferencesManager.getPreferences().getGenomeListURL(); if (HttpUtils.isRemoteURL(genomeListURLString)) { URL serverGenomeURL = HttpUtils.createURL(genomeListURLString); - try { - inputStream = HttpUtils.getInstance().openConnectionStream(serverGenomeURL); - } catch (IOException e) { - log.error(e); - MessageUtils.showMessage("Error: Could not connect to genome server " + genomeListURLString + "
Trying " + BACKUP_GENOME_SERVER_URL); - serverGenomeURL = HttpUtils.createURL(BACKUP_GENOME_SERVER_URL); - inputStream = HttpUtils.getInstance().openConnectionStream(serverGenomeURL); - } + inputStream = HttpUtils.getInstance().openConnectionStream(serverGenomeURL); } else { File file = new File(genomeListURLString.startsWith("file:") ? (new URL(genomeListURLString)).getFile() : genomeListURLString); inputStream = new FileInputStream(file); diff --git a/src/main/java/org/broad/igv/ui/commandbar/GenomeSelectionDialog.java b/src/main/java/org/broad/igv/ui/commandbar/GenomeSelectionDialog.java index 096566b4d5..8a9a0b570a 100644 --- a/src/main/java/org/broad/igv/ui/commandbar/GenomeSelectionDialog.java +++ b/src/main/java/org/broad/igv/ui/commandbar/GenomeSelectionDialog.java @@ -31,14 +31,25 @@ package org.broad.igv.ui.commandbar; +import org.broad.igv.DirectoryManager; +import org.broad.igv.event.GenomeResetEvent; +import org.broad.igv.event.IGVEventBus; import org.broad.igv.feature.genome.GenomeListItem; +import org.broad.igv.feature.genome.GenomeManager; +import org.broad.igv.logging.LogManager; +import org.broad.igv.logging.Logger; +import org.broad.igv.ui.IGV; import org.broad.igv.ui.util.IGVMouseInputAdapter; +import org.broad.igv.ui.util.MessageUtils; import org.broad.igv.ui.util.UIUtilities; +import org.broad.igv.util.LongRunningTask; import javax.swing.*; import javax.swing.border.EmptyBorder; import java.awt.*; import java.awt.event.*; +import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -49,6 +60,8 @@ */ public class GenomeSelectionDialog extends org.broad.igv.ui.IGVDialog { + private static Logger log = LogManager.getLogger(GenomeSelectionDialog.class); + private JPanel dialogPane; private JPanel contentPanel; private JTextArea textArea1; @@ -57,6 +70,7 @@ public class GenomeSelectionDialog extends org.broad.igv.ui.IGVDialog { private JTextField genomeFilter; private JScrollPane scrollPane1; private JList genomeList; + private JCheckBox downloadDataCB; private JPanel buttonBar; private JButton okButton; private JButton cancelButton; @@ -66,13 +80,88 @@ public class GenomeSelectionDialog extends org.broad.igv.ui.IGVDialog { private List allListItems; private DefaultListModel genomeListModel; + + /** + * Open a selection list to load a genome from the server. This method is static because its used by multiple + * UI elements (menu bar and genome selection pulldown). + */ + public static void selectGenomesFromServer() { + + Runnable showDialog = () -> { + + Collection inputListItems = GenomeListManager.getInstance().getServerGenomeList(); + if (inputListItems == null) { + return; + } + GenomeSelectionDialog dialog = new GenomeSelectionDialog(IGV.getInstance().getMainFrame(), inputListItems); + UIUtilities.invokeAndWaitOnEventThread(() -> dialog.setVisible(true)); + + if (dialog.isCanceled()) { + IGVEventBus.getInstance().post(new GenomeResetEvent()); + } else { + List selectedValueList = dialog.getSelectedValues(); + + for (GenomeListItem selectedValue : selectedValueList) { + if (selectedValue != null) { + + boolean downloadData = dialog.isDownloadData(); + boolean success = GenomeManager.getInstance().downloadGenome(selectedValue, downloadData); + + if (success) { + + // If this is a single selection load it + if(selectedValueList.size() == 1) { + + try { + GenomeManager.getInstance().loadGenome(selectedValue.getPath()); + // If the user has previously defined this genome, remove it. + GenomeListManager.getInstance().removeUserDefinedGenome(selectedValue.getId()); + + // If this is a .json genome, attempt to remove existing .genome files + if (selectedValue.getPath().endsWith(".json")) { + removeDotGenomeFile(selectedValue.getId()); + } + + + } catch (IOException e) { + GenomeListManager.getInstance().removeGenomeListItem(selectedValue); + MessageUtils.showErrorMessage("Error loading genome " + selectedValue.getDisplayableName(), e); + log.error("Error loading genome " + selectedValue.getDisplayableName(), e); + } + } + } + } + } + } + }; + + if (SwingUtilities.isEventDispatchThread()) { + LongRunningTask.submit(showDialog); + } else { + showDialog.run(); + } + } + + private static void removeDotGenomeFile(String id) { + try { + File dotGenomeFile = new File(DirectoryManager.getGenomeCacheDirectory(), id + ".genome"); + if (dotGenomeFile.exists()) { + dotGenomeFile.delete(); + } + } catch (Exception e) { + // If anything goes wrong, just log it, this cleanup is not essential + log.error("Error deleting .genome file", e); + } + } + /** * @param parent */ - public GenomeSelectionDialog(java.awt.Frame parent, Collection inputListItems) { + private GenomeSelectionDialog(java.awt.Frame parent, Collection inputListItems) { super(parent); initComponents(); initData(inputListItems); + downloadDataCB.setVisible(true); } private void initData(Collection inputListItems) { @@ -99,18 +188,12 @@ private void rebuildGenomeList(String filterText) { } /** - * If a genome is single clicked, we just store the selection. - * When a genome is double clicked, we treat that as the user - * wanting to load the genome. + * Double-clicking will trigger the OK action. * * @param e */ private void genomeListMouseClicked(MouseEvent e) { - switch (e.getClickCount()) { - case 1: - List selValues = genomeList.getSelectedValuesList(); - break; - case 2: + if (e.getClickCount() == 2) { okButtonActionPerformed(null); } } @@ -123,18 +206,22 @@ public List getSelectedValues() { return selectedValues; } + public boolean isDownloadData(){ + return downloadDataCB.isSelected(); + } + public boolean isCanceled() { return isCanceled; } - private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cancelButtonActionPerformed + private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) { isCanceled = true; selectedValues = null; setVisible(false); dispose(); } - private void okButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_okButtonActionPerformed + private void okButtonActionPerformed(java.awt.event.ActionEvent evt) { isCanceled = false; selectedValues = genomeList.getSelectedValuesList(); setVisible(false); @@ -151,6 +238,7 @@ private void initComponents() { genomeFilter = new JTextField(); scrollPane1 = new JScrollPane(); genomeList = new JList(); + downloadDataCB = new JCheckBox(); buttonBar = new JPanel(); okButton = new JButton(); cancelButton = new JButton(); @@ -231,6 +319,14 @@ public void igvMouseClicked(MouseEvent e) { } contentPanel.add(scrollPane1); + //---- downloadSequenceCB ---- + downloadDataCB.setText("Download data files"); + downloadDataCB.setAlignmentX(1.0F); + downloadDataCB.setToolTipText("Download all files referenced by the genome definition, including the full sequence for this organism. Note that these files can be very large (human is about 3 gigabytes)"); + downloadDataCB.setMaximumSize(new Dimension(1000, 23)); + downloadDataCB.setPreferredSize(new Dimension(300, 23)); + downloadDataCB.setMinimumSize(new Dimension(300, 23)); + contentPanel.add(downloadDataCB); } dialogPane.add(contentPanel, BorderLayout.CENTER); @@ -262,5 +358,4 @@ public void igvMouseClicked(MouseEvent e) { setLocationRelativeTo(getOwner()); }// //GEN-END:initComponents - } diff --git a/src/main/java/org/broad/igv/util/FileUtils.java b/src/main/java/org/broad/igv/util/FileUtils.java index 3c64092151..ba0bb7a064 100644 --- a/src/main/java/org/broad/igv/util/FileUtils.java +++ b/src/main/java/org/broad/igv/util/FileUtils.java @@ -364,9 +364,11 @@ public static String getFileExtension(String filePath) { * @return */ public static String getAbsolutePath(String inputPath, String referencePath) { + if (isRemote(inputPath) || referencePath == null) { return inputPath; } + File inFile = new File(inputPath); if (inFile.isAbsolute()) { return inFile.getAbsolutePath(); From dedd7fc9f31742985fda29961c357bfa6b6b64e4 Mon Sep 17 00:00:00 2001 From: Louis Bergelson Date: Fri, 27 Sep 2024 16:40:47 -0400 Subject: [PATCH 059/130] Remove Snappy to avoid issues with notarization (#1571) * update htsjd 4.1.1 -> 4.1.2 which makes Snappy optional --- build.gradle | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 1508dffb43..ab85c10394 100644 --- a/build.gradle +++ b/build.gradle @@ -104,6 +104,8 @@ configurations { exclude group: 'xalan', module: 'serializer' exclude group: 'xalan', module: 'xalan' + exclude group: 'org.xerial.snappy', module: 'snappy-java' + // Amazon deps exclusions //exclude group: 'software.amazon', module: 'flow' //exclude group: 'software.amazon.awssdk', module: 'annotations' @@ -122,7 +124,7 @@ dependencies { [group: 'org.apache.commons', name: 'commons-compress', version: '1.26.0'], [group: 'org.apache.commons', name: 'commons-jexl', version: '2.1.1'], [group: 'org.apache.commons', name: 'commons-lang3', version: '3.9'], - [group: 'com.github.samtools', name: 'htsjdk', version: '4.1.1'], + [group: 'com.github.samtools', name: 'htsjdk', version: '4.1.2'], [group: 'org.swinglabs', name: 'swing-layout', version: '1.0.3'], [group: 'com.formdev', name: 'jide-oss', version: '3.7.12'], [group: 'com.google.guava', name: 'guava', version: '32.1.3-jre'], @@ -359,7 +361,8 @@ tasks.register('createMacAppWithJavaDistZip', Zip) { } } -task createWinDist(type: Copy, dependsOn: createDist) { +tasks.register('createWinDist', Copy) { + dependsOn createDist with copySpec { from("${buildDir}/IGV-dist") { exclude "*.sh" @@ -440,7 +443,8 @@ tasks.register('signWinExeDist', Exec) { } } -task signWinExeWithJavaDist(type: Exec, dependsOn: createWinWithJavaExeDist) { +tasks.register('signWinExeWithJavaDist', Exec) { + dependsOn createWinWithJavaExeDist standardInput = new ByteArrayInputStream(keyPassword.getBytes()); commandLine(signcodeCommand, "-spc", spcFile, "-v", pvkFile, "-a", "sha512", "-\$", "commercial", "-n", "IGV ${version}", "-i", "http://www.igv.org/", @@ -452,7 +456,8 @@ task signWinExeWithJavaDist(type: Exec, dependsOn: createWinWithJavaExeDist) { } } -task fullJar(type: Jar, dependsOn: jar) { +tasks.register('fullJar', Jar) { + dependsOn jar // Based on https://discuss.gradle.org/t/removing-dependencies-from-a-jar-file-during-jar-task/5521/3 from { ((configurations.compile - configurations.default) + "${buildDir}/libs/igv.jar").collect { From 2838c3056969d7c30aa00d9251f10f4f4312c149 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:20:06 -0700 Subject: [PATCH 060/130] Bug in chromosome aliasing affecting genbank files. See #1573 --- src/main/java/org/broad/igv/feature/genome/ChromAlias.java | 1 - src/main/java/org/broad/igv/feature/genome/ChromAliasBB.java | 4 ++-- .../java/org/broad/igv/feature/genome/ChromAliasDefaults.java | 2 +- .../java/org/broad/igv/feature/genome/ChromAliasFile.java | 2 +- .../java/org/broad/igv/feature/genome/ChromAliasSource.java | 3 +++ .../java/org/broad/igv/feature/genome/ChromAliasBBTest.java | 1 - .../java/org/broad/igv/feature/genome/ChromAliasFileTest.java | 1 - 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/broad/igv/feature/genome/ChromAlias.java b/src/main/java/org/broad/igv/feature/genome/ChromAlias.java index 88e1e34f64..2dac9e06de 100644 --- a/src/main/java/org/broad/igv/feature/genome/ChromAlias.java +++ b/src/main/java/org/broad/igv/feature/genome/ChromAlias.java @@ -12,7 +12,6 @@ public class ChromAlias { public ChromAlias(String chr) { this.chr = chr; this.aliases = new HashMap<>(); - this.aliases.put("chr", chr); // "chr" is the name set } public String getChr() { diff --git a/src/main/java/org/broad/igv/feature/genome/ChromAliasBB.java b/src/main/java/org/broad/igv/feature/genome/ChromAliasBB.java index 46f57fac62..da99a698c4 100644 --- a/src/main/java/org/broad/igv/feature/genome/ChromAliasBB.java +++ b/src/main/java/org/broad/igv/feature/genome/ChromAliasBB.java @@ -24,14 +24,14 @@ public ChromAliasBB(String path, Genome genome) throws IOException { */ public String getChromosomeName(String alias) { if(aliasCache.containsKey(alias)) { - return aliasCache.get(alias).get("chr"); + return aliasCache.get(alias).getChr(); } else { try { ChromAlias aliasRecord = search(alias); if(aliasRecord == null) { aliasRecord.put(alias, null); // Prevents future attempts } else { - return aliasRecord.get("chr"); + return aliasRecord.getChr(); } } catch (IOException e) { // TODO -- log diff --git a/src/main/java/org/broad/igv/feature/genome/ChromAliasDefaults.java b/src/main/java/org/broad/igv/feature/genome/ChromAliasDefaults.java index 9a10e51011..9649675088 100644 --- a/src/main/java/org/broad/igv/feature/genome/ChromAliasDefaults.java +++ b/src/main/java/org/broad/igv/feature/genome/ChromAliasDefaults.java @@ -125,7 +125,7 @@ private void init(String id, List chromosomeNames) { */ @Override public String getChromosomeName(String alias) { - return this.aliasCache.containsKey(alias) ? this.aliasCache.get(alias).get("chr") : alias; + return this.aliasCache.containsKey(alias) ? this.aliasCache.get(alias).getChr() : alias; } /** diff --git a/src/main/java/org/broad/igv/feature/genome/ChromAliasFile.java b/src/main/java/org/broad/igv/feature/genome/ChromAliasFile.java index 261fbc796c..7189d12069 100644 --- a/src/main/java/org/broad/igv/feature/genome/ChromAliasFile.java +++ b/src/main/java/org/broad/igv/feature/genome/ChromAliasFile.java @@ -106,7 +106,7 @@ public ChromAliasFile(List> chromAliases, List chromosomeNa * @returns {*} */ public String getChromosomeName(String alias) { - return this.aliasCache.containsKey(alias) ? this.aliasCache.get(alias).get("chr") : alias; + return this.aliasCache.containsKey(alias) ? this.aliasCache.get(alias).getChr() : alias; } /** diff --git a/src/main/java/org/broad/igv/feature/genome/ChromAliasSource.java b/src/main/java/org/broad/igv/feature/genome/ChromAliasSource.java index b972d3d865..1992f289cd 100644 --- a/src/main/java/org/broad/igv/feature/genome/ChromAliasSource.java +++ b/src/main/java/org/broad/igv/feature/genome/ChromAliasSource.java @@ -26,5 +26,8 @@ public ChromAliasSource() { */ public void add(ChromAlias chromAlias) { aliasCache.put(chromAlias.getChr(), chromAlias); + for(String alias : chromAlias.values()) { + aliasCache.put(alias, chromAlias); + } } } diff --git a/src/test/java/org/broad/igv/feature/genome/ChromAliasBBTest.java b/src/test/java/org/broad/igv/feature/genome/ChromAliasBBTest.java index dd5275aa80..f39f8316f9 100644 --- a/src/test/java/org/broad/igv/feature/genome/ChromAliasBBTest.java +++ b/src/test/java/org/broad/igv/feature/genome/ChromAliasBBTest.java @@ -34,7 +34,6 @@ public void search() throws IOException { String path = "test/data/genomes/GCF_000002655.1.chromAlias.bb"; ChromAliasSource chromAliasSource = new ChromAliasBB(path, mockGenome); ChromAlias chromAlias = chromAliasSource.search("1"); - assertEquals(chromAlias.get("chr"), "NC_007194.1"); assertEquals(chromAlias.get("genbank"), "CM000169.1") ; assertEquals(chromAlias.get("ncbi"), "1"); assertEquals(chromAlias.get("ucsc"), "chr1"); diff --git a/src/test/java/org/broad/igv/feature/genome/ChromAliasFileTest.java b/src/test/java/org/broad/igv/feature/genome/ChromAliasFileTest.java index b83faf4fef..c4f1e32087 100644 --- a/src/test/java/org/broad/igv/feature/genome/ChromAliasFileTest.java +++ b/src/test/java/org/broad/igv/feature/genome/ChromAliasFileTest.java @@ -37,7 +37,6 @@ public void search() throws IOException { ChromAliasFile chromAliasSource = new ChromAliasFile(path, mockGenome.getChromosomeNames()); ChromAlias chromAlias = chromAliasSource.search("1"); assertEquals(chromAlias.getChr(), "NC_007194.1"); - assertEquals(chromAlias.get("chr"), "NC_007194.1"); assertEquals(chromAlias.get("genbank"), "CM000169.1") ; assertEquals(chromAlias.get("ncbi"), "1"); assertEquals(chromAlias.get("ucsc"), "chr1"); From 5629b4360d64017f5725c172b24d93b701dee2e6 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:22:57 -0700 Subject: [PATCH 061/130] * Catch errors in sampling alignments to determine experiment type. * Null check in IGVReferenceSource -- can occur when alignments in CRAM files reference sequences no present in loaded genome. * Revert name change to menu item for downloading hosted genomes --- .../broad/igv/sam/AlignmentDataManager.java | 47 +++++++++++-------- .../org/broad/igv/sam/AlignmentUtils.java | 5 +- .../igv/sam/cram/IGVReferenceSource.java | 11 +++-- .../java/org/broad/igv/ui/IGVMenuBar.java | 2 +- 4 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/main/java/org/broad/igv/sam/AlignmentDataManager.java b/src/main/java/org/broad/igv/sam/AlignmentDataManager.java index 97e7412543..575f5d07cd 100644 --- a/src/main/java/org/broad/igv/sam/AlignmentDataManager.java +++ b/src/main/java/org/broad/igv/sam/AlignmentDataManager.java @@ -378,29 +378,36 @@ private void updateBaseModfications(List alignments) { } public AlignmentTrack.ExperimentType inferType() { - if (this.inferredType != null) { - return this.inferredType; - } - - ReadStats readStats = new ReadStats(); - List sample = AlignmentUtils.firstAlignments(reader, 100); - for (Alignment a : sample) { - readStats.addAlignment(a); - } - inferredType = readStats.inferType(); - if (inferredType == AlignmentTrack.ExperimentType.THIRD_GEN) { - return inferredType; - } else { - // Get a larger sample to distinguish RNA-Seq - readStats = new ReadStats(); - sample = AlignmentUtils.firstAlignments(reader, 2000); - for (Alignment a : sample) { - readStats.addAlignment(a); + if (this.inferredType == null) { + try { + ReadStats readStats = new ReadStats(); + List sample = AlignmentUtils.firstAlignments(reader, 100); + for (Alignment a : sample) { + readStats.addAlignment(a); + } + inferredType = readStats.inferType(); + + if (inferredType == AlignmentTrack.ExperimentType.THIRD_GEN) { + return inferredType; + } else { + // Get a larger sample to distinguish RNA-Seq + readStats = new ReadStats(); + sample = AlignmentUtils.firstAlignments(reader, 2000); + for (Alignment a : sample) { + readStats.addAlignment(a); + } + inferredType = readStats.inferType(); + return inferredType; + } + } catch (Exception e) { + // Ignore errors, inferring alignment type is not essential. + log.error("Error inferring alignment type", e); + inferredType = AlignmentTrack.ExperimentType.UNKOWN; } - inferredType = readStats.inferType(); - return inferredType; } + + return inferredType; } private AlignmentTrack.ExperimentType getExperimentType() { diff --git a/src/main/java/org/broad/igv/sam/AlignmentUtils.java b/src/main/java/org/broad/igv/sam/AlignmentUtils.java index 7fcfcbf269..ece7461492 100644 --- a/src/main/java/org/broad/igv/sam/AlignmentUtils.java +++ b/src/main/java/org/broad/igv/sam/AlignmentUtils.java @@ -233,7 +233,7 @@ public static AlignmentBlockImpl buildAlignmentBlock(char operator, } - public static List firstAlignments(AlignmentReader reader, int count) { + public static List firstAlignments(AlignmentReader reader, int count) throws IOException { List alignments = new ArrayList<>(); CloseableIterator iter = null; @@ -244,12 +244,9 @@ public static List firstAlignments(AlignmentReader reader, int count) alignments.add(iter.next()); } return alignments; - } catch (IOException e) { - } finally { iter.close(); } - return alignments; } diff --git a/src/main/java/org/broad/igv/sam/cram/IGVReferenceSource.java b/src/main/java/org/broad/igv/sam/cram/IGVReferenceSource.java index cdaa8bfda0..3209d46d91 100644 --- a/src/main/java/org/broad/igv/sam/cram/IGVReferenceSource.java +++ b/src/main/java/org/broad/igv/sam/cram/IGVReferenceSource.java @@ -39,6 +39,7 @@ import org.broad.igv.ui.IGV; import org.broad.igv.util.ObjectCache; +import java.util.Arrays; import java.util.HashMap; /** @@ -52,7 +53,7 @@ public class IGVReferenceSource implements CRAMReferenceSource { @Override public byte[] getReferenceBases(SAMSequenceRecord record, boolean tryNameVariants) { - return getReferenceBasesByRegion(record, 0, record.getSequenceLength()); + return getReferenceBasesByRegion(record, 0, record.getSequenceLength()); } @Override @@ -62,9 +63,11 @@ public byte[] getReferenceBasesByRegion(final SAMSequenceRecord sequenceRecord, String chrName = currentGenome.getCanonicalChrName(name); byte[] bases = currentGenome.getSequence(chrName, zeroBasedStart, zeroBasedStart + requestedRegionLength, true); - // CRAM spec requires upper case - for (int i = 0; i < bases.length; i++) { - if (bases[i] >= 97) bases[i] -= 32; + if (bases != null) { + // CRAM spec requires upper case + for (int i = 0; i < bases.length; i++) { + if (bases[i] >= 97) bases[i] -= 32; + } } return bases; } diff --git a/src/main/java/org/broad/igv/ui/IGVMenuBar.java b/src/main/java/org/broad/igv/ui/IGVMenuBar.java index 93f1e808b6..ea8e962778 100644 --- a/src/main/java/org/broad/igv/ui/IGVMenuBar.java +++ b/src/main/java/org/broad/igv/ui/IGVMenuBar.java @@ -433,7 +433,7 @@ private JMenu createGenomesMenu() { JMenu menu = new JMenu("Genomes"); - loadGenomeFromServerMenuItem = new JMenuItem("Download Hosted Genome..."); + loadGenomeFromServerMenuItem = new JMenuItem("Select Hosted Genome..."); loadGenomeFromServerMenuItem.addActionListener(e -> GenomeSelectionDialog.selectGenomesFromServer()); loadGenomeFromServerMenuItem.setToolTipText(LOAD_GENOME_SERVER_TOOLTIP); menu.add(loadGenomeFromServerMenuItem); From c644d51a0393962d519f8f4b7b439614afe3d94b Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Mon, 30 Sep 2024 12:06:33 -0700 Subject: [PATCH 062/130] fix for chrom alias defaults --- .../broad/igv/feature/genome/ChromAlias.java | 20 +++++++++++-------- .../feature/genome/ChromAliasDefaults.java | 1 + 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/broad/igv/feature/genome/ChromAlias.java b/src/main/java/org/broad/igv/feature/genome/ChromAlias.java index 2dac9e06de..dc1de69ab7 100644 --- a/src/main/java/org/broad/igv/feature/genome/ChromAlias.java +++ b/src/main/java/org/broad/igv/feature/genome/ChromAlias.java @@ -5,34 +5,38 @@ import java.util.Map; public class ChromAlias { - + private String chr; + + /** + * Map of name set -> alias + */ private Map aliases; public ChromAlias(String chr) { this.chr = chr; this.aliases = new HashMap<>(); } - + public String getChr() { return chr; } - + public void put(String nameSet, String alias) { aliases.put(nameSet, alias); } + public String get(String nameSet) { return aliases.get(nameSet); } - + public boolean containsKey(String nameSet) { return aliases.containsKey(nameSet); } - + public Collection values() { return aliases.values(); } - - - + + } diff --git a/src/main/java/org/broad/igv/feature/genome/ChromAliasDefaults.java b/src/main/java/org/broad/igv/feature/genome/ChromAliasDefaults.java index 9649675088..f930e43539 100644 --- a/src/main/java/org/broad/igv/feature/genome/ChromAliasDefaults.java +++ b/src/main/java/org/broad/igv/feature/genome/ChromAliasDefaults.java @@ -23,6 +23,7 @@ private void init(String id, List chromosomeNames) { boolean skipRest = false; ChromAlias record = new ChromAlias(name); + record.put("__CANONICAL__", name); aliasRecords.add(record); // From c4f3ce779e6e718e4c21f0d0a1dff65e1ce57fd3 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Tue, 1 Oct 2024 22:21:16 -0700 Subject: [PATCH 063/130] Bug fix -- bigbed parsing fails if there are comments in autoSql. --- .../java/org/broad/igv/ucsc/bb/BBUtils.java | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/broad/igv/ucsc/bb/BBUtils.java b/src/main/java/org/broad/igv/ucsc/bb/BBUtils.java index 05658422ef..f3a2ff218c 100644 --- a/src/main/java/org/broad/igv/ucsc/bb/BBUtils.java +++ b/src/main/java/org/broad/igv/ucsc/bb/BBUtils.java @@ -14,20 +14,24 @@ public static ASTable parseAutosql(String str) { String table = ""; String[] lines = str.trim().split("\\R"); for (String line : lines) { + if (line.startsWith("#")) { + continue; + } line = line.trim(); - if (line.startsWith("table")) { - table = line.split("\\s+")[1].trim(); - } else if (line.startsWith("(")) { - startDecoding = true; - } else if (line.startsWith(")")) { - break; - } else if (startDecoding) { - if (line.length() > 0) { - // " string chrom; \"Reference sequence chromosome or scaffold\"\n" + + if (line.length() > 0) { + if (line.startsWith("table")) { + table = line.split("\\s+")[1].trim(); + } else if (line.startsWith("(")) { + startDecoding = true; + } else if (line.startsWith(")")) { + break; + } else if (startDecoding) { int idx = line.indexOf(";"); - String[] tokens = Globals.whitespacePattern.split(line.substring(0, idx)); - String description = line.substring(idx + 1).replace("\"", "").trim(); - fields.add(new ASField(tokens[0], tokens[1], description)); + if (idx > 0) { + String[] tokens = Globals.whitespacePattern.split(line.substring(0, idx)); + String description = line.substring(idx + 1).replace("\"", "").trim(); + fields.add(new ASField(tokens[0], tokens[1], description)); + } } } } From ea97115240cfed1c4810298ce3cc20f8d02e9e51 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Wed, 2 Oct 2024 22:40:37 -0700 Subject: [PATCH 064/130] update mac launcher --- scripts/mac.app/Contents/MacOS/IGV | Bin 102736 -> 28944 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/scripts/mac.app/Contents/MacOS/IGV b/scripts/mac.app/Contents/MacOS/IGV index daf1d8ecdf87e878399d5eb539cd1473a6fca348..121f61cb797beb5ddbd26fc7b335c1c36f6246b3 100755 GIT binary patch delta 2688 zcmb7GdrZ?;6ut!t6CV_OF>z3kr;1XbohWF_Lw|xyr-R^(DTP)*r8tVOEn1v5+BHqU zyNJmi5%-90Gr=$xgfW?q=pV-DmMv}?=VNMhTUIl(FqYVT=l)v2X8(BMoO{3X`0ly4 zxA*qS(eSoAii7$qG3s%QDHvltVNhu5ar};c@zftD%I0ceWK3YLH7ttOO_Dtp%0koe zJ%(Qt2e_#)PNCwv3g=91Q^^sM9KZ;NwQJ(I*AT{U6wVD%y~-ntk}|5mki%Fj+^i~* zu`Ki)B~921-LUHR0d5;?Ij3dRT@<3 zNzK79eycRs-s{Eup553gDdU{vQuc;pd0<-McGJpFkzMoM3b*n$`J9)#m}ilQ>hU5wsv@*;-$~SX_FFFMKFS(W1@lT#>wcIj6!78C8JOo^~y*o zqd^(P%IKm%4li|1$Cc|oJN8mZUmn0~WWC_Hg$@hCnHiAxx`*oWxbP`D#nE>O?R4bB z{IEb?yPk60S}&OT=#uc%BYc#B+@9tC!;t6mxdf5ZF@-2xO7Sfjb7G^w=8~Oz&e4~3 z{aMc)*!Z;aC5i{Uvb;j!;$M{KnV2sn8T;r=-EQ&3at}*fzS(u&KG*I(0ZW@ zg)SC)snC+prO-V8sbo*F&EII=l$otzrG`p_QCeAE!eFauu+HCT+Gb)EwgzjRx@N1@ zyxC$_mll_?G3GTpZ7r5YvPQB%=DdcwZ6eiZYOdSbXtB1~m_EhEGHvxu+qYSoZ4FIU zmYLbuWZt^jlF4s9-NASG()jx;XHKMxUXQIH`GKEbd9qB^O524?PTat_J6i>W>Y~dN z7vKYb|HR*q7YQmBQ#UTriQYwI`JS~eNYrsnMpRtAI3x}~|n+OmN>9i7FBG=9@uq_nhH ztlN0JB{l}6Mg}cEA))L@OozdsNN6|`lQkHzg)z*|CgDu2^1pYF{8;6G=oIjgzcqm2 zHvDurqd9Sj0ik8!=`vs;E^T1c~F5OvT;x-EP%m~X zgBsyig8Y;pD4G^Q`6|A%CLu9kr7VgXSqjD!)yhxRBq&<>Pc`%D9gM8iMZYFbLK&iM z;BV9>*!N%z(}?vz5OxP)9)#Zu!XE`;dM?KDcR!1hSpTyIb~XrK<&SG8+v8xR+ngTj z1c(}v2uXs>g3wN;KvE&}j;BNDp{Eyj24rl0Jn(aJaGQBy{Bs@D?s&`FifYWnhHp+7 z#E+6sFvOcHS}rt>gW@0bu!WF40x^2|a|rzaIpjajLc%D4#uoHdava_KX)1LHo6xRF z)zNPk(G<&%-Rg%mrOa7boW$GxZB--V*FQs44vpS-l)DOon4#UKhgZwj3sfa`?`@A0S3VWaTpB4ZN`X_NSoQe zr`J}{8+vRMQDSMRS#I09(rIhm#*5R@-4Loc-PZg9di3tvzS&IrqEwe)o6p z_nhy4$9|SO_4t&ucP}42%oyW2#->857}G;EbfF1GUmzrC5Ij_dCb;j&yi*1CQ(;bj z#^7f@h#+$MJI*eP90`(o*Grv@Y$jxLOG0ylB~JwffpUf2+$UVKVB}|T!NPC&`H{8| z39(56FtXKxLp?*dz^R2Zdu!& zrR6k8F;fk5mSb(W(5;%Y@Y+?~XL9Y??ayYEd4lpkzdHkX9T;h`9?TO=y125yS%OCu z%{%>t7&VZQ1RYF7rp~xZ$J55d$7pXQIDoA zT=ZqX2RnW6O`59fVdygaFztowK6H={N^x;#MATrlVKZx7NwSzV)I-%?tF>jdfTZ4L0tu;Pw> zvP7)wv_SrS*SUbCVoCrg?a;|wf}R9~DN zUKVGhP#dSMbw;My)eVf%6HL!-NC4IZ{iJN`fBDT+|aQfO&}vJ0o?%9{hN45?BYOF9B_q790zn4UPf-r?{Eu z|It|CNMgS5fi5ACZmDwn1btGGusklZ@A0Iq-1V0}4jjbA`aM$lfQjhp;iz~zF7)yA z8^kwWwNlzq)zgT9k2}eDHC%+{>LKYB1dhIc$z3|01>y^uTtJ^WkvKde3vMG{YnYp=Xnc0hTjmFH3?6m6Yw5rTXQ`KUlaANlG=JQ{iI8wly ze17$U=G)w--U^wUe_ZG-`gG4<$A-Rp?@s|Y>Qosu7vH!&rck|g*U0eIjUz>;{_@1d zguQv^t(A8#xOIxIbLO6%%|(rCD@My-f2eC!`EADQp8L9Y_ZywDMeUD$`E;M(+^NNV zr{-~i|i_px;GONL^dy1Gttp{$;lu^8ka&mzbFwF zWo#B*Xkg286nmzil}e^&cnC=+MntyCt7M9tZj%Rs6iJq4f}cv40wWFKN-dSPdtxnB z1Dgt*hC3L3!%8hR!)LVBtC-q#bbH>VR|?#Fj3dRbEgL%X-xO2!ya~nBW{iU(VZ4~4 zafy^-%6=yb&=M4$e2&%A%7(^TxSp{>=6Lg%_JA{h4GgQ8+ZmvT)Vpz!_m8z))dQSk zP-Xb=`)!AN6ud{3IfQX~eP`+2zMS6O-5NXUG$4LHa}Q^74quzC+_i=ccuLuT^V4|8 z)5wLhPIpc1j5)#BA`XR2gpS$Dq1fXtEOy|7w4Z=iq0jj5;0f5rW$ep8hO^{49)lli zx3h0@9r!Tp$Aq`LqLc|(S%l~e$#Xd1#IIViug`Prg^h_tI6vhb&T!`D*zWg-HO!4$ zBqe+@&q4ZGjo$52v1gs(kO^3Mg5G_X+)sTN(C#~s=WuD^)9T$e_{RIKK}#I28I;^s zj$0tj4%^c$ZBK*?gG+uEAts6hJ_^Uv$~l}ax9m&`E!pa%7xh@(M7r-kYQl6GraTHV=c>jFcI+rdXDj!kUu8GJGxZb z(JmDRQZdIO8Yn=&(>Ew0%r{iOopLAx&B5+=<8tFiA3>L##_Kv}I55^?EkR;lOpN!q zH-y)_8L$0N)Temq{$6^pmmcP&PxI1ay!3c4J=sg22R&06EY-`A;iczz>3Nba%*Sgn{ZZ!x*CJf5I5awZ%X@2W#?Ewe8E`9AWn~#b6mEc zr_F$VMz-^DqGS_SQ6tlrkX4ESSqPuET>`sWjFLbUiXt^!EDFd_N@n_W#m8Ee+Cj#R z&x`6QDrS{=l0Omr87UwVt0(!X7*^In>!NtYCOpN{%^)Hh@r4rrlt5WHZ3HDyY$btG zA;<~f7cRx8@>mWEZR^i2w56#~GLc%)!Wg%0lWcszC-`w?z3mKk-^31v`|J&1_!LjD zpBHOzEqZBplsY^fdNMcx90#TXA_=Sm$AXm_{7P{%fw^L=QiIR%uEBr!%P0PfgfU%8 zpz@EB{si_tkhGpt8O?RVh2&VdMpsjfjW4%IS!i)*_@j{M<4;1O_E!7Rav`gw8lB&F zg&N(Du{-TLrAF6N4AC#I(XA;%=Z>v-%@emOE&X>#PtIT9j*s4V&ur)D>72u*rQNk- zFIb;{dDFJH&s7hqKN=dkiC?qH$Fu8|UAMOV?NCQ?d&)CEh0@p!@rEn G*Zd3kVIZ6U From dc196ddae58e2fc49774931f5ef6464e8a8ac591 Mon Sep 17 00:00:00 2001 From: Jim Robinson <933148+jrobinso@users.noreply.github.com> Date: Sun, 6 Oct 2024 20:32:48 -0700 Subject: [PATCH 065/130] Encode (#1594) --- scripts/mac.app/Contents/MacOS/main.c | 146 ++++++++ .../igv/{util => }/encode/DCCEncodeUtils.java | 2 +- .../{util => }/encode/EncodeFileRecord.java | 19 +- .../{util => }/encode/EncodeTableModel.java | 36 +- .../EncodeTrackChooser.java} | 314 ++++++++++++------ .../{util => }/encode/UCSCEncodeUtils.java | 130 ++------ .../broad/igv/maf/MultipleAlignmentTrack.java | 2 +- .../java/org/broad/igv/prefs/Constants.java | 3 + .../java/org/broad/igv/sam/CoverageTrack.java | 4 +- .../broad/igv/sam/SpliceJunctionTrack.java | 7 +- .../org/broad/igv/track/AbstractTrack.java | 37 ++- .../broad/igv/track/CombinedDataTrack.java | 4 +- .../org/broad/igv/track/SequenceTrack.java | 6 +- src/main/java/org/broad/igv/track/Track.java | 2 +- .../java/org/broad/igv/ui/IGVMenuBar.java | 98 +++--- .../igv/ui/action/BrowseEncodeAction.java | 102 ++++-- .../broad/igv/ui/action/UCSCGenArkAction.java | 9 - .../broad/igv/ui/panel/TrackNamePanel.java | 2 +- .../org/broad/igv/util/ResourceLocator.java | 13 +- .../org/broad/igv/variant/VariantTrack.java | 8 +- .../igv/{util => }/encode/encode.hg18.txt | 0 .../igv/{util => }/encode/encode.hg19.txt | 0 .../igv/{util => }/encode/encode.mm9.txt | 0 .../org/broad/igv/prefs/preferences.tab | 2 + 24 files changed, 574 insertions(+), 372 deletions(-) create mode 100644 scripts/mac.app/Contents/MacOS/main.c rename src/main/java/org/broad/igv/{util => }/encode/DCCEncodeUtils.java (98%) rename src/main/java/org/broad/igv/{util => }/encode/EncodeFileRecord.java (87%) rename src/main/java/org/broad/igv/{util => }/encode/EncodeTableModel.java (81%) rename src/main/java/org/broad/igv/{util/encode/EncodeFileBrowser.java => encode/EncodeTrackChooser.java} (55%) rename src/main/java/org/broad/igv/{util => }/encode/UCSCEncodeUtils.java (57%) rename src/main/resources/org/broad/igv/{util => }/encode/encode.hg18.txt (100%) rename src/main/resources/org/broad/igv/{util => }/encode/encode.hg19.txt (100%) rename src/main/resources/org/broad/igv/{util => }/encode/encode.mm9.txt (100%) diff --git a/scripts/mac.app/Contents/MacOS/main.c b/scripts/mac.app/Contents/MacOS/main.c new file mode 100644 index 0000000000..5cf4bc0035 --- /dev/null +++ b/scripts/mac.app/Contents/MacOS/main.c @@ -0,0 +1,146 @@ +// +// main.c +// igv-launcher +// +// Created by James Robinson on 9/14/24. +// +// Launch IGV on MacOS (https://github.com/igvteam/igv). +// +// Code adapted from https://incenp.org/notes/2023/universal-java-app-on-macos.html +// + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +/* Remove the last n components of a pathname. The pathname + * is modified in place. Returns 0 if the requested number + * of components have been removed, -1 otherwise. */ +static int remove_last_component(char *buffer , unsigned n) +{ + char *last_slash = NULL; + while ( n-- > 0 ) { + if ( (last_slash = strrchr(buffer, '/')) ) + *last_slash = '\0'; + } + return last_slash ? 0 : -1; +} + +/* + * Test if a given path exists and if it is a directory. + * It returns 1 if given path is directory and exists + * otherwise returns 0. + */ +static int isDirectoryExists(const char *path) +{ + struct stat stats; + stat(path, &stats); + + // Check for file existence + if (S_ISDIR(stats.st_mode)) + return 1; + + return 0; +} + +static int simpleLauncher(int argc, char **argv) +{ + char app_path[PATH_MAX]; + uint32_t path_size = PATH_MAX; + int ret = 0; + + (void) argc; + (void) argv; + + /* Get the path to the "Contents" directory. */ + if ( _NSGetExecutablePath(app_path, &path_size) == -1 ) + err(EXIT_FAILURE, "Cannot get application directory"); + if ( remove_last_component(app_path, 2) == -1 ) + errx(EXIT_FAILURE, "Cannot get application directory"); + + /* Move to that directory. */ + if ( chdir(app_path) == -1 ) + err(EXIT_FAILURE, "Cannot change current directory"); + + char* cmd; + + /* See if there is a bundled JDK */ + if(isDirectoryExists("jdk-21")) { + char jdkPath[2048] = {'\0'}; + snprintf(jdkPath, sizeof(jdkPath), "%s/jdk-21", app_path); + + setenv("JAVA_HOME", jdkPath, true); + + char javaPath[2048] = {'\0'}; + snprintf(javaPath, sizeof(javaPath), "%s/bin/java", jdkPath); + cmd = javaPath; + + printf("Using bundled JDK"); + + //PATH=$JAVA_HOME/bin:$PATH + } else { + + cmd = "java"; + printf("Using System JDK\n"); + } + + /* Get user defined extra arguments, if any */ + char extraArgumentsPath[2048] = {'\0'}; + const char *homeDir = getenv("HOME"); + snprintf(extraArgumentsPath, sizeof(extraArgumentsPath), "%s/.igv/java_arguments", homeDir); + + if(access(extraArgumentsPath, F_OK) == 0) { + char extraArguments[2048] = {'\0'}; + snprintf(extraArguments, sizeof(extraArguments), "@%s", extraArgumentsPath); + + /* Run IGV with user extra arguments */ + + + char* args[] = { + "java", + "-showversion", + "--module-path=Java/lib", + "-Xmx8g", + "@Java/igv.args", + "-Xdock:name=IGV", + "-Xdock:icon=Resources/IGV_64.png", + "-Dapple.laf.useScreenMenuBar=true", + extraArguments, + "--module=org.igv/org.broad.igv.ui.Main", + NULL}; + + ret = execvp(cmd, args); + return ret; + + } else { + + /* Run IGV without user extra arguments */ + + char* args[] = { + "java", + "-showversion", + "--module-path=Java/lib", + "-Xmx8g", + "@Java/igv.args", + "-Xdock:name=IGV", + "-Xdock:icon=Resources/IGV_64.png", + "--module=org.igv/org.broad.igv.ui.Main", + NULL}; + + ret = execvp(cmd, args); + return ret; + } +} + +int main(int argc, char **argv) +{ + return simpleLauncher(argc, argv); +} diff --git a/src/main/java/org/broad/igv/util/encode/DCCEncodeUtils.java b/src/main/java/org/broad/igv/encode/DCCEncodeUtils.java similarity index 98% rename from src/main/java/org/broad/igv/util/encode/DCCEncodeUtils.java rename to src/main/java/org/broad/igv/encode/DCCEncodeUtils.java index fe517c1334..5ea688d9e2 100644 --- a/src/main/java/org/broad/igv/util/encode/DCCEncodeUtils.java +++ b/src/main/java/org/broad/igv/encode/DCCEncodeUtils.java @@ -23,7 +23,7 @@ * THE SOFTWARE. */ -package org.broad.igv.util.encode; +package org.broad.igv.encode; /** * Created by jrobinso on 6/4/15. diff --git a/src/main/java/org/broad/igv/util/encode/EncodeFileRecord.java b/src/main/java/org/broad/igv/encode/EncodeFileRecord.java similarity index 87% rename from src/main/java/org/broad/igv/util/encode/EncodeFileRecord.java rename to src/main/java/org/broad/igv/encode/EncodeFileRecord.java index 56cd8beb17..ca95a8d1a2 100644 --- a/src/main/java/org/broad/igv/util/encode/EncodeFileRecord.java +++ b/src/main/java/org/broad/igv/encode/EncodeFileRecord.java @@ -23,7 +23,7 @@ * THE SOFTWARE. */ -package org.broad.igv.util.encode; +package org.broad.igv.encode; import java.io.File; import java.util.Collection; @@ -70,11 +70,8 @@ public Collection getAttributeNames() { return attributes.keySet(); } - public boolean containsText(String filter) { - for (String value : attributes.values()) { - if (value.contains(filter)) return true; - } - return false; + public Map getAttributes() { + return attributes; } boolean isSelected() { @@ -108,14 +105,4 @@ public String getTrackName() { } - /** - * Test if record has a eough of meta-data to be interpretable - * - * @return - */ - public boolean hasMetaData() { - - return (attributes.containsKey("cell")) || (attributes.containsKey("antibody")); - - } } diff --git a/src/main/java/org/broad/igv/util/encode/EncodeTableModel.java b/src/main/java/org/broad/igv/encode/EncodeTableModel.java similarity index 81% rename from src/main/java/org/broad/igv/util/encode/EncodeTableModel.java rename to src/main/java/org/broad/igv/encode/EncodeTableModel.java index 9b67cb69e5..4c8fc15a92 100644 --- a/src/main/java/org/broad/igv/util/encode/EncodeTableModel.java +++ b/src/main/java/org/broad/igv/encode/EncodeTableModel.java @@ -23,41 +23,16 @@ * THE SOFTWARE. */ -package org.broad.igv.util.encode; +package org.broad.igv.encode; -import javax.swing.*; import javax.swing.table.AbstractTableModel; import javax.swing.table.TableModel; import javax.swing.table.TableRowSorter; import javax.swing.table.TableStringConverter; import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashSet; import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** - * //wgEncodeBroadHistoneGm12878H3k4me1StdSig.bigWig - * // size=346M; - * // dateSubmitted=2009-01-05; - * // dataType=ChipSeq; - * // cell=GM12878; - * // antibody=H3K4me1; - * // control=std; - * // expId=33; - * // setType=exp; - * // controlId=GM12878/Input/std; - * // subId=2804; - * // dataVersion=ENCODE Jan 2011 Freeze; - * // dateResubmitted=2010-11-05; - * // grant=Bernstein; - * // lab=Broad; - * // view=Signal; - * // type=bigWig; - * // dccAccession=wgEncodeEH000033; - * // origAssembly=hg18 - * * @author jrobinso * Date: 10/31/13 * Time: 10:09 PM @@ -68,7 +43,7 @@ public class EncodeTableModel extends AbstractTableModel { private List records; private final TableRowSorter sorter; - public EncodeTableModel(String [] headings, List records) { + public EncodeTableModel(List headings, List records) { this.records = records; @@ -80,11 +55,10 @@ public EncodeTableModel(String [] headings, List records) { tmp.add(heading); } } - //tmp.add("path"); - columnHeadings = tmp.toArray(new String[tmp.size()]); + columnHeadings = tmp.toArray(new String[tmp.size()]); - sorter = new TableRowSorter(this); + sorter = new TableRowSorter<>(this); sorter.setStringConverter(new TableStringConverter() { @Override @@ -149,6 +123,8 @@ public void setValueAt(Object value, int row, int col) { fireTableCellUpdated(row, col); } + + public List getRecords() { return records; } diff --git a/src/main/java/org/broad/igv/util/encode/EncodeFileBrowser.java b/src/main/java/org/broad/igv/encode/EncodeTrackChooser.java similarity index 55% rename from src/main/java/org/broad/igv/util/encode/EncodeFileBrowser.java rename to src/main/java/org/broad/igv/encode/EncodeTrackChooser.java index a027851fc7..294ad85456 100644 --- a/src/main/java/org/broad/igv/util/encode/EncodeFileBrowser.java +++ b/src/main/java/org/broad/igv/encode/EncodeTrackChooser.java @@ -27,7 +27,7 @@ * Created by JFormDesigner on Thu Oct 31 22:31:02 EDT 2013 */ -package org.broad.igv.util.encode; +package org.broad.igv.encode; import java.awt.*; import java.awt.event.*; @@ -40,6 +40,7 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import javax.swing.*; import javax.swing.border.*; import javax.swing.event.DocumentEvent; @@ -49,116 +50,213 @@ import com.jidesoft.swing.JideBoxLayout; import org.broad.igv.logging.*; import org.broad.igv.Globals; +import org.broad.igv.prefs.Constants; +import org.broad.igv.prefs.PreferencesManager; import org.broad.igv.ui.IGV; +import org.broad.igv.ui.action.BrowseEncodeAction; +import org.broad.igv.util.FileUtils; import org.broad.igv.util.Pair; +import org.broad.igv.util.ParsingUtils; import org.broad.igv.util.ResourceLocator; /** * @author Jim Robinson */ -public class EncodeFileBrowser extends org.broad.igv.ui.IGVDialog { +public class EncodeTrackChooser extends org.broad.igv.ui.IGVDialog { - private static Logger log = LogManager.getLogger(EncodeFileBrowser.class); + private static Logger log = LogManager.getLogger(EncodeTrackChooser.class); - private static Map instanceMap = Collections.synchronizedMap(new HashMap()); + private static Map instanceMap = Collections.synchronizedMap(new HashMap<>()); private static NumberFormatter numberFormatter = new NumberFormatter(); - private JButton cancelButton; - private JPanel dialogPane; - private JPanel contentPanel; - private JScrollPane scrollPane1; - private JTable table; - private JPanel filterPanel; - private JLabel filterLabel; - private JTextField filterTextField; - private JLabel rowCountLabel; - private JPanel buttonBar; - private JButton okButton; + private static String ENCODE_HOST = "https://www.encodeproject.org"; + private static Set filteredColumns = new HashSet(Arrays.asList("ID", "Assembly", "HREF", "path")); + private static List filteredExtensions = Arrays.asList("tsv", "tsv.gz"); + + private static Map speciesNames = Map.of( + "ce10", "Caenorhabditis elegans", + "ce11", "Caenorhabditis elegans", + "dm3", "Drosophila melanogaster", + "dm6", "Drosophila melanogaster", + "GRCh38", "Homo sapiens", + "hg19", "Homo sapiens", + "mm10", "Mus musculus", + "mm9", "Mus musculus" + ); + + static HashSet ucscSupportedGenomes = new HashSet<>(Arrays.asList("hg19", "mm9")); + static HashSet supportedGenomes = new HashSet<>( + Arrays.asList("ce10", "ce11", "dm3", "dm6", "GRCh38", "hg19", "mm10", "mm9")); + + + + JTable table; + JTextField filterTextField; + JLabel rowCountLabel; EncodeTableModel model; private boolean canceled; - public synchronized static EncodeFileBrowser getInstance(String genomeId) throws IOException { + /** + * Return a new or cached instance of a track chooser for the given genome and type. + * + * @param genomeId + * @param type + * @return + * @throws IOException + */ + public synchronized static EncodeTrackChooser getInstance(String genomeId, BrowseEncodeAction.Type type) throws IOException { - String encodeGenomeId = getEncodeGenomeId(genomeId); - EncodeFileBrowser instance = instanceMap.get(encodeGenomeId); + String encodeGenomeId = getEncodeGenomeID(genomeId); + String key = encodeGenomeId + type.toString(); + EncodeTrackChooser instance = instanceMap.get(key); if (instance == null) { - Pair> records = getEncodeFileRecords(encodeGenomeId); + Pair, List> records = getEncodeFileRecords(encodeGenomeId, type); if (records == null) { return null; } Frame parent = IGV.hasInstance() ? IGV.getInstance().getMainFrame() : null; - instance = new EncodeFileBrowser(parent, new EncodeTableModel(records.getFirst(), records.getSecond())); - instanceMap.put(encodeGenomeId, instance); + final List headings = records.getFirst(); + final List rows = records.getSecond(); + final String title = getDialogTitle(genomeId, type); + instance = new EncodeTrackChooser(parent, new EncodeTableModel(headings, rows), title); + instanceMap.put(key, instance); } return instance; } - static HashSet supportedGenomes = new HashSet(Arrays.asList("hg19", "mm9")); - public static boolean genomeSupported(String genomeId) { - return genomeId != null && supportedGenomes.contains(getEncodeGenomeId(genomeId)); + private static String getDialogTitle(String genomeId, BrowseEncodeAction.Type type) { + + if (type == BrowseEncodeAction.Type.UCSC) { + return "ENCODE data hosted at UCSC (2012)"; + } else { + switch (type) { + case SIGNALS_CHIP: + return "ENCODE CHiP Seq - Signals"; + case SIGNALS_OTHER: + return "ENCODE Non CHiP Data - Signals"; + default: + return "ENCODE"; + } + } } - private static String getEncodeGenomeId(String genomeId) { - if (genomeId.equals("b37") || genomeId.equals("1kg_v37")) return "hg19"; - else return genomeId; + public static boolean genomeSupportedUCSC(String genomeId) { + return genomeId != null && ucscSupportedGenomes.contains(getEncodeGenomeID(genomeId)); } - private static Pair> getEncodeFileRecords(String genomeId) throws IOException { + public static boolean genomeSupported(String genomeId) { + return genomeId != null && supportedGenomes.contains(getEncodeGenomeID(genomeId)); + } - InputStream is = null; - try { + private static String getEncodeGenomeID(String genomeId) { + switch (genomeId) { + case "hg38": + case "hg38_1kg": + return "GRCh38"; + case "b37": + case "1kg_v37": + return "hg19"; + default: + return genomeId; + } + + } + + private static Pair, List> getEncodeFileRecords(String genomeId, BrowseEncodeAction.Type type) throws IOException { - is = EncodeFileBrowser.class.getResourceAsStream("encode." + genomeId + ".txt"); + try (InputStream is = getStreamFor(genomeId, type)) { if (is == null) { return null; } - BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + return parseRecords(is, type, genomeId); + } + } - String[] headers = Globals.tabPattern.split(reader.readLine()); + private static InputStream getStreamFor(String genomeId, BrowseEncodeAction.Type type) throws IOException { + if (type == BrowseEncodeAction.Type.UCSC) { + return EncodeTrackChooser.class.getResourceAsStream("encode." + genomeId + ".txt"); + } else { + String root = PreferencesManager.getPreferences().get(Constants.ENCODE_FILELIST_URL) + genomeId + "."; + String url = null; + switch (type) { + case SIGNALS_CHIP: + url = root + "signals.chip.txt"; + break; + case SIGNALS_OTHER: + url = root + "signals.other.txt"; + break; + case OTHER: + url = root + "other.txt"; + break; + } + if (url == null) { + throw new RuntimeException("Unknown encode data collection type: " + type); + } + return ParsingUtils.openInputStream(url); + } + } - List records = new ArrayList(20000); - String nextLine; - while ((nextLine = reader.readLine()) != null) { - if (!nextLine.startsWith("#")) { + private static Pair parseRecords(InputStream is, BrowseEncodeAction.Type type, String genomeId) throws IOException { - String[] tokens = Globals.tabPattern.split(nextLine, -1); - String path = tokens[0]; + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); - Map attributes = new HashMap(); - for (int i = 0; i < headers.length; i++) { - String value = i < tokens.length ? tokens[i] : ""; - if (value.length() > 0) { - attributes.put(headers[i], value); - } - } - final EncodeFileRecord record = new EncodeFileRecord(path, attributes); - if(record.hasMetaData()) records.add(record); + String[] headers = Globals.tabPattern.split(reader.readLine()); + + int pathColumn = type == BrowseEncodeAction.Type.UCSC ? 0 : Arrays.asList(headers).indexOf("HREF"); + List records = new ArrayList<>(20000); + String nextLine; + while ((nextLine = reader.readLine()) != null) { + if (!nextLine.startsWith("#")) { + + String[] tokens = Globals.tabPattern.split(nextLine, -1); + String path = type == BrowseEncodeAction.Type.UCSC ? tokens[pathColumn] : ENCODE_HOST + tokens[pathColumn]; + + if(filteredExtensions.stream().anyMatch(e -> path.endsWith(e))) { + continue; + } + + Map attributes = new LinkedHashMap<>(); + for (int i = 0; i < headers.length; i++) { + String value = i < tokens.length ? tokens[i] : ""; + if (value.length() > 0) { + attributes.put(headers[i], shortenField(value, genomeId)); + } } + final EncodeFileRecord record = new EncodeFileRecord(path, attributes); + records.add(record); } - return new Pair(headers, records); - } finally { - if (is != null) is.close(); } + + List filteredHeaders = Arrays.stream(headers).filter(h -> !filteredColumns.contains(h)).collect(Collectors.toList()); + + return new Pair(filteredHeaders, records); + } + + private static String shortenField(String value, String genomeId) { + String species = speciesNames.get(genomeId); + return species == null ? + value : + value.replace("(" + species + ")", "").replace(species, "").trim(); } - private EncodeFileBrowser(Frame owner, EncodeTableModel model) { + private EncodeTrackChooser(Frame owner, EncodeTableModel model, String title) { super(owner); + setTitle(title); this.model = model; setModal(true); - initComponents(); + initComponents(owner); init(model); } private void init(final EncodeTableModel model) { setModal(true); - setTitle("Encode Production Data"); table.setAutoCreateRowSorter(true); table.setModel(model); @@ -263,14 +361,14 @@ public List getSelectedRecords() throws IOException { private class RegexFilter extends RowFilter { - List> matchers; + List> matchers; RegexFilter(String text) { if (text == null) { throw new IllegalArgumentException("Pattern must be non-null"); } - matchers = new ArrayList>(); + matchers = new ArrayList>(); String[] tokens = Globals.whitespacePattern.split(text); for (String t : tokens) { // If token contains an = sign apply to specified column only @@ -336,95 +434,83 @@ public boolean include(Entry value) { } - private void initComponents() { + private void initComponents(Frame owner) { - dialogPane = new JPanel(); - contentPanel = new JPanel(); - scrollPane1 = new JScrollPane(); - table = new JTable(); - filterPanel = new JPanel(); - filterLabel = new JLabel(); - filterTextField = new JTextField(); - rowCountLabel = new JLabel(); - buttonBar = new JPanel(); - okButton = new JButton(); - cancelButton = new JButton(); - - getRootPane().setDefaultButton(okButton); - - final String filterToolTip = "Enter multiple filter strings separated by commas. e.g. GM12878, ChipSeq"; - filterLabel.setToolTipText(filterToolTip); - filterTextField.setToolTipText(filterToolTip); + // All this to have tool tip text! + table = new JTable() { + @Override + public String getToolTipText(MouseEvent e) { + java.awt.Point p = e.getPoint(); + int rowIndex = rowAtPoint(p); + int colIndex = columnAtPoint(p); + int realColumnIndex = convertColumnIndexToModel(colIndex); + if (realColumnIndex > 0) { + return getModel().getValueAt(rowIndex, realColumnIndex).toString(); + } + return null; + } + }; - //======== this ======== + //======== outer content pane ======== Container contentPane = getContentPane(); contentPane.setLayout(new BorderLayout()); //======== dialogPane ======== - + JPanel dialogPane = new JPanel(); dialogPane.setBorder(new EmptyBorder(12, 12, 12, 12)); dialogPane.setLayout(new BorderLayout()); - //======== contentPanel ======== - + //======== main content panel ======== + JPanel contentPanel = new JPanel(); contentPanel.setLayout(new BorderLayout(0, 10)); //======== scrollPane1 ======== - + JScrollPane scrollPane1 = new JScrollPane(); scrollPane1.setViewportView(table); - contentPanel.add(scrollPane1, BorderLayout.CENTER); - //======== panel1 ======== - + //---- Filter panel ---- + JPanel filterPanel = new JPanel(); filterPanel.setLayout(new JideBoxLayout(filterPanel, JideBoxLayout.X_AXIS, 5)); - - //---- label1 ---- - filterLabel.setText("Filter:"); + JLabel filterLabel = new JLabel("Filter:"); + final String filterToolTip = "Enter multiple filter strings separated by commas. e.g. GM12878, ChipSeq"; + filterLabel.setToolTipText(filterToolTip); filterPanel.add(filterLabel, JideBoxLayout.FIX); - - //---- filterTextField ---- + filterTextField = new JTextField(); + filterTextField.setToolTipText(filterToolTip); filterPanel.add(filterTextField, JideBoxLayout.VARY); + rowCountLabel = new JLabel(); rowCountLabel.setHorizontalAlignment(JLabel.RIGHT); - JPanel sillyPanel = new JPanel(); - sillyPanel.setLayout(new JideBoxLayout(sillyPanel, JideBoxLayout.X_AXIS, 0)); - sillyPanel.setPreferredSize(new Dimension(100, 28)); - sillyPanel.add(rowCountLabel, JideBoxLayout.VARY); - - filterPanel.add(sillyPanel, JideBoxLayout.FIX); + JPanel rowCountPanel = new JPanel(); + rowCountPanel.setLayout(new JideBoxLayout(rowCountPanel, JideBoxLayout.X_AXIS, 0)); + rowCountPanel.setPreferredSize(new Dimension(100, 28)); + rowCountPanel.add(rowCountLabel, JideBoxLayout.VARY); + filterPanel.add(rowCountPanel, JideBoxLayout.FIX); contentPanel.add(filterPanel, BorderLayout.NORTH); dialogPane.add(contentPanel, BorderLayout.CENTER); //======== buttonBar ======== - + JPanel buttonBar = new JPanel(); buttonBar.setBorder(new EmptyBorder(12, 0, 0, 0)); buttonBar.setLayout(new GridBagLayout()); ((GridBagLayout) buttonBar.getLayout()).columnWidths = new int[]{0, 85, 80}; ((GridBagLayout) buttonBar.getLayout()).columnWeights = new double[]{1.0, 0.0, 0.0}; //---- okButton ---- - okButton.setText("Load"); - okButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - loadButtonActionPerformed(e); - } - }); + JButton okButton = new JButton("Load"); + getRootPane().setDefaultButton(okButton); + okButton.addActionListener(e -> loadButtonActionPerformed(e)); buttonBar.add(okButton, new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 5), 0, 0)); //---- cancelButton ---- - cancelButton.setText("Cancel"); - cancelButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - cancelButtonActionPerformed(e); - } - }); + JButton cancelButton = new JButton("Cancel"); + cancelButton.addActionListener(e -> cancelButtonActionPerformed(e)); + buttonBar.add(cancelButton, new GridBagConstraints(2, 0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); @@ -432,13 +518,21 @@ public void actionPerformed(ActionEvent e) { dialogPane.add(buttonBar, BorderLayout.SOUTH); contentPane.add(dialogPane, BorderLayout.CENTER); - setSize(700, 620); + + Rectangle ownerBounds = owner.getBounds(); + setSize(ownerBounds.width, 620); setLocationRelativeTo(getOwner()); } + /** + * Main function for testing only + * + * @param args + * @throws IOException + */ public static void main(String[] args) throws IOException { - getInstance("hg19").setVisible(true); + getInstance("hg19", BrowseEncodeAction.Type.UCSC).setVisible(true); } } diff --git a/src/main/java/org/broad/igv/util/encode/UCSCEncodeUtils.java b/src/main/java/org/broad/igv/encode/UCSCEncodeUtils.java similarity index 57% rename from src/main/java/org/broad/igv/util/encode/UCSCEncodeUtils.java rename to src/main/java/org/broad/igv/encode/UCSCEncodeUtils.java index e99222b273..a731770d21 100644 --- a/src/main/java/org/broad/igv/util/encode/UCSCEncodeUtils.java +++ b/src/main/java/org/broad/igv/encode/UCSCEncodeUtils.java @@ -23,15 +23,13 @@ * THE SOFTWARE. */ -package org.broad.igv.util.encode; +package org.broad.igv.encode; import org.broad.igv.Globals; import org.broad.igv.util.HttpUtils; import org.broad.igv.util.ParsingUtils; -import java.awt.*; import java.io.*; -import java.net.URL; import java.util.*; import java.util.List; @@ -42,88 +40,30 @@ */ public class UCSCEncodeUtils { - static HashSet labs = new HashSet(); - static HashSet dataTypes = new HashSet(); - static HashSet cells = new HashSet(); - static HashSet antibodies = new HashSet(); - static HashSet fileTypes = new HashSet(); - static HashSet allHeaders = new LinkedHashSet(); + private static Set labs = new HashSet<>(); + private static Set dataTypes = new HashSet<>(); + private static Set cells = new HashSet<>(); + private static Set antibodies = new HashSet<>(); + private static Set fileTypes = new HashSet<>(); + private static Set allHeaders = new LinkedHashSet<>(); private static List rnaChipQualifiers = Arrays.asList("CellTotal", "Longnonpolya", "Longpolya", "NucleolusTotal", "ChromatinTotal", "ChromatinTotal", "NucleoplasmTotal"); public static void main(String[] args) throws IOException { - -// List records = new ArrayList(); -// parseFilesDotTxt(args[0], records); -// PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(args[1]))); -// -// pw.print("path"); -// for (String h : EncodeTableModel.columnHeadings) { -// pw.print("\t"); -// pw.print(h); -// } -// pw.println(); -// -// for (EncodeFileRecord rec : records) { -// pw.print(rec.getPath()); -// for (String h : EncodeTableModel.columnHeadings) { -// pw.print("\t"); -// String value = rec.getAttributeValue(h); -// pw.print(value == null ? "" : value); -// } -// pw.println(); -// } -// pw.close(); - updateEncodeTableFile(args[0], args[1]); } - private static List parseTableFile(String url) throws IOException { - - List records = new ArrayList(20000); - - BufferedReader reader = null; - - try { - reader = ParsingUtils.openBufferedReader(url); - - String[] headers = Globals.tabPattern.split(reader.readLine()); - - String nextLine; - while ((nextLine = reader.readLine()) != null) { - if (!nextLine.startsWith("#")) { - String[] tokens = Globals.tabPattern.split(nextLine, -1); - String path = tokens[0]; - Map attributes = new HashMap(); - for (int i = 0; i < headers.length; i++) { - String value = tokens[i]; - if (value.length() > 0) { - attributes.put(headers[i], value); - } - } - records.add(new EncodeFileRecord(path, attributes)); - } - - } - return records; - } finally { - reader.close(); - } - } static String[] columnHeadings = {"cell", "dataType", "antibody", "view", "replicate", "type", "lab"}; private static void updateEncodeTableFile(String inputFile, String outputFile) throws IOException { - List records = new ArrayList(); - - BufferedReader reader = null; - try { - reader = ParsingUtils.openBufferedReader(inputFile); + List records = new ArrayList<>(); + try (BufferedReader reader = ParsingUtils.openBufferedReader(inputFile)) { String rootPath = reader.readLine(); String hub = null; @@ -131,56 +71,48 @@ private static void updateEncodeTableFile(String inputFile, String outputFile) t while ((nextLine = reader.readLine()) != null) { if (nextLine.startsWith("#")) { - if (nextLine.startsWith("#hub=")) { - hub = nextLine.substring(5); - } + hub = nextLine.startsWith("#hub=") ? nextLine.substring(5) : hub; } else { String dir = nextLine.equals(".") ? rootPath : rootPath + nextLine; String filesDotTxt = dir + "/files.txt"; - try { - if (HttpUtils.getInstance().resourceAvailable(filesDotTxt)) { + if (HttpUtils.getInstance().resourceAvailable(filesDotTxt)) { + try { parseFilesDotTxt(filesDotTxt, records); + } catch (IOException e) { + // e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. } - } catch (IOException e) { - // e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. } } } - for (String dt : fileTypes) System.out.println(dt); - + fileTypes.forEach(System.out::println); outputRecords(outputFile, records, hub); - } finally { - reader.close(); } } private static void outputRecords(String outputFile, List records, String hub) throws IOException { - PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(outputFile))); - pw.print("path"); - for (String h : columnHeadings) { - pw.print("\t"); - pw.print(h); - } - if (hub != null) { - pw.print("\thub"); - } - pw.println(); - - for (EncodeFileRecord rec : records) { - pw.print(rec.getPath()); + try (PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(outputFile)))) { + StringBuilder sb = new StringBuilder("path"); for (String h : columnHeadings) { - pw.print("\t"); - String value = rec.getAttributeValue(h); - pw.print(value == null ? "" : value); + sb.append("\t").append(h); } if (hub != null) { - pw.print("\t" + hub); + sb.append("\thub"); + } + pw.println(sb.toString()); + + for (EncodeFileRecord rec : records) { + sb = new StringBuilder(rec.getPath()); + for (String h : columnHeadings) { + sb.append("\t").append(Optional.ofNullable(rec.getAttributeValue(h)).orElse("")); + } + if (hub != null) { + sb.append("\t").append(hub); + } + pw.println(sb.toString()); } - pw.println(); } - pw.close(); } static HashSet knownFileTypes = new HashSet(Arrays.asList( diff --git a/src/main/java/org/broad/igv/maf/MultipleAlignmentTrack.java b/src/main/java/org/broad/igv/maf/MultipleAlignmentTrack.java index e9fe48166d..b648d32ef3 100644 --- a/src/main/java/org/broad/igv/maf/MultipleAlignmentTrack.java +++ b/src/main/java/org/broad/igv/maf/MultipleAlignmentTrack.java @@ -153,7 +153,7 @@ public void renderName(Graphics2D g2D, Rectangle trackRectangle, Rectangle visib Rectangle rect = new Rectangle(trackRectangle); g2D.clearRect(rect.x, rect.y, rect.width, rect.height); - Font font = FontManager.getFont(fontSize); + Font font = FontManager.getFont(getFontSize()); g2D.setFont(font); int y = trackRectangle.y; diff --git a/src/main/java/org/broad/igv/prefs/Constants.java b/src/main/java/org/broad/igv/prefs/Constants.java index b8bc1a425f..f87eb6c403 100644 --- a/src/main/java/org/broad/igv/prefs/Constants.java +++ b/src/main/java/org/broad/igv/prefs/Constants.java @@ -304,6 +304,9 @@ private Constants() { public static final String CIRC_VIEW_PORT = "CIRC_VIEW_PORT"; public static final String CIRC_VIEW_HOST = "CIRC_VIEW_HOST"; + // Misc URLS + public static final String ENCODE_FILELIST_URL = "ENCODE_FILELIST_URL"; + /** * List of keys that affect the alignments loaded. This list is used to trigger a reload, if required. diff --git a/src/main/java/org/broad/igv/sam/CoverageTrack.java b/src/main/java/org/broad/igv/sam/CoverageTrack.java index c7bcfae143..b7ebc0e08c 100644 --- a/src/main/java/org/broad/igv/sam/CoverageTrack.java +++ b/src/main/java/org/broad/igv/sam/CoverageTrack.java @@ -147,8 +147,8 @@ public void setColor(Color color) { @Override public String getSample() { - if (sampleId != null) { - return sampleId; // Explicitly set sample ID (e.g. from server load XML) + if (getSampleId() != null) { + return getSampleId(); // Explicitly set sample ID (e.g. from server load XML) } return alignmentTrack == null ? null : alignmentTrack.getSample(); } diff --git a/src/main/java/org/broad/igv/sam/SpliceJunctionTrack.java b/src/main/java/org/broad/igv/sam/SpliceJunctionTrack.java index 2e3d5af972..29640eb0bc 100644 --- a/src/main/java/org/broad/igv/sam/SpliceJunctionTrack.java +++ b/src/main/java/org/broad/igv/sam/SpliceJunctionTrack.java @@ -31,9 +31,6 @@ import org.broad.igv.logging.*; import org.broad.igv.Globals; import org.broad.igv.feature.SpliceJunctionFeature; -import org.broad.igv.prefs.Constants; -import org.broad.igv.prefs.IGVPreferences; -import org.broad.igv.prefs.PreferencesManager; import org.broad.igv.renderer.DataRange; import org.broad.igv.renderer.GraphicUtils; import org.broad.igv.renderer.Renderer; @@ -108,8 +105,8 @@ public void setRenderer(Renderer renderer) { @Override public String getSample() { - if (sampleId != null) { - return sampleId; // Explicitly set sample ID (e.g. from server load XML) + if (getSampleId() != null) { + return getSampleId(); // Explicitly set sample ID (e.g. from server load XML) } return alignmentTrack.getSample(); } diff --git a/src/main/java/org/broad/igv/track/AbstractTrack.java b/src/main/java/org/broad/igv/track/AbstractTrack.java index 8673abb4c1..aa3a4e0afe 100644 --- a/src/main/java/org/broad/igv/track/AbstractTrack.java +++ b/src/main/java/org/broad/igv/track/AbstractTrack.java @@ -63,51 +63,44 @@ */ public abstract class AbstractTrack implements Track { - + private static Logger log = LogManager.getLogger(AbstractTrack.class); public static final Color DEFAULT_COLOR = Color.blue.darker(); public static final DisplayMode DEFAULT_DISPLAY_MODE = DisplayMode.COLLAPSED; public static final int DEFAULT_HEIGHT = -1; public static final int VISIBILITY_WINDOW = -1; public static final boolean DEFAULT_SHOW_FEATURE_NAMES = true; - private static Logger log = LogManager.getLogger(AbstractTrack.class); - - protected String id; + private ResourceLocator resourceLocator; + private String id; + private String sampleId; private String attributeKey; private String name; private String featureInfoURL; private boolean itemRGB = true; private boolean useScore; + protected boolean autoScale; private float viewLimitMin = Float.NaN; // From UCSC track line private float viewLimitMax = Float.NaN; // From UCSC track line - - protected int fontSize; + private int fontSize; private boolean showDataRange = true; - protected String sampleId; - - private ResourceLocator resourceLocator; private int top; protected int minimumHeight = -1; private TrackType trackType = TrackType.OTHER; - private boolean selected = false; private boolean visible = true; - private boolean sortable = true; boolean overlaid; - boolean drawYLine = false; float yLine = 0; + private Map attributes = new HashMap(); private ContinuousColorScale colorScale; - protected boolean autoScale; - String autoscaleGroup; protected Color posColor = null; @@ -857,11 +850,21 @@ public void setDisplayMode(DisplayMode mode) { } - public String getNameValueString(int y) { + public String getTooltipText(int y) { StringBuffer buffer = new StringBuffer(); buffer.append("" + getName()); + Map metadata = resourceLocator.getMetadata(); + if(metadata.size() > 0) { + for(Map.Entry entry : metadata.entrySet()) { + String value = entry.getValue(); + if(value != null && value.length() > 0) { + buffer.append("
" + entry.getKey() + ": " + entry.getValue()); + } + } + } + if (resourceLocator != null && resourceLocator.getPath() != null) { buffer.append("
" + this.resourceLocator.getPath()); } @@ -1180,4 +1183,8 @@ public void unmarshalXML(Element element, Integer version) { } } + public String getSampleId() { + return sampleId; + } + } diff --git a/src/main/java/org/broad/igv/track/CombinedDataTrack.java b/src/main/java/org/broad/igv/track/CombinedDataTrack.java index 87bee53eef..7294fb4e90 100644 --- a/src/main/java/org/broad/igv/track/CombinedDataTrack.java +++ b/src/main/java/org/broad/igv/track/CombinedDataTrack.java @@ -24,8 +24,8 @@ public void marshalXML(Document document, Element element) { super.marshalXML(document, element); - element.setAttribute("track1", ((CombinedDataSource) dataSource).getTrackl().id); - element.setAttribute("track2", ((CombinedDataSource) dataSource).getTrack2().id); + element.setAttribute("track1", ((CombinedDataSource) dataSource).getTrackl().getId()); + element.setAttribute("track2", ((CombinedDataSource) dataSource).getTrack2().getId()); element.setAttribute("op", ((CombinedDataSource) dataSource).getOperation().toString()); diff --git a/src/main/java/org/broad/igv/track/SequenceTrack.java b/src/main/java/org/broad/igv/track/SequenceTrack.java index 55db8d7eb4..edbc437a90 100644 --- a/src/main/java/org/broad/igv/track/SequenceTrack.java +++ b/src/main/java/org/broad/igv/track/SequenceTrack.java @@ -166,7 +166,7 @@ public void renderName(Graphics2D g, Rectangle trackRectangle, Rectangle visible // Use local graphics -- this method corrupts graphics context when exporting to "png" files Graphics2D graphics = (Graphics2D) g.create(); - Font font = FontManager.getFont(fontSize); + Font font = FontManager.getFont(getFontSize()); boolean visible = isVisible(); @@ -421,10 +421,10 @@ public Renderer getRenderer() { } @Override - public String getNameValueString(int y) { + public String getTooltipText(int y) { CodonTable explicitlySelectedTable = CodonTableManager.getInstance().getCurrentCodonTable(); if (explicitlySelectedTable != null) { - String nvs = "" + super.getNameValueString(y); + String nvs = "" + super.getTooltipText(y); nvs += "
Translation Table: "; nvs += explicitlySelectedTable.getDisplayName(); return nvs; diff --git a/src/main/java/org/broad/igv/track/Track.java b/src/main/java/org/broad/igv/track/Track.java index 1f9a2ea22f..553828b516 100644 --- a/src/main/java/org/broad/igv/track/Track.java +++ b/src/main/java/org/broad/igv/track/Track.java @@ -127,7 +127,7 @@ void renderAttributes(Graphics2D graphics, Rectangle trackRectangle, Rectangle v String getDisplayName(); - String getNameValueString(int y); + String getTooltipText(int y); String getSample(); diff --git a/src/main/java/org/broad/igv/ui/IGVMenuBar.java b/src/main/java/org/broad/igv/ui/IGVMenuBar.java index ea8e962778..a0427907d3 100644 --- a/src/main/java/org/broad/igv/ui/IGVMenuBar.java +++ b/src/main/java/org/broad/igv/ui/IGVMenuBar.java @@ -36,12 +36,10 @@ import org.broad.igv.event.IGVEventBus; import org.broad.igv.event.IGVEventObserver; import org.broad.igv.feature.genome.Genome; -import org.broad.igv.feature.genome.GenomeListItem; import org.broad.igv.feature.genome.GenomeManager; import org.broad.igv.feature.genome.GenomeUtils; import org.broad.igv.track.AttributeManager; import org.broad.igv.track.Track; -import org.broad.igv.ui.commandbar.GenomeListManager; import org.broad.igv.ui.commandbar.GenomeSelectionDialog; import org.broad.igv.util.GoogleUtils; import org.broad.igv.oauth.OAuthProvider; @@ -65,12 +63,13 @@ import org.broad.igv.util.BrowserLauncher; import org.broad.igv.util.LongRunningTask; import org.broad.igv.util.blat.BlatClient; -import org.broad.igv.util.encode.EncodeFileBrowser; +import org.broad.igv.encode.EncodeTrackChooser; import javax.swing.*; import javax.swing.event.MenuEvent; import javax.swing.event.MenuListener; import javax.swing.plaf.basic.BasicBorders; +import javax.swing.plaf.basic.BasicMenuItemUI; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -114,7 +113,9 @@ public class IGVMenuBar extends JMenuBar implements IGVEventObserver { private JMenuItem loadGenomeFromServerMenuItem; private JMenuItem loadTracksFromServerMenuItem; private JMenuItem selectGenomeAnnotationsItem; - private JMenuItem encodeMenuItem; + private JMenuItem encodeUCSCMenuItem; + + private List encodeMenuItems = new ArrayList<>(); private JMenuItem reloadSessionItem; @@ -189,7 +190,7 @@ private List createMenus() { // by loading a protected Google resource try { googleMenu = createGoogleMenu(); - if(googleMenu != null) { + if (googleMenu != null) { boolean enabled = PreferencesManager.getPreferences().getAsBoolean(ENABLE_GOOGLE_MENU); enableGoogleMenu(enabled); menus.add(googleMenu); @@ -206,7 +207,6 @@ private List createMenus() { LongRunningTask.submit(this::updateAWSMenu); - menus.add(createHelpMenu()); // Experimental -- remove for production release @@ -277,7 +277,7 @@ public void enableExtrasMenu() { JMenu createFileMenu() { Genome genome = GenomeManager.getInstance().getCurrentGenome(); - String genomeId = genome == null ? null : genome.getId(); + String genomeId = genome == null ? null : genome.getId(); List menuItems = new ArrayList(); MenuAction menuAction = null; @@ -296,31 +296,14 @@ JMenu createFileMenu() { menuAction = new LoadFromServerAction("Load From Server...", KeyEvent.VK_S, igv); menuAction.setToolTipText(UIConstants.LOAD_SERVER_DATA_TOOLTIP); loadTracksFromServerMenuItem = MenuAndToolbarUtils.createMenuItem(menuAction); - // loadTracksFromServerMenuItem.setVisible(genomeId != null && LoadFromServerAction.getNodeURLs(genomeId) != null); menuItems.add(loadTracksFromServerMenuItem); - // Track hubs -- moved to Genomes menu -// menuAction = new LoadFromURLMenuAction(LoadFromURLMenuAction.LOAD_TRACKHUB, KeyEvent.VK_S, igv); -// menuAction.setToolTipText(UIConstants.LOAD_TRACKHUB_TOOLTIP); -// menuItems.add(MenuAndToolbarUtils.createMenuItem(menuAction)); -// -// menuAction = new SelectGenomeAnnotationTracksAction("Select Hub Tracks...", igv); -// selectGenomeAnnotationsItem = MenuAndToolbarUtils.createMenuItem(menuAction); -// selectGenomeAnnotationsItem.setVisible(genome != null && genome.getHub() != null); -// menuItems.add(selectGenomeAnnotationsItem); -// -// menuAction = new LoadFromURLMenuAction(LoadFromURLMenuAction.LOAD_FROM_HTSGET, 0, igv); -// menuAction.setToolTipText(UIConstants.LOAD_HTSGET_TOOLTOP); -// menuItems.add(MenuAndToolbarUtils.createMenuItem(menuAction)); - if (PreferencesManager.getPreferences().getAsBoolean(DB_ENABLED)) { menuAction = new LoadFromDatabaseAction("Load from Database...", 0, igv); menuItems.add(MenuAndToolbarUtils.createMenuItem(menuAction)); } - encodeMenuItem = MenuAndToolbarUtils.createMenuItem(new BrowseEncodeAction("Load from ENCODE (2012)...", KeyEvent.VK_E, igv)); - encodeMenuItem.setVisible(EncodeFileBrowser.genomeSupported(genomeId)); - menuItems.add(encodeMenuItem); + addEncodeItems(menuItems, genomeId); menuItems.add(new JSeparator()); menuAction = new ReloadTracksMenuAction("Reload Tracks", -1, igv); @@ -429,6 +412,40 @@ public void actionPerformed(ActionEvent e) { return fileMenu; } + private void addEncodeItems(List menuItems, String genomeId) { + + JSeparator separator = new JSeparator(); + menuItems.add(separator); + + JLabel encodeLabel = new JLabel(" ENCODE"); + encodeLabel.setFont(encodeLabel.getFont().deriveFont(Font.BOLD)); + menuItems.add(encodeLabel); + + // Post 2012 ENCODE menu + JMenuItem chipItem = new JMenuItem(); + chipItem.setAction(new BrowseEncodeAction("CHiP - Signals", 0, BrowseEncodeAction.Type.SIGNALS_CHIP, igv)); + encodeMenuItems.add(chipItem); + + JMenuItem otherSignalsItem = new JMenuItem(); + otherSignalsItem.setAction(new BrowseEncodeAction("Other - Signals", 0, BrowseEncodeAction.Type.SIGNALS_OTHER, igv)); + encodeMenuItems.add(otherSignalsItem); + + JMenuItem otherItem = new JMenuItem(); + otherItem.setAction(new BrowseEncodeAction("Other (peaks, calls, ...)", 0, BrowseEncodeAction.Type.OTHER, igv)); + encodeMenuItems.add(otherItem); + + for(JComponent item : encodeMenuItems) { + menuItems.add(item); + item.setVisible(EncodeTrackChooser.genomeSupported(genomeId)); + } + + // UCSC hosted ENCODE menu. + encodeUCSCMenuItem = MenuAndToolbarUtils.createMenuItem( + new BrowseEncodeAction("ENCODE (2012)...", KeyEvent.VK_E, BrowseEncodeAction.Type.UCSC, igv)); + encodeUCSCMenuItem.setVisible(EncodeTrackChooser.genomeSupportedUCSC(genomeId)); + menuItems.add(encodeUCSCMenuItem); + } + private JMenu createGenomesMenu() { JMenu menu = new JMenu("Genomes"); @@ -1052,7 +1069,7 @@ public void menuCanceled(MenuEvent e) { private JMenu createGoogleMenu() { final OAuthProvider googleProvider = OAuthUtils.getInstance().getGoogleProvider(); - if(googleProvider == null) { + if (googleProvider == null) { log.error("Error creating google oauth provider"); return null; } @@ -1121,17 +1138,11 @@ public void menuCanceled(MenuEvent e) { * @throws IOException */ public void enableGoogleMenu(boolean enable) throws IOException { - if(googleMenu != null) { + if (googleMenu != null) { googleMenu.setVisible(enable); } } -// public void enableRemoveGenomes() { -// if (removeImportedGenomeAction != null) { -// removeImportedGenomeAction.setEnabled(true); -// } -// } - public void resetSessionActions() { if (filterTracksAction != null) { filterTracksAction.resetTrackFilter(); @@ -1169,10 +1180,6 @@ public boolean isFilterShowAllTracks() { return false; } - public JMenu getViewMenu() { - return viewMenu; - } - final public void doExitApplication() { try { @@ -1201,7 +1208,12 @@ public void receiveEvent(final IGVEvent event) { if (event instanceof GenomeChangeEvent) { UIUtilities.invokeOnEventThread(() -> { final Genome genome = ((GenomeChangeEvent) event).genome(); - encodeMenuItem.setVisible(EncodeFileBrowser.genomeSupported(genome.getId())); + final String genomeId = genome.getId(); + encodeUCSCMenuItem.setVisible(EncodeTrackChooser.genomeSupportedUCSC(genomeId)); + for(JComponent item : encodeMenuItems) { + item.setVisible(EncodeTrackChooser.genomeSupported(genomeId)); + } + }); } } @@ -1278,5 +1290,15 @@ private void exportTrackNames(final Collection selectedTracks) { } } +} + +class Foo extends BasicMenuItemUI { + + + public Foo() { + this.disabledForeground = Color.black; + } + + } diff --git a/src/main/java/org/broad/igv/ui/action/BrowseEncodeAction.java b/src/main/java/org/broad/igv/ui/action/BrowseEncodeAction.java index 01f1225f3f..c5eb6c64dc 100644 --- a/src/main/java/org/broad/igv/ui/action/BrowseEncodeAction.java +++ b/src/main/java/org/broad/igv/ui/action/BrowseEncodeAction.java @@ -30,18 +30,19 @@ import org.broad.igv.feature.genome.Genome; import org.broad.igv.track.AttributeManager; import org.broad.igv.ui.IGV; +import org.broad.igv.ui.WaitCursorManager; import org.broad.igv.ui.util.MessageUtils; import org.broad.igv.util.ResourceLocator; -import org.broad.igv.util.encode.EncodeFileBrowser; -import org.broad.igv.util.encode.EncodeFileRecord; +import org.broad.igv.encode.EncodeTrackChooser; +import org.broad.igv.encode.EncodeFileRecord; +import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; +import java.util.*; import java.util.List; -import java.util.Map; +import java.util.concurrent.ExecutionException; /** * @author jrobinso @@ -50,6 +51,14 @@ */ public class BrowseEncodeAction extends MenuAction { + + public enum Type { + UCSC, + SIGNALS_CHIP, + SIGNALS_OTHER, + OTHER + } + private static Logger log = LogManager.getLogger(BrowseEncodeAction.class); private static Map colors; @@ -66,11 +75,16 @@ public class BrowseEncodeAction extends MenuAction { colors.put("H3K9ME1", new Color(100, 0, 0)); } + static Set sampleInfoAttributes = new HashSet<>(Arrays.asList( + "dataType", "cell", "antibody", "lab", "Biosample", "AssayType", "Target")); + + private final Type type; IGV igv; - public BrowseEncodeAction(String label, int mnemonic, IGV igv) { + public BrowseEncodeAction(String label, int mnemonic, Type type, IGV igv) { super(label, null, mnemonic); + this.type = type; this.igv = igv; } @@ -78,46 +92,66 @@ public BrowseEncodeAction(String label, int mnemonic, IGV igv) { @Override public void actionPerformed(ActionEvent event) { - String[] visibleAttributes = {"dataType", "cell", "antibody", "lab"}; - try { - Genome genome = GenomeManager.getInstance().getCurrentGenome(); - EncodeFileBrowser browser = EncodeFileBrowser.getInstance(genome.getId()); + Genome genome = GenomeManager.getInstance().getCurrentGenome(); - if (browser == null) { - MessageUtils.showMessage("Encode data is not available for " + genome.getDisplayName() + " through IGV."); - return; + final WaitCursorManager.CursorToken token = WaitCursorManager.showWaitCursor(); + SwingWorker worker = new SwingWorker() { + @Override + protected EncodeTrackChooser doInBackground() throws Exception { + return EncodeTrackChooser.getInstance(genome.getId(), BrowseEncodeAction.this.type); } - browser.setVisible(true); - if (browser.isCanceled()) return; + @Override + protected void done() { + WaitCursorManager.removeWaitCursor(token); + try { + EncodeTrackChooser chooser = get(); + if (chooser == null) { + MessageUtils.showMessage("Encode data is not available for " + genome.getDisplayName() + " through IGV."); + return; + } - java.util.List records = browser.getSelectedRecords(); - if (records.size() > 0) { - List locators = new ArrayList(records.size()); - for (EncodeFileRecord record : records) { - ResourceLocator rl = new ResourceLocator(record.getPath()); - rl.setName(record.getTrackName()); + chooser.setVisible(true); + if (chooser.isCanceled()) return; - final String antibody = record.getAttributeValue("antibody"); - if (antibody != null) { - rl.setColor(colors.get(antibody.toUpperCase())); - } + java.util.List records = chooser.getSelectedRecords(); + if (records.size() > 0) { + List locators = new ArrayList<>(records.size()); + for (EncodeFileRecord record : records) { - for (String name : visibleAttributes) { - String value = record.getAttributeValue(name); - if (value != null) { - AttributeManager.getInstance().addAttribute(rl.getName(), name, value); + ResourceLocator rl = new ResourceLocator(record.getPath()); + rl.setName(record.getTrackName()); + + Map attributes = record.getAttributes(); + + String antibody = attributes.containsKey("antibody") ? attributes.get("antibody") : attributes.get("Target"); + if (antibody != null) { + rl.setColor(colors.get(antibody.toUpperCase())); + } + + for (Map.Entry entry : attributes.entrySet()) { + String value = entry.getValue(); + if (value != null && value.length() > 0 && sampleInfoAttributes.contains(entry.getKey())) { + AttributeManager.getInstance().addAttribute(rl.getName(), entry.getKey(), value); + } + } + + rl.setMetadata(attributes); + + locators.add(rl); } + igv.loadTracks(locators); } - locators.add(rl); + } catch (Exception e) { + log.error("Error opening Encode browser", e); + throw new RuntimeException(e); } - igv.loadTracks(locators); } + }; + + worker.execute(); - } catch (IOException e) { - log.error("Error opening Encode browser", e); - } } } diff --git a/src/main/java/org/broad/igv/ui/action/UCSCGenArkAction.java b/src/main/java/org/broad/igv/ui/action/UCSCGenArkAction.java index 785d23a307..bdb06b20f9 100644 --- a/src/main/java/org/broad/igv/ui/action/UCSCGenArkAction.java +++ b/src/main/java/org/broad/igv/ui/action/UCSCGenArkAction.java @@ -26,26 +26,17 @@ package org.broad.igv.ui.action; import org.broad.igv.Globals; -import org.broad.igv.feature.genome.Genome; -import org.broad.igv.feature.genome.GenomeListItem; import org.broad.igv.feature.genome.GenomeManager; import org.broad.igv.feature.genome.load.HubGenomeLoader; import org.broad.igv.logging.LogManager; import org.broad.igv.logging.Logger; -import org.broad.igv.track.AttributeManager; import org.broad.igv.ui.IGV; -import org.broad.igv.ui.commandbar.GenomeListManager; import org.broad.igv.ui.table.SearchableTableDialog; import org.broad.igv.ui.table.SearchableTableModel; import org.broad.igv.ui.table.SearchableTableRecord; import org.broad.igv.ui.util.MessageUtils; -import org.broad.igv.util.FileUtils; import org.broad.igv.util.ParsingUtils; -import org.broad.igv.util.ResourceLocator; -import org.broad.igv.util.encode.EncodeFileBrowser; -import org.broad.igv.util.encode.EncodeFileRecord; -import java.awt.*; import java.awt.event.ActionEvent; import java.io.BufferedReader; import java.io.IOException; diff --git a/src/main/java/org/broad/igv/ui/panel/TrackNamePanel.java b/src/main/java/org/broad/igv/ui/panel/TrackNamePanel.java index 43c7a123fb..90b76771f9 100644 --- a/src/main/java/org/broad/igv/ui/panel/TrackNamePanel.java +++ b/src/main/java/org/broad/igv/ui/panel/TrackNamePanel.java @@ -312,7 +312,7 @@ public String getTooltipTextForLocation(int x, int y) { Collection tracks = mouseableRegion.getTracks(); if (tracks != null && tracks.size() == 1) { Track track = tracks.iterator().next(); - text = track.getNameValueString(y); + text = track.getTooltipText(y); } else { text = mouseableRegion.getText(); } diff --git a/src/main/java/org/broad/igv/util/ResourceLocator.java b/src/main/java/org/broad/igv/util/ResourceLocator.java index abf3c99fbb..7fb3e4b030 100644 --- a/src/main/java/org/broad/igv/util/ResourceLocator.java +++ b/src/main/java/org/broad/igv/util/ResourceLocator.java @@ -123,7 +123,11 @@ public class ResourceLocator { String sampleId; - private HashMap attributes = new HashMap(); + + /** + * Track metadata. Primarily for generating description and popup text. + */ + private Map metadata; private boolean indexed; private boolean dataURL; @@ -622,6 +626,13 @@ public String getTrixURL() { return trixURL; } + public Map getMetadata() { + return metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } public static ResourceLocator fromTrackConfig(TrackConfig trackConfig) { String trackPath = trackConfig.url; diff --git a/src/main/java/org/broad/igv/variant/VariantTrack.java b/src/main/java/org/broad/igv/variant/VariantTrack.java index 1e3633edf1..b6a8e13db5 100644 --- a/src/main/java/org/broad/igv/variant/VariantTrack.java +++ b/src/main/java/org/broad/igv/variant/VariantTrack.java @@ -672,7 +672,7 @@ public void renderName(Graphics2D g2D, Rectangle trackRectangle, Rectangle visib top = trackRectangle.y; Rectangle rect = new Rectangle(trackRectangle); - g2D.setFont(FontManager.getFont(fontSize)); + g2D.setFont(FontManager.getFont(getFontSize())); g2D.setColor(BAND2_COLOR); @@ -806,7 +806,7 @@ private void drawBackground(Graphics2D g2D, Rectangle bandRectangle, Rectangle v Rectangle textRectangle = new Rectangle(bandRectangle); textRectangle.height--; - int bandFontSize = Math.min(fontSize, (int) bandRectangle.getHeight() - 1); + int bandFontSize = Math.min(getFontSize(), (int) bandRectangle.getHeight() - 1); Font font = FontManager.getFont(bandFontSize); Font oldFont = g2D.getFont(); g2D.setFont(font); @@ -915,9 +915,9 @@ public void setColor(Color color) { super.setColor(color); } - public String getNameValueString(int y) { + public String getTooltipText(int y) { if (y < top + getVariantsHeight()) { - return super.getNameValueString(y); + return super.getTooltipText(y); } else { String sample = getSampleAtPosition(y); return sample; diff --git a/src/main/resources/org/broad/igv/util/encode/encode.hg18.txt b/src/main/resources/org/broad/igv/encode/encode.hg18.txt similarity index 100% rename from src/main/resources/org/broad/igv/util/encode/encode.hg18.txt rename to src/main/resources/org/broad/igv/encode/encode.hg18.txt diff --git a/src/main/resources/org/broad/igv/util/encode/encode.hg19.txt b/src/main/resources/org/broad/igv/encode/encode.hg19.txt similarity index 100% rename from src/main/resources/org/broad/igv/util/encode/encode.hg19.txt rename to src/main/resources/org/broad/igv/encode/encode.hg19.txt diff --git a/src/main/resources/org/broad/igv/util/encode/encode.mm9.txt b/src/main/resources/org/broad/igv/encode/encode.mm9.txt similarity index 100% rename from src/main/resources/org/broad/igv/util/encode/encode.mm9.txt rename to src/main/resources/org/broad/igv/encode/encode.mm9.txt diff --git a/src/main/resources/org/broad/igv/prefs/preferences.tab b/src/main/resources/org/broad/igv/prefs/preferences.tab index 9d7944d8b2..4ca38fe9e9 100644 --- a/src/main/resources/org/broad/igv/prefs/preferences.tab +++ b/src/main/resources/org/broad/igv/prefs/preferences.tab @@ -294,6 +294,8 @@ CIRC_VIEW_PORT CircView port integer 60152 #Hidden +ENCODE_FILELIST_URL https://s3.amazonaws.com/igv.org.app/encode/ + PROVISIONING_URL_DEFAULT https://igv.org/services/desktop_google SAM.SHOW_JUNCTION_FLANKINGREGIONS FALSE SAM.JUNCTION_MIN_FLANKING_WIDTH 0 From 51ced332bcd9e5d9aaa6b70b02981398868f94c3 Mon Sep 17 00:00:00 2001 From: kunmonster <71956115+kunmonster@users.noreply.github.com> Date: Mon, 7 Oct 2024 11:34:51 +0800 Subject: [PATCH 066/130] Fix issue1512 (#1590) * add restriction on the values of (x,y),if the value of x is less than -width or the value of y is less than -height,the windows will be hidden. fixs issue #1512 --- src/main/java/org/broad/igv/ui/IGV.java | 43 +++++++++++++++++++++---- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/broad/igv/ui/IGV.java b/src/main/java/org/broad/igv/ui/IGV.java index 3c1f3e3a3e..421a4628dc 100644 --- a/src/main/java/org/broad/igv/ui/IGV.java +++ b/src/main/java/org/broad/igv/ui/IGV.java @@ -243,21 +243,52 @@ public void windowGainedFocus(WindowEvent windowEvent) { mainFrame.setMinimumSize(new Dimension(300, 300)); // Set the application's previous location and size + // get the current main screen bounds 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); + // get info of all screens and store them into the array + GraphicsEnvironment graphEnv = GraphicsEnvironment.getLocalGraphicsEnvironment(); + GraphicsDevice[] graphDev = graphEnv.getScreenDevices(); + Rectangle[] boundsArr = new Rectangle[graphDev.length]; + + for (int i=0;i= curScreen.getMaxX() || userMaxY >= curScreen.getMaxY()){ + applicationBounds = new Rectangle(curScreen.x,curScreen.y,Math.min(1150,curScreen.width),Math.min(800,curScreen.height)); + } + break; + } + } + } + if(isNullOrNotContained){ + // user's preference is null or the (x,y) in user's preference is not contained in any screen + // set the application to the main screen + applicationBounds = new Rectangle(0, 0, Math.min(1150,screenBounds.width), Math.min(800,screenBounds.height)); } mainFrame.setBounds(applicationBounds); subscribeToEvents(); + // Start running periodic autosaves (unless the user has specified not to retain timed autosaves) + if (PreferencesManager.getPreferences().getAsInt(Constants.AUTOSAVES_TO_KEEP) > 0) { int timerDelay = PreferencesManager.getPreferences().getAsInt(AUTOSAVE_FREQUENCY) * 60000; // Convert timer delay to ms sessionAutosaveTimer.scheduleAtFixedRate(new AutosaveTimerTask(this), timerDelay, timerDelay); From 15e8c2aac604cc61fb44345d1f2476b42e65c554 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Mon, 7 Oct 2024 11:22:27 -0700 Subject: [PATCH 067/130] null check --- src/main/java/org/broad/igv/track/AbstractTrack.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/broad/igv/track/AbstractTrack.java b/src/main/java/org/broad/igv/track/AbstractTrack.java index aa3a4e0afe..59226aabbb 100644 --- a/src/main/java/org/broad/igv/track/AbstractTrack.java +++ b/src/main/java/org/broad/igv/track/AbstractTrack.java @@ -856,7 +856,7 @@ public String getTooltipText(int y) { buffer.append("" + getName()); Map metadata = resourceLocator.getMetadata(); - if(metadata.size() > 0) { + if(metadata != null && metadata.size() > 0) { for(Map.Entry entry : metadata.entrySet()) { String value = entry.getValue(); if(value != null && value.length() > 0) { From 4778137e61534f4f2507ecc2126a255cd2d51b19 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Wed, 9 Oct 2024 10:12:33 -0700 Subject: [PATCH 068/130] Extend file format determination code. --- .../org/broad/igv/track/FileFormatUtils.java | 22 +++++++++++++++++++ .../org/broad/igv/track/TrackProperties.java | 12 ++++++++++ .../java/org/broad/igv/util/ParsingUtils.java | 7 ++++-- .../broad/igv/track/FileFormatUtilsTest.java | 4 ++++ 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/broad/igv/track/FileFormatUtils.java b/src/main/java/org/broad/igv/track/FileFormatUtils.java index 4969624c83..6171ff27c9 100644 --- a/src/main/java/org/broad/igv/track/FileFormatUtils.java +++ b/src/main/java/org/broad/igv/track/FileFormatUtils.java @@ -4,6 +4,7 @@ import htsjdk.samtools.util.BlockCompressedInputStream; import org.broad.igv.ucsc.twobit.UnsignedByteBuffer; import org.broad.igv.ucsc.twobit.UnsignedByteBufferImpl; +import org.broad.igv.util.ParsingUtils; import org.broad.igv.util.stream.IGVSeekableStreamFactory; import java.io.*; @@ -89,6 +90,27 @@ public static String determineFormat(String path) throws IOException { if (firstLine.startsWith("##gff-version")) { return "gff"; } + if(firstLine.startsWith("##fileformat=")) { + return firstLine.substring(13); // Non standard extension of VCF convention + } + + // Read maximum of first 100 lines searching for format indication. + int n = 0; + String nextLine; + while((nextLine = reader.readLine()) != null && n++ < 100) { + if(nextLine.startsWith("#")) continue; + if(nextLine.startsWith("track")) { + TrackProperties properties = new TrackProperties(); + ParsingUtils.parseTrackLine(nextLine, properties); + if(properties.getFormat() != null) { + return properties.getFormat(); + } + } + if(nextLine.startsWith("fixedStep") || nextLine.startsWith("variableStep")) { + return "wig"; + } + } + if (maybeSampleInfo(bytes)) { return "sampleinfo"; } diff --git a/src/main/java/org/broad/igv/track/TrackProperties.java b/src/main/java/org/broad/igv/track/TrackProperties.java index 16ffa2043f..96658053f7 100644 --- a/src/main/java/org/broad/igv/track/TrackProperties.java +++ b/src/main/java/org/broad/igv/track/TrackProperties.java @@ -143,6 +143,11 @@ public enum BaseCoord { private String coverageURL; + /** + * Non-standard track field to indicate file format + */ + private String format; + /** * Track attributes (meta data) */ @@ -153,6 +158,13 @@ public TrackProperties() { } + public String getFormat() { + return format; + } + + public void setFormat(String format) { + this.format = format; + } public void setTrackLine(String trackLine) { this.trackLine = trackLine; diff --git a/src/main/java/org/broad/igv/util/ParsingUtils.java b/src/main/java/org/broad/igv/util/ParsingUtils.java index 06e4685aa3..68d72fe00d 100644 --- a/src/main/java/org/broad/igv/util/ParsingUtils.java +++ b/src/main/java/org/broad/igv/util/ParsingUtils.java @@ -386,7 +386,10 @@ public static boolean parseTrackLine(String nextLine, TrackProperties trackPrope String key = kv.get(0).toLowerCase().trim(); String value = kv.get(1).replaceAll("\"", ""); - if (key.equals("coords")) { + if(key.equals("format")) { + trackProperties.setFormat(value); + } + else if (key.equals("coords")) { if (value.equals("0")) { trackProperties.setBaseCoord(TrackProperties.BaseCoord.ZERO); } else if (value.equals("1")) { @@ -394,7 +397,7 @@ public static boolean parseTrackLine(String nextLine, TrackProperties trackPrope } } - if (key.equals("name")) { + else if (key.equals("name")) { trackProperties.setName(value); //dhmay adding name check for TopHat junctions files. graphType is also checked. if (value.equals("junctions")) { diff --git a/src/test/java/org/broad/igv/track/FileFormatUtilsTest.java b/src/test/java/org/broad/igv/track/FileFormatUtilsTest.java index 1010d9537d..eaf23587d4 100644 --- a/src/test/java/org/broad/igv/track/FileFormatUtilsTest.java +++ b/src/test/java/org/broad/igv/track/FileFormatUtilsTest.java @@ -42,5 +42,9 @@ public void testDetermineFormat() throws Exception { String sampleInfoFile = "http://igvdata.broadinstitute.org/data/hg18/tcga/gbm/gbmsubtypes/sampleTable.txt.gz"; format = FileFormatUtils.determineFormat(sampleInfoFile); assertEquals("sampleinfo", format); + + String wigFile = TestUtils.DATA_DIR + "wig/dm3_var_sample.wig"; + format = FileFormatUtils.determineFormat(wigFile); + assertEquals("wig", format); } } \ No newline at end of file From c0e7f091d6752046b43c0daa710db39e7039fd6d Mon Sep 17 00:00:00 2001 From: Louis Bergelson Date: Fri, 11 Oct 2024 11:31:34 -0400 Subject: [PATCH 069/130] Fix for Amazon failing. (#1599) * Fix the amazon failures due to LogFactory not found by adding a module dependency (#1598) * on apache logging. This is necessary because commons-logging became modularized * between 1.2.0 and 1.3.0 but amazon is expecting the non-modular 1.2.0 while * 1.3.0 is being brought in due to changes in htsjdk. * See https://logging.apache.org/blog/2023/12/02/apache-common-logging-1.3.0.html for * more info. * Fix for https://github.com/igvteam/igv/issues/1598 --- build.gradle | 9 ++++++++- src/main/java/module-info.java | 4 ++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ab85c10394..16d6f892d5 100644 --- a/build.gradle +++ b/build.gradle @@ -137,9 +137,16 @@ dependencies { [group: 'software.amazon.awssdk', name: 'cognitoidentity', version: amazonVersion], [group: 'software.amazon.awssdk', name: 'sts', version: amazonVersion], [group: 'software.amazon.awssdk', name: 's3', version: amazonVersion], - [group: 'software.amazon.awssdk', name: 'sso', version: amazonVersion] + [group: 'software.amazon.awssdk', name: 'sso', version: amazonVersion], + + // This is a transitive dependency from amazon but due to modularization changes between + // 1.20 and 1.3.0 we need to specify it here. + // This can be removed when amazon moves from 1.2.0 -> 1.3.0 and updates it's module requirements + // See https://logging.apache.org/blog/2023/12/02/apache-common-logging-1.3.0.html + [group: 'commons-logging', name: 'commons-logging', version: '1.3.0'] ) + testImplementation( [group: 'junit', name: 'junit', version: '4.13.2'], [group: 'com.sparkjava', name: 'spark-core', version: '2.9.4'], diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index fb0b4a1f3c..20ea2ca08f 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -43,5 +43,9 @@ requires software.amazon.awssdk.http; requires software.amazon.awssdk.utils; + // Transitive dependency of amazon modules that is required to be specified + // because amazon resolves the unmodularized 1.2.0 version while htjsdk brings in the modular 1.3.0 + requires org.apache.commons.logging; + requires jide.oss; } From bfaab9d952292dafecd814bf570b30d01891d102 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Sat, 12 Oct 2024 22:41:53 -0700 Subject: [PATCH 070/130] lowercase for fileformat comparison --- src/main/java/org/broad/igv/track/FileFormatUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/broad/igv/track/FileFormatUtils.java b/src/main/java/org/broad/igv/track/FileFormatUtils.java index 6171ff27c9..b92f9f4a41 100644 --- a/src/main/java/org/broad/igv/track/FileFormatUtils.java +++ b/src/main/java/org/broad/igv/track/FileFormatUtils.java @@ -91,7 +91,7 @@ public static String determineFormat(String path) throws IOException { return "gff"; } if(firstLine.startsWith("##fileformat=")) { - return firstLine.substring(13); // Non standard extension of VCF convention + return firstLine.substring(13).toLowerCase(); // Non standard extension of VCF convention } // Read maximum of first 100 lines searching for format indication. From f36d4cc8219864448dd2a058573badc9afd0c45c Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Sun, 13 Oct 2024 22:57:47 -0700 Subject: [PATCH 071/130] Rename ENCODE items --- .../java/org/broad/igv/ui/IGVMenuBar.java | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/broad/igv/ui/IGVMenuBar.java b/src/main/java/org/broad/igv/ui/IGVMenuBar.java index a0427907d3..fda2e3510b 100644 --- a/src/main/java/org/broad/igv/ui/IGVMenuBar.java +++ b/src/main/java/org/broad/igv/ui/IGVMenuBar.java @@ -303,7 +303,37 @@ JMenu createFileMenu() { menuItems.add(MenuAndToolbarUtils.createMenuItem(menuAction)); } - addEncodeItems(menuItems, genomeId); + // ENCODE items. These will be hidden / shown depending on genome chosen + JSeparator separator = new JSeparator(); + menuItems.add(separator); + + JLabel encodeLabel = new JLabel(" ENCODE"); + encodeLabel.setFont(encodeLabel.getFont().deriveFont(Font.BOLD)); + menuItems.add(encodeLabel); + + // Post 2012 ENCODE menu + JMenuItem chipItem = new JMenuItem(); + chipItem.setAction(new BrowseEncodeAction("Signals ChIP ...", 0, BrowseEncodeAction.Type.SIGNALS_CHIP, igv)); + encodeMenuItems.add(chipItem); + + JMenuItem otherSignalsItem = new JMenuItem(); + otherSignalsItem.setAction(new BrowseEncodeAction("Signals Other ...", 0, BrowseEncodeAction.Type.SIGNALS_OTHER, igv)); + encodeMenuItems.add(otherSignalsItem); + + JMenuItem otherItem = new JMenuItem(); + otherItem.setAction(new BrowseEncodeAction("Other ...", 0, BrowseEncodeAction.Type.OTHER, igv)); + encodeMenuItems.add(otherItem); + + for(JComponent item : encodeMenuItems) { + menuItems.add(item); + item.setVisible(EncodeTrackChooser.genomeSupported(genomeId)); + } + + // UCSC hosted ENCODE menu. + encodeUCSCMenuItem = MenuAndToolbarUtils.createMenuItem( + new BrowseEncodeAction("UCSC Repository (2012) ...", KeyEvent.VK_E, BrowseEncodeAction.Type.UCSC, igv)); + encodeUCSCMenuItem.setVisible(EncodeTrackChooser.genomeSupportedUCSC(genomeId)); + menuItems.add(encodeUCSCMenuItem); menuItems.add(new JSeparator()); menuAction = new ReloadTracksMenuAction("Reload Tracks", -1, igv); From d0afb727e8533b1a14b5d91b127cba37c4a0a776 Mon Sep 17 00:00:00 2001 From: Jim Robinson <933148+jrobinso@users.noreply.github.com> Date: Fri, 18 Oct 2024 20:36:42 -0700 Subject: [PATCH 072/130] Genomes (#1603) Add support for downloading genomes including sequence and annotations. --- .../java/org/broad/igv/DirectoryManager.java | 3 +- .../org/broad/igv/batch/CommandExecutor.java | 25 +- ...{GenomeUtils.java => ChromSizesUtils.java} | 102 +---- .../igv/feature/genome/DotGenomeUtils.java | 89 ++++ .../org/broad/igv/feature/genome/Genome.java | 109 +++-- .../feature/genome/GenomeDownloadUtils.java | 164 +++++++ .../igv/feature/genome/GenomeListItem.java | 12 +- .../igv/feature/genome/GenomeManager.java | 254 ++++------- .../igv/feature/genome/fasta/FastaIndex.java | 3 + .../feature/genome/load/ChromsizesLoader.java | 7 +- .../feature/genome/load/DotGenomeLoader.java | 23 +- .../genome/load/FastaGenomeLoader.java | 63 +-- .../feature/genome/load/GenbankLoader.java | 8 +- .../igv/feature/genome/load/GenomeConfig.java | 327 ++++++++++++-- .../igv/feature/genome/load/GenomeLoader.java | 15 +- .../genome/load/GenomeObjectLoader.java | 24 - .../feature/genome/load/HubGenomeLoader.java | 14 +- .../feature/genome/load/JsonGenomeLoader.java | 98 +++-- .../igv/feature/genome/load/TrackConfig.java | 271 ++++++++++-- src/main/java/org/broad/igv/ucsc/Hub.java | 99 +++-- .../igv/ucsc/HubTrackSelectionDialog.java | 23 +- .../broad/igv/ucsc/twobit/TwoBitIndex.java | 10 +- .../broad/igv/ucsc/twobit/TwoBitSequence.java | 7 +- src/main/java/org/broad/igv/ui/IGV.java | 50 +-- .../java/org/broad/igv/ui/IGVMenuBar.java | 18 +- .../SelectGenomeAnnotationTracksAction.java | 10 +- .../igv/ui/commandbar/GenomeComboBox.java | 50 +-- .../igv/ui/commandbar/GenomeListManager.java | 410 ++++++++---------- .../ui/commandbar/GenomeSelectionDialog.java | 361 --------------- .../HostedGenomeSelectionDialog.java | 352 +++++++++++++++ .../igv/ui/commandbar/IGVCommandBar.java | 39 +- .../ui/commandbar/RemoveGenomesDialog.java | 49 +-- .../org/broad/igv/ui/panel/DataPanel.java | 12 +- .../igv/ui/util/download/Downloader.java | 2 +- .../org/broad/igv/util/ResourceLocator.java | 38 +- .../igv/feature/genome/GenomeManagerTest.java | 1 - .../broad/igv/feature/genome/GenomeTest.java | 25 +- .../broad/igv/tools/IGVToolsCountTest.java | 1 - src/test/java/org/broad/igv/ucsc/HubTest.java | 14 +- .../java/org/broad/igv/util/TestUtils.java | 5 +- test/batch/genome.txt | 2 + 41 files changed, 1770 insertions(+), 1419 deletions(-) rename src/main/java/org/broad/igv/feature/genome/{GenomeUtils.java => ChromSizesUtils.java} (54%) create mode 100644 src/main/java/org/broad/igv/feature/genome/DotGenomeUtils.java create mode 100644 src/main/java/org/broad/igv/feature/genome/GenomeDownloadUtils.java delete mode 100644 src/main/java/org/broad/igv/feature/genome/load/GenomeObjectLoader.java delete mode 100644 src/main/java/org/broad/igv/ui/commandbar/GenomeSelectionDialog.java create mode 100644 src/main/java/org/broad/igv/ui/commandbar/HostedGenomeSelectionDialog.java create mode 100644 test/batch/genome.txt diff --git a/src/main/java/org/broad/igv/DirectoryManager.java b/src/main/java/org/broad/igv/DirectoryManager.java index 5d7c98a5e1..994c274c4b 100644 --- a/src/main/java/org/broad/igv/DirectoryManager.java +++ b/src/main/java/org/broad/igv/DirectoryManager.java @@ -387,8 +387,7 @@ public static void moveDirectoryContents(File oldDirectory, File newDirectory) { } } - public static boolean isChildOf(File base, File child) - throws IOException { + public static boolean isChildOf(File base, File child) { File parent = child.getParentFile(); while (parent != null) { diff --git a/src/main/java/org/broad/igv/batch/CommandExecutor.java b/src/main/java/org/broad/igv/batch/CommandExecutor.java index ee1c8daa9c..c3db745217 100755 --- a/src/main/java/org/broad/igv/batch/CommandExecutor.java +++ b/src/main/java/org/broad/igv/batch/CommandExecutor.java @@ -123,7 +123,7 @@ public String execute(String commandLine) { } else if (cmd.equalsIgnoreCase("scrolltotrack") || cmd.equalsIgnoreCase("gototrack")) { boolean res = this.igv.scrollToTrack(StringUtils.stripQuotes(param1)); result = res ? "OK" : String.format("Error: Track %s not found", param1); - } else if (cmd.equalsIgnoreCase("scrolltotop") ) { + } else if (cmd.equalsIgnoreCase("scrolltotop")) { this.igv.scrollToTop(); result = "OK"; } else if (cmd.equalsIgnoreCase("snapshotdirectory")) { @@ -143,8 +143,7 @@ public String execute(String commandLine) { return result; } String id = GenomeManager.getInstance().getCurrentGenome().getId(); - if (id != null) - { + if (id != null) { GenomeListItem item = GenomeListManager.getInstance().getGenomeListItem(id); if (item != null) { result = item.getPath(); @@ -633,25 +632,21 @@ private String genome(String param1) { if (param1 == null) { return "ERROR missing genome parameter"; } - String result = "OK"; - String genomeID = param1; - - igv.selectGenomeFromList(genomeID); - if (GenomeManager.getInstance().getCurrentGenome().getId().equals(genomeID)) { - return result; - } + String result; + String genomeIDorPath = param1; - String genomePath = resolveFileReference(genomeID); try { - GenomeManager.getInstance().loadGenome(genomePath); + GenomeManager.getInstance().loadGenomeById(genomeIDorPath); + result = "OK"; } catch (IOException e) { - result = "ERROR: Could not load genome: " + genomeID; + result = "ERROR: Could not load genome: " + genomeIDorPath; MessageUtils.showMessage(result); } return result; } + /** * Load function for port and batch script * @@ -1263,9 +1258,9 @@ private static AlignmentTrack.GroupOption getAlignmentGroupOption(String str) { return AlignmentTrack.GroupOption.READ_GROUP; } else if (str.equalsIgnoreCase("base")) { return AlignmentTrack.GroupOption.BASE_AT_POS; - }else if (str.equalsIgnoreCase("insertion")) { + } else if (str.equalsIgnoreCase("insertion")) { return AlignmentTrack.GroupOption.INSERTION_AT_POS; - }else { + } else { try { return AlignmentTrack.GroupOption.valueOf(str.toUpperCase()); } catch (IllegalArgumentException e) { diff --git a/src/main/java/org/broad/igv/feature/genome/GenomeUtils.java b/src/main/java/org/broad/igv/feature/genome/ChromSizesUtils.java similarity index 54% rename from src/main/java/org/broad/igv/feature/genome/GenomeUtils.java rename to src/main/java/org/broad/igv/feature/genome/ChromSizesUtils.java index c414de8467..3858bd9a2e 100644 --- a/src/main/java/org/broad/igv/feature/genome/GenomeUtils.java +++ b/src/main/java/org/broad/igv/feature/genome/ChromSizesUtils.java @@ -25,12 +25,11 @@ package org.broad.igv.feature.genome; -import org.broad.igv.Globals; import org.broad.igv.feature.Chromosome; import org.broad.igv.util.ParsingUtils; import java.io.*; -import java.util.*; + /** * Static utility functions for genome data-wrangling. @@ -39,24 +38,14 @@ * Date: 4/22/13 * Time: 1:27 PM */ -public class GenomeUtils { +public class ChromSizesUtils { public static void main(String[] args) throws IOException { String genomeListFile = "genomes/genomes.tab"; String outputDirectory = "genomes/sizes"; - String outputFile = "nonFastas.txt"; - updateChromSizes(genomeListFile, new File(outputDirectory)); - - //findNonFastas(genomeListFile, new File(outputFile)); - -// mergeINCDCNames( -// new File("genomes/alias/hg38_alias.tab"), -// new File("/Users/jrobinso/projects/INSDC/GCF_000001405.26.assembly.txt"), -// new File("/Users/jrobinso/projects/INSDC")); - } @@ -136,92 +125,5 @@ public static void exportChromSizes(File directory, Genome genome) throws FileNo } - /** - * Merge chromosome names from an NCBI assembly.txt file with an existing IGV alias file - * - * @param aliasFile - * @param assemblyFile - */ - public static void mergeINCDCNames(File aliasFile, File assemblyFile, File outputDirectory) throws IOException { - - Map> aliasRows = new LinkedHashMap>(); - - BufferedReader br = null; - PrintWriter pw = null; - - // Build alias dictionary - br = new BufferedReader(new FileReader(aliasFile)); - String nextLine; - while ((nextLine = br.readLine()) != null) { - String[] tokens = Globals.whitespacePattern.split(nextLine); - HashSet row = new LinkedHashSet(Arrays.asList(tokens)); - for (String nm : tokens) { - aliasRows.put(nm, row); - } - } - br.close(); - - // Loop through assembly file - int[] chrIndeces = {0, 4, 6, 9}; - br = new BufferedReader(new FileReader(assemblyFile)); - boolean start = false; - List newRows = new ArrayList(); - while ((nextLine = br.readLine()) != null) { - if (start) { - - String[] tokens = Globals.tabPattern.split(nextLine); - boolean foundRow = false; - for (int i : chrIndeces) { - Set row = aliasRows.get(tokens[i]); - if (row != null) { - for (int j : chrIndeces) { - if (!"na".equals(tokens[j])) { - row.add(tokens[j]); - } - } - foundRow = true; - break; - } - } - if (!foundRow) { - String newRow = tokens[chrIndeces[0]]; - for (int i = 1; i < chrIndeces.length; i++) { - String chrNm = tokens[chrIndeces[i]]; - if (!"na".equals(chrNm)) { - newRow += ("\t" + chrNm); - } - } - newRows.add(newRow); - System.out.println("New alias row: " + newRow); - } - - } else if (nextLine.startsWith("# Sequence-Name")) { - start = true; - } - - } - br.close(); - - pw = new PrintWriter(new BufferedWriter(new FileWriter(new File(outputDirectory, aliasFile.getName())))); - Set> output = new HashSet>(); - for (Set row : aliasRows.values()) { - if (row.size() == 0) continue; - if (!output.contains(row)) { - output.add(row); - List chrNames = new ArrayList(row); - pw.print(chrNames.get(0)); - for (int i = 1; i < chrNames.size(); i++) { - pw.print("\t" + chrNames.get(i)); - } - pw.println(); - } - } - for (String row : newRows) { - pw.println(row); - } - pw.close(); - - } - } diff --git a/src/main/java/org/broad/igv/feature/genome/DotGenomeUtils.java b/src/main/java/org/broad/igv/feature/genome/DotGenomeUtils.java new file mode 100644 index 0000000000..b4d4b92f48 --- /dev/null +++ b/src/main/java/org/broad/igv/feature/genome/DotGenomeUtils.java @@ -0,0 +1,89 @@ +package org.broad.igv.feature.genome; + +import org.broad.igv.DirectoryManager; +import org.broad.igv.feature.genome.load.GenomeDescriptor; +import org.broad.igv.feature.genome.load.GenomeLoader; +import org.broad.igv.logging.LogManager; +import org.broad.igv.logging.Logger; +import org.broad.igv.ui.IGV; +import org.broad.igv.ui.util.download.Downloader; +import org.broad.igv.util.HttpUtils; +import org.broad.igv.util.Utilities; + +import java.awt.*; +import java.io.*; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLDecoder; +import java.util.Map; + +/** + * Utilities for the ".genome" format. These files are not created anymore, having been replaced by the genome json + * format, but we need to maintain support for reading and managing them. + */ +public class DotGenomeUtils { + + private static Logger log = LogManager.getLogger(DotGenomeUtils.class); + + + /** + * Returns a File of the provided genomePath. If the genomePath is a URL, it will be downloaded + * and saved in the genome cache directory. + * + * @param genomePath + * @return + * @throws MalformedURLException + * @throws UnsupportedEncodingException + */ + public static File getDotGenomeFile(String genomePath) throws MalformedURLException, UnsupportedEncodingException { + + File archiveFile; + + if (HttpUtils.isRemoteURL(genomePath.toLowerCase())) { + // We need a local copy, as there is no http zip file reader + URL genomeArchiveURL = HttpUtils.createURL(genomePath); + final String tmp = URLDecoder.decode(genomeArchiveURL.getFile(), "UTF-8"); + String cachedFilename = Utilities.getFileNameFromURL(tmp); + if (!DirectoryManager.getGenomeCacheDirectory().exists()) { + DirectoryManager.getGenomeCacheDirectory().mkdir(); + } + archiveFile = new File(DirectoryManager.getGenomeCacheDirectory(), cachedFilename); + Frame parent = IGV.hasInstance() ? IGV.getInstance().getMainFrame() : null; + Downloader.download(genomeArchiveURL, archiveFile, parent); + } else { + archiveFile = new File(genomePath); + } + return archiveFile; + } + + + + public static File getLocalFasta(String id) { + return GenomeLoader.localSequenceMap.get(id); + } + + public static void removeLocalFasta(String id) { + GenomeLoader.localSequenceMap.remove(id); + updateSequenceMapFile(); + } + + + private static void updateSequenceMapFile() { + + PrintWriter pw = null; + + try { + File sequenceFile = new File(DirectoryManager.getGenomeCacheDirectory(), GenomeDescriptor.SEQUENCE_MAP_FILE); + pw = new PrintWriter(new BufferedWriter(new FileWriter(sequenceFile))); + + for (Map.Entry entry : GenomeLoader.localSequenceMap.entrySet()) { + pw.println(entry.getKey() + "\t" + entry.getValue()); + } + } catch (IOException e) { + log.error("Error writing sequence map", e); + } finally { + if (pw != null) pw.close(); + } + } + +} diff --git a/src/main/java/org/broad/igv/feature/genome/Genome.java b/src/main/java/org/broad/igv/feature/genome/Genome.java index 00ee49f90f..64da4e7e70 100644 --- a/src/main/java/org/broad/igv/feature/genome/Genome.java +++ b/src/main/java/org/broad/igv/feature/genome/Genome.java @@ -55,7 +55,6 @@ import org.broad.igv.track.TribbleFeatureSource; import org.broad.igv.ucsc.Hub; import org.broad.igv.ucsc.twobit.TwoBitSequence; -import org.broad.igv.util.ParsingUtils; import org.broad.igv.util.ResourceLocator; import org.broad.igv.util.liftover.Liftover; @@ -73,6 +72,8 @@ public class Genome { private static final int MAX_WHOLE_GENOME_LONG = 100; private static Logger log = LogManager.getLogger(Genome.class); + + GenomeConfig config; private String id; private String displayName; private List chromosomeNames; @@ -95,37 +96,36 @@ public class Genome { private String homeChromosome; private String defaultPos; private String nameSet; - private Hub hub; public Genome(GenomeConfig config) throws IOException { - id = config.id; - displayName = config.name; - nameSet = config.nameSet; - blatDB = config.blatDB; - if (config.ucsdID == null) { + id = config.getId(); + displayName = config.getName(); + nameSet = config.getNameSet(); + blatDB = config.getBlatDB(); + if (config.getUcsdID() == null) { ucscID = ucsdIDMap.containsKey(id) ? ucsdIDMap.get(id) : id; } else { - ucscID = config.ucsdID; + ucscID = config.getUcsdID(); } - blatDB = (config.blatDB != null) ? config.blatDB : ucscID; - defaultPos = config.defaultPos; + blatDB = (config.getBlatDB() != null) ? config.getBlatDB() : ucscID; + defaultPos = config.getDefaultPos(); // Load the sequence object. Some configurations will specify both 2bit and fasta references. The 2 bit // has preference but the fasta index might still be read for chromosome information. Sequence uncachedSequence; - if (config.sequence != null) { + if (config.getSequence() != null) { // Genbank sequences are read directly into memory and referenced by the "sequence" object - uncachedSequence = config.sequence; - } else if (config.twoBitURL != null) { - uncachedSequence = (config.twoBitBptURL != null) ? - new TwoBitSequence(config.twoBitURL, config.twoBitBptURL) : - new TwoBitSequence(config.twoBitURL); - } else if (config.fastaURL != null) { - String fastaPath = config.fastaURL; - String indexPath = config.indexURL; - String gziIndexPath = config.gziIndexURL != null ? config.gziIndexURL : config.compressedIndexURL; // Synonyms + uncachedSequence = config.getSequence(); + } else if (config.getTwoBitURL() != null) { + uncachedSequence = (config.getTwoBitBptURL() != null) ? + new TwoBitSequence(config.getTwoBitURL(), config.getTwoBitBptURL()) : + new TwoBitSequence(config.getTwoBitURL()); + } else if (config.getFastaURL() != null) { + String fastaPath = config.getFastaURL(); + String indexPath = config.getIndexURL(); + String gziIndexPath = config.getGziIndexURL(); // Synonyms uncachedSequence = fastaPath.endsWith(".gz") ? new FastaBlockCompressedSequence(fastaPath, gziIndexPath, indexPath) : new FastaIndexedSequence(fastaPath, indexPath); @@ -138,32 +138,32 @@ public Genome(GenomeConfig config) throws IOException { // lengths are required to support whole genome view. Both can be obtained from fasta index files, but // for .2bit sequences a 'chromSizes" file is required. If not supplied the chr pulldown and wg view are disabled. List chromosomeList = null; - if (config.chromSizesURL != null) { - chromosomeList = ChromSizesParser.parse(config.chromSizesURL); + if (config.getChromSizesURL() != null) { + chromosomeList = ChromSizesParser.parse(config.getChromSizesURL()); } else if (sequence != null && sequence.hasChromosomes()) { chromosomeList = sequence.getChromosomes(); - } else if (config.indexURL != null) { + } else if (config.getIndexURL() != null) { try { // If chromosome info is not otherwise available try to parse the fasta index, if available. This // situation can occur if a twoBitURL is defined but chromSizes is not. - FastaIndex index = new FastaIndex(config.indexURL); + FastaIndex index = new FastaIndex(config.getIndexURL()); chromosomeList = index.getChromosomes(); } catch (IOException e) { log.error("Error loading fasta index", e); } } - // If list of chromosomes is specified use it for the whole genome view, and to prepopulate the - // ordered list of chromosomes. + // If ordered list of chromosome names is specified, use it for the whole genome view, and to prepopulate the + // ordered list of chromosome names. this.chromosomeNames = new ArrayList<>(); Set ordered = new HashSet<>(); - if (config.chromosomeOrder != null) { - this.longChromosomeNames = Arrays.asList(config.chromosomeOrder); + if (config.getChromosomeOrder() != null) { + this.longChromosomeNames = Arrays.asList(config.getChromosomeOrder()); this.chromosomeNames.addAll(this.longChromosomeNames); ordered.addAll(this.longChromosomeNames); } - // If we have chromosome information pre-populate the chromosome cache. + // If we have chromosome length information pre-populate the chromosome cache. this.chromosomeMap = new HashMap<>(); if (chromosomeList != null) { for (Chromosome c : chromosomeList) { @@ -173,50 +173,53 @@ public Genome(GenomeConfig config) throws IOException { } } // If whole genome chromosomes are not explicitly specified try to infer them. - if (this.longChromosomeNames == null && config.wholeGenomeView != false) { + if (this.longChromosomeNames == null && config.isWholeGenomeView() != false) { this.longChromosomeNames = computeLongChromosomeNames(); } + } else { + // No chromosome list. Try to fetch chromosome names from the sequence + if(this.chromosomeNames.isEmpty()) { + this.chromosomeNames = sequence.getChromosomeNames(); + } } // Whole genome view is enabled by default if we have the chromosome information amd the // number of chromosomes is not too large - showWholeGenomeView = config.wholeGenomeView && + showWholeGenomeView = config.isWholeGenomeView() && + chromosomeList != null && chromosomeList.size() > 1 && longChromosomeNames.size() <= MAX_WHOLE_GENOME_LONG; - // Cytobands - if (config.cytobands != null) { - cytobandSource = new CytobandMap(config.cytobands); // Directly supplied, from .genome file - } else if (config.cytobandBbURL != null) { - cytobandSource = new CytobandSourceBB(config.cytobandBbURL, this); - } else if (config.cytobandURL != null) { - cytobandSource = new CytobandMap(config.cytobandURL); + if (config.getCytobands() != null) { + cytobandSource = new CytobandMap(config.getCytobands()); // Directly supplied, from .genome file + } else if (config.getCytobandBbURL() != null) { + cytobandSource = new CytobandSourceBB(config.getCytobandBbURL(), this); + } else if (config.getCytobandURL() != null) { + cytobandSource = new CytobandMap(config.getCytobandURL()); } - // Chromosome aliases - if (config.aliasURL != null) { - chromAliasSource = (new ChromAliasFile(config.aliasURL, chromosomeNames)); - } else if (config.chromAliasBbURL != null) { - chromAliasSource = (new ChromAliasBB(config.chromAliasBbURL, this)); + if (config.getAliasURL() != null) { + chromAliasSource = (new ChromAliasFile(config.getAliasURL(), chromosomeNames)); + } else if (config.getChromAliasBbURL() != null) { + chromAliasSource = (new ChromAliasBB(config.getChromAliasBbURL(), this)); if (chromosomeNames == null || chromosomeNames.size() == 0) { chromosomeNames = Arrays.asList(((ChromAliasBB) chromAliasSource).getChromosomeNames()); } } else { chromAliasSource = (new ChromAliasDefaults(id, chromosomeNames)); } - if (config.chromAliases != null) { - addChrAliases(config.chromAliases); + if (config.getChromAliases() != null) { + addChrAliases(config.getChromAliases()); } - // Set the default position. if (showWholeGenomeView) { homeChromosome = Globals.CHR_ALL; - } else if (config.defaultPos != null) { - int idx = config.defaultPos.indexOf(":"); - homeChromosome = idx > 0 ? config.defaultPos.substring(0, idx) : config.defaultPos; + } else if (config.getDefaultPos() != null) { + int idx = config.getDefaultPos().indexOf(":"); + homeChromosome = idx > 0 ? config.getDefaultPos().substring(0, idx) : config.getDefaultPos(); } else if (this.chromosomeNames != null && this.chromosomeNames.size() > 0) { homeChromosome = this.chromosomeNames.get(0); } else { @@ -258,16 +261,13 @@ private void addTracks(GenomeConfig config) { ArrayList tracks = new ArrayList<>(); ArrayList hiddenTracks = new ArrayList<>(); - List trackConfigs = config.tracks; - if (trackConfigs == null) { - trackConfigs = config.annotations; - } + List trackConfigs = config.getTrackConfigs(); if (trackConfigs != null) { trackConfigs.forEach((TrackConfig trackConfig) -> { ResourceLocator res = ResourceLocator.fromTrackConfig(trackConfig); - Boolean hidden = trackConfig.hidden; // Not to be confused with "visible" + Boolean hidden = trackConfig.getHidden(); // Not to be confused with "visible" if (hidden != null && hidden) { hiddenTracks.add(res); } else { @@ -410,7 +410,6 @@ public String getDefaultPos() { return defaultPos == null ? homeChromosome : defaultPos; } - public Chromosome getChromosome(String name) { String chrName = getCanonicalChrName(name); if (chromosomeMap.containsKey(chrName)) { diff --git a/src/main/java/org/broad/igv/feature/genome/GenomeDownloadUtils.java b/src/main/java/org/broad/igv/feature/genome/GenomeDownloadUtils.java new file mode 100644 index 0000000000..a18d8139f2 --- /dev/null +++ b/src/main/java/org/broad/igv/feature/genome/GenomeDownloadUtils.java @@ -0,0 +1,164 @@ +package org.broad.igv.feature.genome; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.broad.igv.DirectoryManager; +import org.broad.igv.feature.genome.load.GenomeConfig; +import org.broad.igv.feature.genome.load.TrackConfig; +import org.broad.igv.logging.LogManager; +import org.broad.igv.logging.Logger; +import org.broad.igv.ui.IGV; +import org.broad.igv.ui.commandbar.GenomeListManager; +import org.broad.igv.ui.util.download.Downloader; +import org.broad.igv.util.FileUtils; +import org.broad.igv.util.HttpUtils; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; +import java.util.Map; + +/** + * Class of static functions for managing genome downloads + */ +public class GenomeDownloadUtils { + + private static Logger log = LogManager.getLogger(GenomeDownloadUtils.class); + + public static boolean isAnnotationsDownloadable(GenomeListItem item) { + return item.getPath().endsWith(".json"); + } + + public static boolean isSequenceDownloadable(GenomeListItem item) { + if (item.getPath().endsWith(".json")) { + try { + String jsonString = HttpUtils.getInstance().getContentsAsJSON(new URL(item.getPath())); + return jsonString.contains("twoBitURL"); + } catch (IOException e) { + log.error("Error fetching genome json " + item.getPath()); + } + } + return false; + } + + public static File downloadGenome(GenomeConfig c, boolean downloadSequence, boolean downloadAnnotations) throws IOException { + + GenomeConfig config = c.copy(); + + // Create a directory for the data files (sequence and annotations) + final File genomeDirectory = DirectoryManager.getGenomeCacheDirectory(); + File dataDirectory = new File(genomeDirectory, config.getId()); + if (dataDirectory.exists()) { + if (!dataDirectory.isDirectory()) { + throw new RuntimeException("Error downloading genome. " + dataDirectory.getAbsolutePath() + " exists and is not a directory."); + } + } else { + if (!dataDirectory.mkdir()) { + throw new RuntimeException("Error downloading genome. Could not create directory: " + dataDirectory.getAbsolutePath()); + } + } + + String relativeDataDirectory = config.getId() + "/"; + + if (config.getTwoBitURL() != null) { + + URL url = new URL(config.getTwoBitURL()); + File localFile; + if (downloadSequence) { + localFile = download(url, dataDirectory); + } else { + localFile = constructLocalFile(url, dataDirectory); // It might be there from previous downloads + } + if (localFile.exists()) { + config.setTwoBitURL(relativeDataDirectory + localFile.getName()); + // Null out urls not needed for .2bit sequences. + config.setFastaURL(null); + config.setIndexURL(null); + config.setGziIndexURL(null); + config.setTwoBitBptURL(null); // Not needed for local .2bit files + } + } else if (config.getFastaURL() != null) { + + String[] fastaFields = {"fastaURL", "indexURL", "gziIndexURL", "compressedIndexURL"}; + downloadAndUpdateConfig(downloadSequence, fastaFields, config, dataDirectory, relativeDataDirectory); + + } else { + throw new RuntimeException("Sequence for genome " + config.getName() + " is not downloadable."); + } + + String[] annotationFields = {"chromAliasBbURL", "cytobandURL", "cytobandBbURL", "aliasURL", "chromSizesURL"}; + downloadAndUpdateConfig(downloadAnnotations, annotationFields, config, dataDirectory, relativeDataDirectory); + + List trackConfigs = config.getTrackConfigs(); + if (trackConfigs != null) { + String[] trackFields = {"url", "indexURL", "trixURL"}; + for (TrackConfig trackConfig : trackConfigs) { + downloadAndUpdateConfig(downloadAnnotations, trackFields, trackConfig, dataDirectory, relativeDataDirectory); + } + + } + + File localGenomeFile = new File(genomeDirectory, config.getId() + ".json"); + saveLocalGenome(config, localGenomeFile); + return localGenomeFile; + + } + + private static void downloadAndUpdateConfig(boolean downloadData, String[] fields, Object config, File dataDirectory, String relativeDataDirectory) { + URL url; + File localFile; + for (String f : fields) { + try { + Field field = config.getClass().getDeclaredField(f); + field.setAccessible(true); + Object urlField = field.get(config); + if (urlField != null) { + url = new URL(urlField.toString()); + if (downloadData) { + localFile = download(url, dataDirectory); + } else { + localFile = constructLocalFile(url, dataDirectory); // It might be there from previous downloads + } + if (localFile.exists()) { + field.set(config, relativeDataDirectory + localFile.getName()); + } + } + } catch (Exception e) { + log.error(e); + } + } + } + + private static void saveLocalGenome(GenomeConfig genomeConfig, File localFile) throws IOException { + log.info("Saving " + localFile.getAbsolutePath()); + try (BufferedWriter writer = new BufferedWriter(new FileWriter(localFile))) { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + gson.toJson(genomeConfig, writer); + } + } + + + public static File constructLocalFile(URL url, File directory) { + String path = url.getPath(); + int idx = path.lastIndexOf('/'); + String filename = idx < 0 ? path : path.substring(idx + 1); + return new File(directory, filename); + } + + private static File download(URL url, File directory) throws MalformedURLException { + + File localFile = constructLocalFile(url, directory); + + boolean success = Downloader.download(url, localFile, IGV.getInstance().getMainFrame()); + if (!success) { + throw new RuntimeException("Download canceled"); + } + return localFile; + } + +} diff --git a/src/main/java/org/broad/igv/feature/genome/GenomeListItem.java b/src/main/java/org/broad/igv/feature/genome/GenomeListItem.java index fd3c3d8a3d..22da778524 100644 --- a/src/main/java/org/broad/igv/feature/genome/GenomeListItem.java +++ b/src/main/java/org/broad/igv/feature/genome/GenomeListItem.java @@ -86,18 +86,12 @@ public String toString() { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - - GenomeListItem that = (GenomeListItem) o; - - if (id != null ? !id.equals(that.id) : that.id != null) return false; - if (path != null ? !path.equals(that.path) : that.path != null) return false; - - return true; + GenomeListItem item = (GenomeListItem) o; + return Objects.equals(displayableName, item.displayableName) && Objects.equals(path, item.path) && Objects.equals(id, item.id); } @Override public int hashCode() { - return Objects.hash(path, id); + return Objects.hash(displayableName, path, id); } - } diff --git a/src/main/java/org/broad/igv/feature/genome/GenomeManager.java b/src/main/java/org/broad/igv/feature/genome/GenomeManager.java index 2654131572..abc2ea6f67 100644 --- a/src/main/java/org/broad/igv/feature/genome/GenomeManager.java +++ b/src/main/java/org/broad/igv/feature/genome/GenomeManager.java @@ -37,14 +37,10 @@ import org.broad.igv.DirectoryManager; import org.broad.igv.Globals; import org.broad.igv.event.GenomeChangeEvent; -import org.broad.igv.event.GenomeResetEvent; import org.broad.igv.event.IGVEventBus; import org.broad.igv.exceptions.DataLoadException; import org.broad.igv.feature.FeatureDB; -import org.broad.igv.feature.genome.load.ChromAliasParser; -import org.broad.igv.feature.genome.load.GenomeDescriptor; -import org.broad.igv.feature.genome.load.GenomeLoader; -import org.broad.igv.feature.genome.load.JsonGenomeLoader; +import org.broad.igv.feature.genome.load.*; import org.broad.igv.jbrowse.CircularViewUtilities; import org.broad.igv.logging.LogManager; import org.broad.igv.logging.Logger; @@ -58,20 +54,15 @@ import org.broad.igv.ui.commandbar.GenomeListManager; import org.broad.igv.ui.panel.FrameManager; import org.broad.igv.ui.util.*; -import org.broad.igv.ui.util.download.Downloader; -import org.broad.igv.util.FileUtils; -import org.broad.igv.util.HttpUtils; import org.broad.igv.util.ResourceLocator; -import org.broad.igv.util.Utilities; -import java.awt.*; import java.io.*; -import java.net.MalformedURLException; import java.net.SocketException; -import java.net.URL; -import java.net.URLDecoder; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.*; import java.util.List; +import java.util.stream.Stream; import static org.broad.igv.prefs.Constants.SHOW_SINGLE_TRACK_PANE_KEY; @@ -103,42 +94,23 @@ public synchronized static GenomeManager getInstance() { private GenomeManager() { genomeListManager = GenomeListManager.getInstance(); - GenomeLoader.localSequenceMap = GenomeLoader.loadSequenceMap(); + GenomeLoader.loadSequenceMap(); + } + + + public String getGenomeId() { + return currentGenome == null ? null : currentGenome.getId(); } /** - * Returns a File of the provided genomePath. If the genomePath is a URL, it will be downloaded - * and saved in the genome cache directory. + * IGV always has exactly 1 genome loaded at a time. + * This returns the currently loaded genome * - * @param genomePath * @return - * @throws MalformedURLException - * @throws UnsupportedEncodingException + * @api */ - public static File getGenomeFile(String genomePath) throws MalformedURLException, UnsupportedEncodingException { - - File archiveFile; - - if (HttpUtils.isRemoteURL(genomePath.toLowerCase())) { - // We need a local copy, as there is no http zip file reader - URL genomeArchiveURL = HttpUtils.createURL(genomePath); - final String tmp = URLDecoder.decode(genomeArchiveURL.getFile(), "UTF-8"); - String cachedFilename = Utilities.getFileNameFromURL(tmp); - if (!DirectoryManager.getGenomeCacheDirectory().exists()) { - DirectoryManager.getGenomeCacheDirectory().mkdir(); - } - archiveFile = new File(DirectoryManager.getGenomeCacheDirectory(), cachedFilename); - Frame parent = IGV.hasInstance() ? IGV.getInstance().getMainFrame() : null; - Downloader.download(genomeArchiveURL, archiveFile, parent); - } else { - archiveFile = new File(genomePath); - } - return archiveFile; - } - - // Setter provided for unit tests - public void setCurrentGenomeForTest(Genome genome) { - this.currentGenome = genome; + public Genome getCurrentGenome() { + return currentGenome; } /** @@ -215,11 +187,9 @@ public Genome loadGenome(String genomePath) throws IOException { IGV.getInstance().resetSession(null); } - // If the genome is loaded from anywhere other than the cache directory add an entry to the pulldown - if(newGenome != null && !(new File(genomePath).getParentFile().equals(genomeCacheDirectory))) { - GenomeListItem genomeListItem = new GenomeListItem(newGenome.getDisplayName(), genomePath, newGenome.getId()); - GenomeListManager.getInstance().addGenomeItem(genomeListItem, true); - } + // Add an entry to the pulldown + GenomeListItem genomeListItem = new GenomeListItem(newGenome.getDisplayName(), genomePath, newGenome.getId()); + GenomeListManager.getInstance().addGenomeItem(genomeListItem); setCurrentGenome(newGenome); @@ -243,11 +213,13 @@ public void setCurrentGenome(Genome newGenome) { if (IGV.hasInstance()) { IGV.getInstance().goToLocus(newGenome.getHomeChromosome()); // newGenome.getDefaultPos()); FrameManager.getDefaultFrame().setChromosomeName(newGenome.getHomeChromosome(), true); - loadGenomeAnnotations(newGenome); + + restoreGenomeTracks(newGenome); + IGV.getInstance().resetFrames(); IGV.getInstance().getSession().clearHistory(); - if(newGenome != Genome.nullGenome()) { + if (newGenome != Genome.nullGenome()) { // This should only occur on startup failure PreferencesManager.getPreferences().setLastGenome(newGenome.getId()); } @@ -261,19 +233,6 @@ public void setCurrentGenome(Genome newGenome) { } /** - * Load and initialize the track objects from the genome's track resource locators. Does not add the tracks - * to the IGV instance. - * - * @param genome - */ - public void loadGenomeAnnotations(Genome genome) { - restoreGenomeTracks(genome); - IGV.getInstance().repaint(); - } - - /** - * Add a genomes tracks to the IGV instance. - * * @param genome */ public void restoreGenomeTracks(Genome genome) { @@ -329,149 +288,82 @@ public void restoreGenomeTracks(Genome genome) { } - /** - * Delete .genome files from the cache directory - */ - public void clearGenomeCache() { - File[] files = DirectoryManager.getGenomeCacheDirectory().listFiles(); - for (File file : files) { - if (file.getName().toLowerCase().endsWith(Globals.GENOME_FILE_EXTENSION)) { - file.delete(); - } - } - } - - public String getGenomeId() { - return currentGenome == null ? null : currentGenome.getId(); - } + public File downloadGenome(GenomeListItem item, boolean downloadSequence, boolean downloadAnnotations) { - /** - * IGV always has exactly 1 genome loaded at a time. - * This returns the currently loaded genome - * - * @return - * @api - */ - public Genome getCurrentGenome() { - return currentGenome; - } - - - public boolean downloadGenome(GenomeListItem item, boolean downloadDataFiles) { - - boolean success; try { - File genomeFile = getGenomeFile(item.getPath()); // Has side affect of downloading .genome file - - if (downloadDataFiles) { - - - if (item.getPath().endsWith(".genome")) { - GenomeDescriptor genomeDescriptor = GenomeDescriptor.parseGenomeArchiveFile(genomeFile); - String fastaPath = genomeDescriptor.getSequencePath(); - if (fastaPath != null && FileUtils.isRemote(fastaPath)) { - File localFile = downloadFasta(fastaPath); - if (localFile != null) { - addLocalFasta(item.getId(), localFile); - } - } - - } else if (item.getPath().endsWith(".json")) { - - JsonGenomeLoader.GenomeDescriptor desc = (new JsonGenomeLoader(item.getPath())).loadDescriptor(); - - - } + if (item.getPath().endsWith(".genome")) { + File genomeFile = DotGenomeUtils.getDotGenomeFile(item.getPath()); // Will be downloaded if remote -- neccessary to unzip + return genomeFile; + } else { + JsonGenomeLoader loader = new JsonGenomeLoader(item.getPath()); + GenomeConfig config = loader.loadGenomeConfig(); + File downloadedGenome = GenomeDownloadUtils.downloadGenome(config, downloadSequence, downloadAnnotations); + return downloadedGenome; } - success = true; - } catch (Exception e) { - success = false; - MessageUtils.showErrorMessage("Error downloading genome", e); + MessageUtils.showMessage("Error downloading genome: " + e.getMessage()); log.error("Error downloading genome " + item.getDisplayableName()); + return null; } - - - if (success) { - genomeListManager.addGenomeItem(item, false); - IGVEventBus.getInstance().post(new GenomeResetEvent()); - } - - return success; } /** - * Download a fasta file and associated index files. + * Delete the specified genome files * - * @throws IOException + * @param removedValuesList */ - File downloadFasta(String fastaPath) throws IOException { - - File defaultDir = DirectoryManager.getFastaCacheDirectory(); - File targetDir = defaultDir; - //File targetDir = FileDialogUtils.chooseDirectory("Select directory for sequence", defaultDir); - if (targetDir == null) { - targetDir = defaultDir; - } - - String filename = Utilities.getFileNameFromURL(fastaPath); + public void deleteDownloadedGenomes(List removedValuesList) throws IOException { - File localFile = new File(targetDir, filename); - boolean downloaded = Downloader.download(HttpUtils.createURL(fastaPath), localFile, IGV.getInstance().getMainFrame()); + for (GenomeListItem item : removedValuesList) { - if (downloaded) { - URL indexUrl = HttpUtils.createURL(fastaPath + ".fai"); - File localIndexFile = new File(targetDir, filename + ".fai"); - downloaded = Downloader.download(indexUrl, localIndexFile, IGV.getInstance().getMainFrame()); - } + String loc = item.getPath(); + File genomeFile = new File(loc); - if (downloaded) { + if (genomeFile.exists() && DirectoryManager.isChildOf(DirectoryManager.getGenomeCacheDirectory(), genomeFile)) { - if (fastaPath.endsWith(".gz")) { - URL gziUrl = HttpUtils.createURL(fastaPath + ".gzi"); - File localGziPath = new File(targetDir, filename + ".gzi"); - downloaded = Downloader.download(gziUrl, localGziPath, IGV.getInstance().getMainFrame()); - } - } + genomeFile.delete(); - return downloaded ? localFile : null; - } + // Delete associated data files + File dataFileDirectory = new File(DirectoryManager.getGenomeCacheDirectory(), item.getId()); + File localFasta = DotGenomeUtils.getLocalFasta(item.getId()); // (Legacy .genome convention) + if ((dataFileDirectory.isDirectory() || localFasta != null) && + MessageUtils.confirm("Delete downloaded data files?")) { - public static File getLocalFasta(String id) { - return GenomeLoader.localSequenceMap.get(id); - } + if (dataFileDirectory.isDirectory()) { + try (Stream paths = Files.walk(dataFileDirectory.toPath())) { + paths.sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } + dataFileDirectory.delete(); + } - public static void removeLocalFasta(String id) { - GenomeLoader.localSequenceMap.remove(id); - updateSequenceMapFile(); + if (localFasta != null) { + // If fasta file is in the "igv/genomes" directory delete it + DotGenomeUtils.removeLocalFasta(item.getId()); + if (DirectoryManager.isChildOf(DirectoryManager.getGenomeCacheDirectory(), localFasta)) { + if (MessageUtils.confirm("Delete fasta file: " + localFasta.getAbsolutePath() + "?")) { + localFasta.delete(); + File indexFile = new File(localFasta.getAbsolutePath() + ".fai"); + if (indexFile.exists()) { + indexFile.delete(); + } + } + } + } + } + } + } } - private static void addLocalFasta(String id, File localFile) { - GenomeLoader.localSequenceMap.put(id, localFile); - updateSequenceMapFile(); + // Setter provided for unit tests + public void setCurrentGenomeForTest(Genome genome) { + this.currentGenome = genome; } - private static void updateSequenceMapFile() { - - PrintWriter pw = null; - - try { - File sequenceFile = new File(DirectoryManager.getGenomeCacheDirectory(), GenomeDescriptor.SEQUENCE_MAP_FILE); - pw = new PrintWriter(new BufferedWriter(new FileWriter(sequenceFile))); - - for (Map.Entry entry : GenomeLoader.localSequenceMap.entrySet()) { - pw.println(entry.getKey() + "\t" + entry.getValue()); - } - } catch (IOException e) { - log.error("Error writing sequence map", e); - } finally { - if (pw != null) pw.close(); - } - } - } diff --git a/src/main/java/org/broad/igv/feature/genome/fasta/FastaIndex.java b/src/main/java/org/broad/igv/feature/genome/fasta/FastaIndex.java index b36e6e5240..9ef5b9564b 100644 --- a/src/main/java/org/broad/igv/feature/genome/fasta/FastaIndex.java +++ b/src/main/java/org/broad/igv/feature/genome/fasta/FastaIndex.java @@ -108,6 +108,9 @@ private void parseIndexFile(String indexFile) throws IOException { // Build sequence structure add(new FastaSequenceIndexEntry(contig, location, size, basesPerLine, bytesPerLine)); } + if(sequenceEntries.size() == 0) { + throw new RuntimeException("No sequences found in " + indexFile); + } } finally { if (reader != null) { reader.close(); diff --git a/src/main/java/org/broad/igv/feature/genome/load/ChromsizesLoader.java b/src/main/java/org/broad/igv/feature/genome/load/ChromsizesLoader.java index 445e555656..6f0a473444 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/ChromsizesLoader.java +++ b/src/main/java/org/broad/igv/feature/genome/load/ChromsizesLoader.java @@ -15,15 +15,14 @@ public ChromsizesLoader(String genomePath) { } /** - * Define a minimal genome from a chrom.sizes file. It is assumed (required) that the file follow the - * UCSC naming convention => [id].chrom.sizes + * Define a minimal genome from a chrom.sizes file. + * * @return * @throws IOException */ @Override public Genome loadGenome() throws IOException { - int firstPeriodIdx = genomePath.indexOf('.'); - String genomeId = genomePath.substring(0, firstPeriodIdx); + String genomeId = genomePath; List chromosomes = ChromSizesParser.parse(genomePath); Genome newGenome = new Genome(genomeId, chromosomes); return newGenome; diff --git a/src/main/java/org/broad/igv/feature/genome/load/DotGenomeLoader.java b/src/main/java/org/broad/igv/feature/genome/load/DotGenomeLoader.java index d764ba5a63..bc1c7abb96 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/DotGenomeLoader.java +++ b/src/main/java/org/broad/igv/feature/genome/load/DotGenomeLoader.java @@ -3,9 +3,6 @@ import htsjdk.tribble.Feature; import org.broad.igv.feature.*; import org.broad.igv.feature.genome.Genome; -import org.broad.igv.feature.genome.Sequence; -import org.broad.igv.feature.genome.fasta.FastaBlockCompressedSequence; -import org.broad.igv.feature.genome.fasta.FastaIndexedSequence; import org.broad.igv.feature.gff.GFFFeatureSource; import org.broad.igv.logging.LogManager; import org.broad.igv.logging.Logger; @@ -80,8 +77,8 @@ private static FeatureTrack createGeneTrack(Genome genome, BufferedReader reader } /** - * Create a genome from a ".genome" file. In addition to the reference sequence .genome files can optionally - * specify cytobands and annotations. + * Create a genome from a ".genome" file. A ".genome" file is simply a zip archive. In addition to a URL to the + * reference fasta .genome files can optionally contain cytobands and annotations. */ @Override @@ -92,6 +89,8 @@ public Genome loadGenome() throws IOException { GenomeDescriptor genomeDescriptor = GenomeDescriptor.parseGenomeArchiveFile(archiveFile); final String id = genomeDescriptor.getId(); final String displayName = genomeDescriptor.getName(); + + // Legacy (versions < 2.19) means to override a remote fasta URL with a local file String sequencePath = localSequenceMap.containsKey(genomeDescriptor.getId()) ? loadSequenceMap().get(genomeDescriptor.getId()).getAbsolutePath() : genomeDescriptor.getSequencePath(); @@ -99,15 +98,15 @@ public Genome loadGenome() throws IOException { if (sequencePath == null) { // TODO -- is this an error? } else { - config.fastaURL = sequencePath; - config.indexURL = sequencePath + ".fai"; + config.setFastaURL(sequencePath); + config.setIndexURL(sequencePath + ".fai"); if (sequencePath.endsWith(".gz")) { - config.gziIndexURL = sequencePath + ".gzi"; + config.setGziIndexURL(sequencePath + ".gzi"); } } - config.id = id; - config.name = displayName; + config.setId(id); + config.setName(displayName); if (genomeDescriptor.hasCytobands()) { @@ -115,7 +114,7 @@ public Genome loadGenome() throws IOException { try { cytobandStream = genomeDescriptor.getCytoBandStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(cytobandStream)); - config.cytobands = CytoBandFileParser.loadData(reader); + config.setCytobands(CytoBandFileParser.loadData(reader)); } catch (IOException ex) { log.error("Error loading cytoband file", ex); @@ -130,7 +129,7 @@ public Genome loadGenome() throws IOException { aliasStream = genomeDescriptor.getChrAliasStream(); if (aliasStream != null) { BufferedReader reader = new BufferedReader(new InputStreamReader(aliasStream)); - config.chromAliases = ChromAliasParser.loadChrAliases (reader); + config.setChromAliases(ChromAliasParser.loadChrAliases (reader)); } } catch (IOException e) { // We don't want to bomb if the alias load fails. Just log it and proceed. diff --git a/src/main/java/org/broad/igv/feature/genome/load/FastaGenomeLoader.java b/src/main/java/org/broad/igv/feature/genome/load/FastaGenomeLoader.java index 4d5c00c950..2664c2c079 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/FastaGenomeLoader.java +++ b/src/main/java/org/broad/igv/feature/genome/load/FastaGenomeLoader.java @@ -9,6 +9,9 @@ import java.io.File; import java.io.IOException; +/** + * Loader for a minimal genome of only reference sequence, which can be defined by a fasta or 2bit file. + */ public class FastaGenomeLoader extends GenomeLoader { private String genomePath; @@ -21,42 +24,44 @@ public FastaGenomeLoader(String genomePath) { @Override public Genome loadGenome() throws IOException { - String fastaPath = null; - String fastaIndexPath = null; - if (genomePath.endsWith(".fai")) { - fastaPath = genomePath.substring(0, genomePath.length() - 4); - fastaIndexPath = genomePath; - } else { - fastaPath = genomePath; - fastaIndexPath = genomePath + ".fai"; - } - - if (!FileUtils.resourceExists(fastaIndexPath)) { - //Have to make sure we have a local copy of the fasta file - //to index it - if (!FileUtils.isRemote(fastaPath)) { - fastaIndexPath = fastaPath + ".fai"; - FastaUtils.createIndexFile(fastaPath, fastaIndexPath); - } - } - - String id = fastaPath; + GenomeConfig config = new GenomeConfig(); + String id = genomePath; String name; - if (HttpUtils.isRemoteURL(fastaPath)) { - name = Utilities.getFileNameFromURL(fastaPath); + if (HttpUtils.isRemoteURL(genomePath)) { + name = Utilities.getFileNameFromURL(genomePath); } else { - File file = new File(fastaPath); + File file = new File(genomePath); if (!file.exists()) { - throw new IOException(fastaPath + " does not exist, could not load genome"); + throw new IOException(genomePath + " does not exist, could not load genome"); } name = file.getName(); } - GenomeConfig config = new GenomeConfig(); - config.id = id; - config.name = name; - config.fastaURL = fastaPath; - config.indexURL = fastaIndexPath; + + config.setId(id); + config.setName(name); + + if (genomePath.endsWith(".2bit")) { + config.setTwoBitURL(genomePath); + + } else { + String fastaPath = null; + String fastaIndexPath; + if (genomePath.endsWith(".fai")) { + fastaPath = genomePath.substring(0, genomePath.length() - 4); + fastaIndexPath = genomePath; + } else { + fastaIndexPath = genomePath + ".fai"; + } + + // If fasta is a local file create index, if needed + if (!FileUtils.isRemote(fastaPath) && !FileUtils.resourceExists(fastaIndexPath)) { + FastaUtils.createIndexFile(fastaPath, fastaIndexPath); + } + + config.setFastaURL(fastaPath); + config.setIndexURL(fastaIndexPath); + } return new Genome(config); } diff --git a/src/main/java/org/broad/igv/feature/genome/load/GenbankLoader.java b/src/main/java/org/broad/igv/feature/genome/load/GenbankLoader.java index 70e724685b..2176943582 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/GenbankLoader.java +++ b/src/main/java/org/broad/igv/feature/genome/load/GenbankLoader.java @@ -37,9 +37,9 @@ public Genome loadGenome() throws IOException { Sequence sequence = new InMemorySequence(chr, seq); GenomeConfig config = new GenomeConfig(); - config.id = chr; - config.name = name; - config.sequence = sequence; + config.setId(chr); + config.setName(name); + config.setSequence(sequence); String[] aliases = genbankParser.getAliases(); if (aliases != null) { @@ -48,7 +48,7 @@ public Genome loadGenome() throws IOException { for (String a : aliases) { aliasList.add(a); } - config.chromAliases = (Arrays.asList(aliasList)); + config.setChromAliases((Arrays.asList(aliasList))); } diff --git a/src/main/java/org/broad/igv/feature/genome/load/GenomeConfig.java b/src/main/java/org/broad/igv/feature/genome/load/GenomeConfig.java index 4a5678a9df..d698d1ffaa 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/GenomeConfig.java +++ b/src/main/java/org/broad/igv/feature/genome/load/GenomeConfig.java @@ -3,59 +3,308 @@ import com.google.gson.Gson; import org.broad.igv.feature.Cytoband; import org.broad.igv.feature.genome.Sequence; +import org.broad.igv.logging.LogManager; +import org.broad.igv.logging.Logger; -import java.util.LinkedHashMap; -import java.util.List; +import java.util.*; +import java.util.stream.Collectors; /** * A static json-like object representing a genome configuration. Emulates the javascript equivalent. * This class was created to ease port of code from javascript. + * + * NOTE: The property names match names in the corresponding genome json files. They cannot be renamed. *

- * Objects of this class are created by two paths: (1) GSON deserializtion of a genome json file, and (2) - * parsing track hub files. + * Objects of this class are created by two paths: + * (1) GSON deserializtion of a genome json file, and + * (2) parsing track hub files. */ -public class GenomeConfig { - - public String id; - public String name; - public String fastaURL; - public String indexURL; - public String gziIndexURL; - public String compressedIndexURL; - public String twoBitURL; - public String nameSet; - public boolean wholeGenomeView = true; - public String defaultPos; - public String description; - public String blat; - public String chromAliasBbURL; - public String twoBitBptURL; - public String infoURL; - public String cytobandURL; - public String cytobandBbURL; - - public Boolean ordered; - public String blatDB; - public String ucsdID; - public String aliasURL; - public String[] chromosomeOrder; - public String chains; - - public String chromSizesURL; - - public List tracks; - - public List annotations; // Backward compatibility, synonym for tracks +public class GenomeConfig implements Cloneable { + + private static Logger log = LogManager.getLogger(GenomeConfig.class); + + private String id; + private String name; + private String fastaURL; + private String indexURL; + private String gziIndexURL; + private String compressedIndexURL; // Used by reflection from GSON. Do not remove + private String twoBitURL; + private String twoBitBptURL; + private String nameSet; + private boolean wholeGenomeView = true; + private String defaultPos; + private String description; + private String blat; + private String chromAliasBbURL; + + private String infoURL; + private String cytobandURL; + private String cytobandBbURL; + + private Boolean ordered; + private String blatDB; + private String ucsdID; + private String aliasURL; + private String[] chromosomeOrder; + private String chains; + + private String chromSizesURL; + + private List tracks; + + private List annotations; // Backward compatibility, synonym for tracks // The properties below support the legacy ".genome" file, which directly loads resources from a zip archive. - public Sequence sequence; - public LinkedHashMap> cytobands; - public List> chromAliases; + private Sequence sequence; + private LinkedHashMap> cytobands; + private List> chromAliases; public static GenomeConfig fromJson(String json) { return (new Gson()).fromJson(json, GenomeConfig.class); + } + + public GenomeConfig() { + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getFastaURL() { + return fastaURL; + } + + public void setFastaURL(String fastaURL) { + this.fastaURL = fastaURL; + } + + public String getIndexURL() { + return indexURL; + } + + public void setIndexURL(String indexURL) { + this.indexURL = indexURL; + } + + public String getGziIndexURL() { + return this.gziIndexURL != null ? gziIndexURL : compressedIndexURL; + } + + public void setGziIndexURL(String gziIndexURL) { + this.gziIndexURL = gziIndexURL; + } + + public String getTwoBitURL() { + return twoBitURL; + } + + public void setTwoBitURL(String twoBitURL) { + this.twoBitURL = twoBitURL; + } + + public String getNameSet() { + return nameSet; + } + + public void setNameSet(String nameSet) { + this.nameSet = nameSet; + } + + public boolean isWholeGenomeView() { + return wholeGenomeView; + } + + public void setWholeGenomeView(boolean wholeGenomeView) { + this.wholeGenomeView = wholeGenomeView; + } + + public String getDefaultPos() { + return defaultPos; + } + + public void setDefaultPos(String defaultPos) { + this.defaultPos = defaultPos; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getBlat() { + return blat; + } + + public void setBlat(String blat) { + this.blat = blat; + } + + public String getChromAliasBbURL() { + return chromAliasBbURL; + } + + public void setChromAliasBbURL(String chromAliasBbURL) { + this.chromAliasBbURL = chromAliasBbURL; + } + + public String getTwoBitBptURL() { + return twoBitBptURL; + } + + public void setTwoBitBptURL(String twoBitBptURL) { + this.twoBitBptURL = twoBitBptURL; + } + public String getInfoURL() { + return infoURL; } + + public void setInfoURL(String infoURL) { + this.infoURL = infoURL; + } + + public String getCytobandURL() { + return cytobandURL; + } + + public void setCytobandURL(String cytobandURL) { + this.cytobandURL = cytobandURL; + } + + public String getCytobandBbURL() { + return cytobandBbURL; + } + + public void setCytobandBbURL(String cytobandBbURL) { + this.cytobandBbURL = cytobandBbURL; + } + + public Boolean getOrdered() { + return ordered; + } + + public void setOrdered(Boolean ordered) { + this.ordered = ordered; + } + + public String getBlatDB() { + return blatDB; + } + + public void setBlatDB(String blatDB) { + this.blatDB = blatDB; + } + + public String getUcsdID() { + return ucsdID; + } + + public void setUcsdID(String ucsdID) { + this.ucsdID = ucsdID; + } + + public String getAliasURL() { + return aliasURL; + } + + public void setAliasURL(String aliasURL) { + this.aliasURL = aliasURL; + } + + public String[] getChromosomeOrder() { + return chromosomeOrder; + } + + public void setChromosomeOrder(String[] chromosomeOrder) { + this.chromosomeOrder = chromosomeOrder; + } + + public String getChains() { + return chains; + } + + public void setChains(String chains) { + this.chains = chains; + } + + public String getChromSizesURL() { + return chromSizesURL; + } + + public void setChromSizesURL(String chromSizesURL) { + this.chromSizesURL = chromSizesURL; + } + + public List getTrackConfigs() { + return tracks != null ? tracks : annotations; + } + + public void setTracks(List tracks) { + this.tracks = tracks; + } + + public Sequence getSequence() { + return sequence; + } + + public void setSequence(Sequence sequence) { + this.sequence = sequence; + } + + public LinkedHashMap> getCytobands() { + return cytobands; + } + + public void setCytobands(LinkedHashMap> cytobands) { + this.cytobands = cytobands; + } + + public List> getChromAliases() { + return chromAliases; + } + + public void setChromAliases(List> chromAliases) { + this.chromAliases = chromAliases; + } + + public GenomeConfig copy() { + return this.clone(); + } + + protected GenomeConfig clone() { + try { + GenomeConfig clone = (GenomeConfig) super.clone(); + + if (this.tracks != null) { + clone.tracks = new ArrayList<>(); + for (TrackConfig trackConfig : this.tracks) { + clone.tracks.add(trackConfig.clone()); + } + } + + return clone; + } catch (CloneNotSupportedException e) { + log.error("Cloning not supported", e); + return this; + } + } + } diff --git a/src/main/java/org/broad/igv/feature/genome/load/GenomeLoader.java b/src/main/java/org/broad/igv/feature/genome/load/GenomeLoader.java index 5cf7aae6d5..791a1298ec 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/GenomeLoader.java +++ b/src/main/java/org/broad/igv/feature/genome/load/GenomeLoader.java @@ -3,9 +3,9 @@ import org.broad.igv.DirectoryManager; import org.broad.igv.Globals; import org.broad.igv.feature.FeatureDB; +import org.broad.igv.feature.genome.DotGenomeUtils; import org.broad.igv.feature.genome.Genome; import org.broad.igv.feature.genome.GenomeException; -import org.broad.igv.feature.genome.GenomeManager; import org.broad.igv.logging.LogManager; import org.broad.igv.logging.Logger; import org.broad.igv.track.FeatureCollectionSource; @@ -31,14 +31,13 @@ */ abstract public class GenomeLoader { - public static final long ONE_WEEK = 7 * 24 * 60 * 60 * 1000; private static Logger log = LogManager.getLogger(GenomeLoader.class); public static Map localSequenceMap; public static GenomeLoader getLoader(String genomePath) throws IOException { if (genomePath.endsWith(".genome")) { - File archiveFile = GenomeManager.getGenomeFile(genomePath); + File archiveFile = DotGenomeUtils.getDotGenomeFile(genomePath); if (!archiveFile.exists()) { MessageUtils.showMessage("File not found: " + archiveFile.getAbsolutePath()); return null; // Happens if genome download was canceled. @@ -50,14 +49,6 @@ public static GenomeLoader getLoader(String genomePath) throws IOException { return new ChromsizesLoader(genomePath); } else if (genomePath.endsWith(".json")) { return new JsonGenomeLoader(genomePath); - } else if (genomePath.endsWith(".2bit")) { - GenomeConfig config = new GenomeConfig(); - config.twoBitURL = genomePath; - config.id = genomePath; - config.name = (HttpUtils.isRemoteURL(genomePath)) ? - Utilities.getFileNameFromURL(genomePath) : - (new File(genomePath)).getName(); - return new GenomeObjectLoader(config); } else if (genomePath.endsWith("hub.txt")) { return new HubGenomeLoader(genomePath); } else { @@ -66,7 +57,7 @@ public static GenomeLoader getLoader(String genomePath) throws IOException { String gziPath = genomePath + ".gzi"; String faiPath = genomePath + ".fai"; if (!(FileUtils.resourceExists(gziPath) && FileUtils.resourceExists(faiPath))) { - throw new GenomeException("IGV cannot readed gzipped fasta files."); + throw new GenomeException("IGV cannot read gzipped fasta files."); } } return new FastaGenomeLoader(genomePath); diff --git a/src/main/java/org/broad/igv/feature/genome/load/GenomeObjectLoader.java b/src/main/java/org/broad/igv/feature/genome/load/GenomeObjectLoader.java deleted file mode 100644 index 40c7e8ccba..0000000000 --- a/src/main/java/org/broad/igv/feature/genome/load/GenomeObjectLoader.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.broad.igv.feature.genome.load; - -import org.broad.igv.feature.genome.Genome; -import org.broad.igv.logging.LogManager; -import org.broad.igv.logging.Logger; - -import java.io.IOException; - -public class GenomeObjectLoader extends GenomeLoader { - - private static Logger log = LogManager.getLogger(GenomeObjectLoader.class); - - private GenomeConfig genomeConfig; - - public GenomeObjectLoader(GenomeConfig genomeConfig) throws IOException { - this.genomeConfig = genomeConfig; - } - - @Override - public Genome loadGenome() throws IOException { - return new Genome(genomeConfig); - } - -} diff --git a/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java b/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java index 37d5382406..467d63af30 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java +++ b/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java @@ -10,8 +10,10 @@ import java.io.IOException; import java.util.*; -import java.util.stream.Collectors; +/** + * Loads a "genome" from a UCSC track hub + */ public class HubGenomeLoader extends GenomeLoader { String hubURL; @@ -56,13 +58,13 @@ public Genome loadGenome() throws IOException { List trackConfigGroups = hub.getGroupedTrackConfigurations(); for (TrackConfigGroup group : trackConfigGroups) { for (TrackConfig trackConfig : group.tracks) { - if (selectedTrackNames.contains(trackConfig.name)) { + if (selectedTrackNames.contains(trackConfig.getName())) { selectedTracks.add(trackConfig); } } } - selectedTracks.sort((o1, o2) -> o1.order - o2.order); - config.tracks = selectedTracks; + selectedTracks.sort((o1, o2) -> o1.getOrder() - o2.getOrder()); + config.setTracks(selectedTracks); } // If running in interactive mode opend dialog to set tracks. @@ -71,10 +73,10 @@ else if (IGV.hasInstance() && !Globals.isBatch() && !Globals.isHeadless() && !Gl dlg.setVisible(true); List selectedTracks = dlg.getSelectedConfigs(); - config.tracks = selectedTracks; + config.setTracks(selectedTracks); // Remember selections in user preferences - List names = selectedTracks.stream().map((trackConfig) -> trackConfig.name).toList(); + List names = selectedTracks.stream().map((trackConfig) -> trackConfig.getName()).toList(); PreferencesManager.getPreferences().put(key, String.join(",", names)); } diff --git a/src/main/java/org/broad/igv/feature/genome/load/JsonGenomeLoader.java b/src/main/java/org/broad/igv/feature/genome/load/JsonGenomeLoader.java index d2c8499dba..c0750a7f99 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/JsonGenomeLoader.java +++ b/src/main/java/org/broad/igv/feature/genome/load/JsonGenomeLoader.java @@ -13,7 +13,6 @@ import org.broad.igv.logging.Logger; import org.broad.igv.track.TribbleFeatureSource; import org.broad.igv.util.FileUtils; -import org.broad.igv.util.HttpUtils; import org.broad.igv.util.ParsingUtils; import org.broad.igv.util.ResourceLocator; @@ -38,23 +37,12 @@ public JsonGenomeLoader(String genomePath) { public Genome loadGenome() throws IOException { - try (InputStream is = ParsingUtils.openInputStream(genomePath)){ + GenomeConfig genomeConfig = loadGenomeConfig(); - String jsonString = ParsingUtils.readContentsFromStream(is); - - if (jsonString.contains("chromosomeOrder")) { - jsonString = fixChromosomeOrder(jsonString); - } - - GenomeConfig genomeConfig = GenomeConfig.fromJson(jsonString); - - fixPaths(genomeConfig); - - Genome genome = new Genome(genomeConfig); - - // Load liftover "chain" files. This enables navigating by coordinates of another genome. - // Not a common option. + Genome genome = new Genome(genomeConfig); + // Load liftover "chain" files. This enables navigating by coordinates of another genome. + // Not a common option. // JsonElement chains = config.chains; // if (chains != null) { // Map liftoverMap = new HashMap<>(); @@ -67,7 +55,26 @@ public Genome loadGenome() throws IOException { // newGenome.setLiftoverMap(liftoverMap); // } - return genome; + return genome; + + + } + + public GenomeConfig loadGenomeConfig() throws IOException { + + try (InputStream is = ParsingUtils.openInputStream(genomePath)) { + + String jsonString = ParsingUtils.readContentsFromStream(is); + + if (jsonString.contains("chromosomeOrder")) { + jsonString = fixChromosomeOrder(jsonString); + } + + GenomeConfig genomeConfig = GenomeConfig.fromJson(jsonString); + + fixPaths(genomeConfig); + + return genomeConfig; } } @@ -99,50 +106,47 @@ private String fixChromosomeOrder(String jsonString) { */ private GenomeConfig fixPaths(GenomeConfig config) { - if (config.chromAliasBbURL != null) { - config.chromAliasBbURL = FileUtils.getAbsolutePath(config.chromAliasBbURL, genomePath); - } - if (config.twoBitURL != null) { - config.twoBitURL = FileUtils.getAbsolutePath(config.twoBitURL, genomePath); + if (config.getChromAliasBbURL() != null) { + config.setChromAliasBbURL(FileUtils.getAbsolutePath(config.getChromAliasBbURL(), genomePath)); } - if (config.cytobandBbURL != null) { - config.cytobandBbURL = FileUtils.getAbsolutePath(config.cytobandBbURL, genomePath); + if (config.getTwoBitURL() != null) { + config.setTwoBitURL(FileUtils.getAbsolutePath(config.getTwoBitURL(), genomePath)); } - if (config.chromSizesURL != null) { - config.chromSizesURL = FileUtils.getAbsolutePath(config.chromSizesURL, genomePath); + if (config.getTwoBitBptURL() != null) { + config.setTwoBitBptURL(FileUtils.getAbsolutePath(config.getTwoBitBptURL(), genomePath)); } - if (config.fastaURL != null) { - config.fastaURL = FileUtils.getAbsolutePath(config.fastaURL, genomePath); + if (config.getCytobandBbURL() != null) { + config.setCytobandBbURL(FileUtils.getAbsolutePath(config.getCytobandBbURL(), genomePath)); } - if (config.indexURL != null) { - config.indexURL = FileUtils.getAbsolutePath(config.indexURL, genomePath); + if (config.getChromSizesURL() != null) { + config.setChromSizesURL(FileUtils.getAbsolutePath(config.getChromSizesURL(), genomePath)); } - if (config.gziIndexURL != null) { - config.gziIndexURL = FileUtils.getAbsolutePath(config.gziIndexURL, genomePath); + if (config.getFastaURL() != null) { + config.setFastaURL(FileUtils.getAbsolutePath(config.getFastaURL(), genomePath)); } - if (config.compressedIndexURL != null) { - config.compressedIndexURL = FileUtils.getAbsolutePath(config.compressedIndexURL, genomePath); + if (config.getIndexURL() != null) { + config.setIndexURL(FileUtils.getAbsolutePath(config.getIndexURL(), genomePath)); } - if (config.cytobandURL != null) { - config.cytobandURL = FileUtils.getAbsolutePath(config.cytobandURL, genomePath); + if (config.getGziIndexURL() != null) { + config.setGziIndexURL(FileUtils.getAbsolutePath(config.getGziIndexURL(), genomePath)); } - if (config.aliasURL != null) { - config.aliasURL = FileUtils.getAbsolutePath(config.aliasURL, genomePath); + if (config.getCytobandURL() != null) { + config.setCytobandURL(FileUtils.getAbsolutePath(config.getCytobandURL(), genomePath)); } - if (config.chromAliasBbURL != null) { - config.chromAliasBbURL = FileUtils.getAbsolutePath(config.chromAliasBbURL, genomePath); + if (config.getAliasURL() != null) { + config.setAliasURL(FileUtils.getAbsolutePath(config.getAliasURL(), genomePath)); } - List trackConfigs = config.tracks; - if (trackConfigs == null) { - trackConfigs = config.annotations; + if (config.getChromAliasBbURL() != null) { + config.setChromAliasBbURL(FileUtils.getAbsolutePath(config.getChromAliasBbURL(), genomePath)); } + List trackConfigs = config.getTrackConfigs(); if (trackConfigs != null) { trackConfigs.forEach((TrackConfig trackConfig) -> { - if (trackConfig.url != null) { - trackConfig.url = FileUtils.getAbsolutePath(trackConfig.url, genomePath); + if (trackConfig.getUrl() != null) { + trackConfig.setUrl(FileUtils.getAbsolutePath(trackConfig.getUrl(), genomePath)); } - if (trackConfig.indexURL != null) { - trackConfig.indexURL = FileUtils.getAbsolutePath(trackConfig.indexURL, genomePath); + if (trackConfig.getIndexURL() != null) { + trackConfig.setIndexURL(FileUtils.getAbsolutePath(trackConfig.getIndexURL(), genomePath)); } }); } diff --git a/src/main/java/org/broad/igv/feature/genome/load/TrackConfig.java b/src/main/java/org/broad/igv/feature/genome/load/TrackConfig.java index de0a3739bd..5cab6af46a 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/TrackConfig.java +++ b/src/main/java/org/broad/igv/feature/genome/load/TrackConfig.java @@ -2,37 +2,38 @@ /** * A static json-like object, emulates javascript equivalent. Created to ease port of session code from javascript. - * Purpose is to hold state information, we do not bloat this class with needless set/get methods + * */ -public class TrackConfig { - - public String id; - public String name; - public String url; - public String indexURL; - public String format; - public String displayMode; - public String description; - public Boolean autoscale; - public Integer maxHeight; - public Integer height; - public Integer minHeight; - public String color; - public String altColor; - public Float min; - public Float max; - public Boolean visible; - public String infoURL; - public String searchIndex; - public String searchTrix; - public String group; - public Integer order; - public Integer visibilityWindow; - public Boolean indexed; - public Boolean hidden; - public String html; - public String panelName; +public class TrackConfig implements Cloneable { + + private String id; + private String name; + private String url; + private String indexURL; + private String trixURL; + private String format; + private String displayMode; + private String description; + private Boolean autoscale; + private Integer maxHeight; + private Integer height; + private Integer minHeight; + private String color; + private String altColor; + private Float min; + private Float max; + private Boolean visible; + private String infoURL; + private String searchIndex; + + private String group; + private Integer order; + private Integer visibilityWindow; + private Boolean indexed; + private Boolean hidden; + private String html; + private String panelName; public TrackConfig() { } @@ -45,4 +46,216 @@ public TrackConfig(String url) { this.url = url; } + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getIndexURL() { + return indexURL; + } + + public void setIndexURL(String indexURL) { + this.indexURL = indexURL; + } + + public String getFormat() { + return format; + } + + public void setFormat(String format) { + this.format = format; + } + + public String getDisplayMode() { + return displayMode; + } + + public void setDisplayMode(String displayMode) { + this.displayMode = displayMode; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Boolean getAutoscale() { + return autoscale; + } + + public void setAutoscale(Boolean autoscale) { + this.autoscale = autoscale; + } + + public Integer getMaxHeight() { + return maxHeight; + } + + public void setMaxHeight(Integer maxHeight) { + this.maxHeight = maxHeight; + } + + public Integer getHeight() { + return height; + } + + public void setHeight(Integer height) { + this.height = height; + } + + public Integer getMinHeight() { + return minHeight; + } + + public void setMinHeight(Integer minHeight) { + this.minHeight = minHeight; + } + + public String getColor() { + return color; + } + + public void setColor(String color) { + this.color = color; + } + + public String getAltColor() { + return altColor; + } + + public void setAltColor(String altColor) { + this.altColor = altColor; + } + + public Float getMin() { + return min; + } + + public void setMin(Float min) { + this.min = min; + } + + public Float getMax() { + return max; + } + + public void setMax(Float max) { + this.max = max; + } + + public Boolean getVisible() { + return visible; + } + + public void setVisible(Boolean visible) { + this.visible = visible; + } + + public String getInfoURL() { + return infoURL; + } + + public void setInfoURL(String infoURL) { + this.infoURL = infoURL; + } + + public String getSearchIndex() { + return searchIndex; + } + + public void setSearchIndex(String searchIndex) { + this.searchIndex = searchIndex; + } + + public String getTrixURL() { + return trixURL; + } + + public void setTrixURL(String trixURL) { + this.trixURL = trixURL; + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + public Integer getOrder() { + return order; + } + + public void setOrder(Integer order) { + this.order = order; + } + + public Integer getVisibilityWindow() { + return visibilityWindow; + } + + public void setVisibilityWindow(Integer visibilityWindow) { + this.visibilityWindow = visibilityWindow; + } + + public Boolean getIndexed() { + return indexed; + } + + public void setIndexed(Boolean indexed) { + this.indexed = indexed; + } + + public Boolean getHidden() { + return hidden; + } + + public void setHidden(Boolean hidden) { + this.hidden = hidden; + } + + public String getHtml() { + return html; + } + + public void setHtml(String html) { + this.html = html; + } + + public String getPanelName() { + return panelName; + } + + public void setPanelName(String panelName) { + this.panelName = panelName; + } + + @Override + protected TrackConfig clone() throws CloneNotSupportedException { + return (TrackConfig) super.clone(); + } } diff --git a/src/main/java/org/broad/igv/ucsc/Hub.java b/src/main/java/org/broad/igv/ucsc/Hub.java index ec5e7d45eb..79cddb09ad 100644 --- a/src/main/java/org/broad/igv/ucsc/Hub.java +++ b/src/main/java/org/broad/igv/ucsc/Hub.java @@ -177,42 +177,42 @@ public GenomeConfig getGenomeConfig(boolean includeTracks) { // TODO -- add blat? htmlPath? GenomeConfig config = new GenomeConfig(); - config.id = this.genomeStanza.getProperty("genome"); + config.setId(this.genomeStanza.getProperty("genome")); if (this.genomeStanza.hasProperty("scientificName")) { - config.name = this.genomeStanza.getProperty("scientificName"); + config.setName(this.genomeStanza.getProperty("scientificName")); } else if (this.genomeStanza.hasProperty("organism")) { - config.name = this.genomeStanza.getProperty("organism"); + config.setName(this.genomeStanza.getProperty("organism")); } else if (this.genomeStanza.hasProperty("description")) { - config.name = this.genomeStanza.getProperty("description"); + config.setName(this.genomeStanza.getProperty("description")); } - if(config.name == null) { - config.name = config.id; + if(config.getName() == null) { + config.setName(config.getId()); } else { - config.name += " (" + config.id + ")"; + config.setName(config.getName() + " (" + config.getId() + ")"); } - config.twoBitURL = this.baseURL + this.genomeStanza.getProperty("twoBitPath"); - config.nameSet = "ucsc"; - config.wholeGenomeView = false; + config.setTwoBitURL(this.baseURL + this.genomeStanza.getProperty("twoBitPath")); + config.setNameSet("ucsc"); + config.setWholeGenomeView(false); if (this.genomeStanza.hasProperty("defaultPos")) { - config.defaultPos = this.genomeStanza.getProperty("defaultPos"); + config.setDefaultPos(this.genomeStanza.getProperty("defaultPos")); } - config.description = config.id; + config.setDescription(config.getId()); if (this.genomeStanza.hasProperty("blat")) { - config.blat = this.baseURL + this.genomeStanza.getProperty("blat"); + config.setBlat(this.baseURL + this.genomeStanza.getProperty("blat")); } if (this.genomeStanza.hasProperty("chromAliasBb")) { - config.chromAliasBbURL = this.baseURL + this.genomeStanza.getProperty("chromAliasBb"); + config.setChromAliasBbURL(this.baseURL + this.genomeStanza.getProperty("chromAliasBb")); } if (this.genomeStanza.hasProperty("twoBitBptURL")) { - config.twoBitBptURL = this.baseURL + this.genomeStanza.getProperty("twoBitBptURL"); + config.setTwoBitBptURL(this.baseURL + this.genomeStanza.getProperty("twoBitBptURL")); } if (this.genomeStanza.hasProperty("twoBitBptUrl")) { - config.twoBitBptURL = this.baseURL + this.genomeStanza.getProperty("twoBitBptUrl"); + config.setTwoBitBptURL(this.baseURL + this.genomeStanza.getProperty("twoBitBptUrl")); } // chromSizes can take a very long time to load, and is not useful with the default WGV = off @@ -221,17 +221,17 @@ public GenomeConfig getGenomeConfig(boolean includeTracks) { // } if (this.genomeStanza.hasProperty("description")) { - config.description += "\n" + this.genomeStanza.getProperty("description"); + config.setDescription(config.getDescription() + "\n" + this.genomeStanza.getProperty("description")); } if (this.genomeStanza.hasProperty("organism")) { - config.description += "\n" + this.genomeStanza.getProperty("organism"); + config.setDescription(config.getDescription() + "\n" + this.genomeStanza.getProperty("organism")); } if (this.genomeStanza.hasProperty("scientificName")) { - config.description += "\n" + this.genomeStanza.getProperty("scientificName"); + config.setDescription(config.getDescription() + "\n" + this.genomeStanza.getProperty("scientificName")); } if (this.genomeStanza.hasProperty("htmlPath")) { - config.infoURL = this.baseURL + this.genomeStanza.getProperty("htmlPath"); + config.setInfoURL(this.baseURL + this.genomeStanza.getProperty("htmlPath")); } // Search for cytoband @@ -246,7 +246,7 @@ shortLabel Chromosome Band (Ideogram) */ for (Stanza t : this.trackStanzas) { if ("cytoBandIdeo".equals(t.name) && t.hasProperty("bigDataUrl")) { - config.cytobandBbURL = this.baseURL + t.getProperty("bigDataUrl"); + config.setCytobandBbURL(this.baseURL + t.getProperty("bigDataUrl")); break; } } @@ -257,7 +257,7 @@ shortLabel Chromosome Band (Ideogram) return !Hub.filterTracks.contains(t.name) && (!"hide".equals(t.getProperty("visibility"))); }; - config.tracks = this.getTracksConfigs(filter); + config.setTracks(this.getTracksConfigs(filter)); } // config.trackConfigurations = this.#getGroupedTrackConfigurations() @@ -271,7 +271,7 @@ public List getGroupedTrackConfigurations() { LinkedHashMap> trackConfigMap = new LinkedHashMap<>(); java.util.function.Function filter = (stanza -> !stanza.name.equals("cytoBandIdeo")); for (TrackConfig c : this.getTracksConfigs(filter)) { - String groupName = c.group != null ? c.group : "other"; + String groupName = c.getGroup() != null ? c.getGroup() : "other"; if (!trackConfigMap.containsKey(groupName)) { trackConfigMap.put(groupName, new ArrayList<>()); } @@ -314,84 +314,83 @@ TrackConfig getTrackConfig(Stanza t) { String url = this.baseURL + t.getProperty("bigDataUrl"); TrackConfig config = new TrackConfig(url); - config.panelName = IGV.DATA_PANEL_NAME; + config.setPanelName(IGV.DATA_PANEL_NAME); - config.id = t.getProperty("track"); - config.name = t.getProperty("shortLabel"); + config.setId(t.getProperty("track")); + config.setName(t.getProperty("shortLabel")); // TODO -- work on recognizing big* formats // config.format = t.format(); - config.url = this.baseURL + t.getProperty("bigDataUrl"); + config.setUrl(this.baseURL + t.getProperty("bigDataUrl")); // Expanded display mode does not work well in IGV desktop for some tracks //config.displayMode = t.displayMode(); if ("vcfTabix".equals(format)) { - config.indexURL = config.url + ".tbi"; + config.setIndexURL(config.getUrl() + ".tbi"); } if (t.hasProperty("longLabel") && t.hasProperty("html")) { - config.description = - "" + t.getProperty("longLabel") + ""; + config.setDescription("" + t.getProperty("longLabel") + ""); } else if (t.hasProperty("longLabel")) { - config.description = t.getProperty("longLabel"); + config.setDescription(t.getProperty("longLabel")); } if(t.hasProperty("html")) { - config.html = this.baseURL + t.getProperty("html"); + config.setHtml(this.baseURL + t.getProperty("html")); } - config.visible = !("hide".equals(t.getProperty("visibility"))); + config.setVisible(!("hide".equals(t.getProperty("visibility")))); if (t.hasProperty("autoScale")) { - config.autoscale = t.getProperty("autoScale").toLowerCase().equals("on"); + config.setAutoscale(t.getProperty("autoScale").toLowerCase().equals("on")); } if (t.hasProperty("maxHeightPixels")) { String[] tokens = t.getProperty("maxHeightPixels").split(":"); - config.maxHeight = Integer.parseInt(tokens[0]); - config.height = Integer.parseInt(tokens[1]); - config.minHeight = Integer.parseInt(tokens[2]); + config.setMaxHeight(Integer.parseInt(tokens[0])); + config.setHeight(Integer.parseInt(tokens[1])); + config.setMinHeight(Integer.parseInt(tokens[2])); } // TODO -- graphTypeDefault // TODO -- windowingFunction if (t.hasProperty("color")) { String c = t.getProperty("color"); - config.color = c.indexOf(",") > 0 ? "rgb(" + c + ")" : c; + config.setColor(c.indexOf(",") > 0 ? "rgb(" + c + ")" : c); } if (t.hasProperty("altColor")) { String c = t.getProperty("altColor"); - config.altColor = c.indexOf(",") > 0 ? "rgb(" + c + ")" : c; + config.setAltColor(c.indexOf(",") > 0 ? "rgb(" + c + ")" : c); } if (t.hasProperty("viewLimits")) { String[] tokens = t.getProperty("viewLimits").split(":"); - config.min = Float.parseFloat(tokens[0]); + config.setMin(Float.parseFloat(tokens[0])); if (tokens.length > 1) { - config.max = Float.parseFloat(tokens[1]); + config.setMax(Float.parseFloat(tokens[1])); } } if (t.hasProperty("itemRgb")) { // TODO -- this not supported yet } if ("hide".equals(t.getProperty("visibility"))) { - config.visible = false; + config.setVisible(false); } if (t.hasProperty("url")) { - config.infoURL = t.getProperty("url"); + config.setInfoURL(t.getProperty("url")); } if (t.hasProperty("searchIndex")) { - config.searchIndex = t.getProperty("searchIndex"); + config.setSearchIndex(t.getProperty("searchIndex")); } if (t.hasProperty("searchTrix")) { - config.searchTrix = this.baseURL + t.getProperty("searchTrix"); + config.setTrixURL(this.baseURL + t.getProperty("searchTrix")); } if (t.hasProperty("group")) { - config.group = t.getProperty("group"); - if (this.groupPriorityMap != null && this.groupPriorityMap.containsKey(config.group)) { - int nextPriority = this.groupPriorityMap.get(config.group) + 1; - config.order = nextPriority; - this.groupPriorityMap.put(config.group, nextPriority); + config.setGroup(t.getProperty("group")); + if (this.groupPriorityMap != null && this.groupPriorityMap.containsKey(config.getGroup())) { + int nextPriority = this.groupPriorityMap.get(config.getGroup()) + 1; + config.setOrder(nextPriority); + this.groupPriorityMap.put(config.getGroup(), nextPriority); } } diff --git a/src/main/java/org/broad/igv/ucsc/HubTrackSelectionDialog.java b/src/main/java/org/broad/igv/ucsc/HubTrackSelectionDialog.java index 2e9512f848..11ce541de8 100644 --- a/src/main/java/org/broad/igv/ucsc/HubTrackSelectionDialog.java +++ b/src/main/java/org/broad/igv/ucsc/HubTrackSelectionDialog.java @@ -2,16 +2,11 @@ import org.broad.igv.Globals; import org.broad.igv.feature.genome.load.TrackConfig; -import org.broad.igv.ui.FontManager; import org.broad.igv.ui.util.HyperlinkFactory; import javax.swing.*; import javax.swing.border.Border; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.*; @@ -112,9 +107,9 @@ private void okAction() { for (Map.Entry entry : configMap.entrySet()) { final TrackConfig trackConfig = entry.getValue(); if (entry.getKey().isSelected()) { - trackConfig.visible = true; + trackConfig.setVisible(true); } else { - trackConfig.visible = false; + trackConfig.setVisible(false); } } setVisible(false); @@ -144,11 +139,11 @@ private JPanel categoryPanel(TrackConfigGroup configGroup) { JPanel p = new JPanel(); final JCheckBox checkBox = new JCheckBox(); configMap.put(checkBox, trackConfig); - checkBox.setSelected(trackConfig.visible); + checkBox.setSelected(trackConfig.getVisible()); - JLabel label = trackConfig.html == null ? - new JLabel(trackConfig.name) : - HyperlinkFactory.createLink(trackConfig.name, trackConfig.html); + JLabel label = trackConfig.getHtml() == null ? + new JLabel(trackConfig.getName()) : + HyperlinkFactory.createLink(trackConfig.getName(), trackConfig.getHtml()); label.setLabelFor(checkBox); p.add(checkBox); @@ -166,8 +161,8 @@ private JPanel categoryPanel(TrackConfigGroup configGroup) { * @return */ public List getSelectedConfigs() { - List selected = configMap.values().stream().filter(trackConfig -> trackConfig.visible).collect(Collectors.toList()); - selected.sort((o1, o2) -> o1.order - o2.order); + List selected = configMap.values().stream().filter(trackConfig -> trackConfig.getVisible()).collect(Collectors.toList()); + selected.sort((o1, o2) -> o1.getOrder() - o2.getOrder()); return selected; } @@ -391,7 +386,7 @@ public static void main(String[] args) throws InterruptedException, InvocationTa dlf.setVisible(true); for (TrackConfig config : dlf.getSelectedConfigs()) { - System.out.println(config.name); + System.out.println(config.getName()); } } diff --git a/src/main/java/org/broad/igv/ucsc/twobit/TwoBitIndex.java b/src/main/java/org/broad/igv/ucsc/twobit/TwoBitIndex.java index af77ab5394..488a0f24c0 100644 --- a/src/main/java/org/broad/igv/ucsc/twobit/TwoBitIndex.java +++ b/src/main/java/org/broad/igv/ucsc/twobit/TwoBitIndex.java @@ -7,9 +7,7 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.*; public class TwoBitIndex implements BPIndex { @@ -64,12 +62,16 @@ private void readIndex(int seqCount) throws IOException { public long[] search(String term) { if(sequenceDataOffsets.containsKey(term)) { - return new long[]{(long) sequenceDataOffsets.get(term)}; + return new long[]{sequenceDataOffsets.get(term)}; } else { return null; } } + public List getSequenceNames() { + return new ArrayList<>(sequenceDataOffsets.keySet()); + } + ByteBuffer loadBinaryBuffer(long start, int size) throws IOException { try (SeekableStream is = IGVSeekableStreamFactory.getInstance().getStreamFor(path)) { ByteBuffer bb = ByteBuffer.allocate(size); diff --git a/src/main/java/org/broad/igv/ucsc/twobit/TwoBitSequence.java b/src/main/java/org/broad/igv/ucsc/twobit/TwoBitSequence.java index 27368ad839..1176173216 100644 --- a/src/main/java/org/broad/igv/ucsc/twobit/TwoBitSequence.java +++ b/src/main/java/org/broad/igv/ucsc/twobit/TwoBitSequence.java @@ -86,7 +86,12 @@ public byte getBase(String chr, int position) { @Override public List getChromosomeNames() { - return null; + if(this.index instanceof TwoBitIndex) { + return ((TwoBitIndex) this.index).getSequenceNames(); + } else { + // Index is an externally loaded BP+ tree. We can't know the sequence (key) names without walking the whole tree. + return null; + } } @Override diff --git a/src/main/java/org/broad/igv/ui/IGV.java b/src/main/java/org/broad/igv/ui/IGV.java index 421a4628dc..9b71424e3e 100644 --- a/src/main/java/org/broad/igv/ui/IGV.java +++ b/src/main/java/org/broad/igv/ui/IGV.java @@ -252,7 +252,7 @@ public void windowGainedFocus(WindowEvent windowEvent) { GraphicsDevice[] graphDev = graphEnv.getScreenDevices(); Rectangle[] boundsArr = new Rectangle[graphDev.length]; - for (int i=0;i= curScreen.getMaxX() || userMaxY >= curScreen.getMaxY()){ - applicationBounds = new Rectangle(curScreen.x,curScreen.y,Math.min(1150,curScreen.width),Math.min(800,curScreen.height)); + if (userMaxX >= curScreen.getMaxX() || userMaxY >= curScreen.getMaxY()) { + applicationBounds = new Rectangle(curScreen.x, curScreen.y, Math.min(1150, curScreen.width), Math.min(800, curScreen.height)); } break; } } } - if(isNullOrNotContained){ + if (isNullOrNotContained) { // user's preference is null or the (x,y) in user's preference is not contained in any screen // set the application to the main screen - applicationBounds = new Rectangle(0, 0, Math.min(1150,screenBounds.width), Math.min(800,screenBounds.height)); + applicationBounds = new Rectangle(0, 0, Math.min(1150, screenBounds.width), Math.min(800, screenBounds.height)); } mainFrame.setBounds(applicationBounds); @@ -356,11 +356,6 @@ public void focusSearchBox() { contentPane.getCommandBar().focusSearchBox(); } - - public void selectGenomeFromList(String genomeId) { - contentPane.getCommandBar().selectGenome(genomeId); - } - public void enableExtrasMenu() { menuBar.enableExtrasMenu(); @@ -1975,21 +1970,6 @@ public void run() { if (!genomeLoaded) { Genome genome = Genome.nullGenome(); GenomeManager.getInstance().setCurrentGenome(genome); - - - //GenomeManager.getInstance().setCurrentGenome(Genome.NoneGenome()); - // If the error is with the default genome try refreshing it. -// if(genomeId.equals(GenomeListManager.DEFAULT_GENOME.getId())) { -// GenomeManager.getInstance().refreshHostedGenome(genomeId); -// } -// -// genomeId = GenomeListManager.DEFAULT_GENOME.getId(); -// try { -// GenomeManager.getInstance().loadGenomeById(genomeId); -// } catch (IOException e) { -// MessageUtils.showErrorMessage("Error loading genome: " + genomeId, e); -// log.error("Error loading genome: " + genomeId, e); -// } } } @@ -2023,7 +2003,12 @@ public void run() { } if (!success) { String genomeId = preferences.getDefaultGenome(); - contentPane.getCommandBar().selectGenome(genomeId); + //contentPane.getCommandBar().selectGenome(genomeId); + try { + GenomeManager.getInstance().loadGenomeById(genomeId); + } catch (IOException e) { + throw new RuntimeException(e); + } } } else if (igvArgs.getDataFileStrings() != null) { @@ -2103,7 +2088,12 @@ public void run() { // Load the default genome if unsuccessful if (!success) { String genomeId = preferences.getDefaultGenome(); - contentPane.getCommandBar().selectGenome(genomeId); + //contentPane.getCommandBar().selectGenome(genomeId); + try { + GenomeManager.getInstance().loadGenomeById(genomeId); + } catch (IOException e) { + throw new RuntimeException(e); + } } } } diff --git a/src/main/java/org/broad/igv/ui/IGVMenuBar.java b/src/main/java/org/broad/igv/ui/IGVMenuBar.java index fda2e3510b..c97b9e1424 100644 --- a/src/main/java/org/broad/igv/ui/IGVMenuBar.java +++ b/src/main/java/org/broad/igv/ui/IGVMenuBar.java @@ -36,11 +36,12 @@ import org.broad.igv.event.IGVEventBus; import org.broad.igv.event.IGVEventObserver; import org.broad.igv.feature.genome.Genome; +import org.broad.igv.feature.genome.GenomeDownloadUtils; import org.broad.igv.feature.genome.GenomeManager; -import org.broad.igv.feature.genome.GenomeUtils; +import org.broad.igv.feature.genome.ChromSizesUtils; import org.broad.igv.track.AttributeManager; import org.broad.igv.track.Track; -import org.broad.igv.ui.commandbar.GenomeSelectionDialog; +import org.broad.igv.ui.commandbar.HostedGenomeSelectionDialog; import org.broad.igv.util.GoogleUtils; import org.broad.igv.oauth.OAuthProvider; import org.broad.igv.oauth.OAuthUtils; @@ -458,13 +459,13 @@ private void addEncodeItems(List menuItems, String genomeId) { JMenuItem otherSignalsItem = new JMenuItem(); otherSignalsItem.setAction(new BrowseEncodeAction("Other - Signals", 0, BrowseEncodeAction.Type.SIGNALS_OTHER, igv)); - encodeMenuItems.add(otherSignalsItem); + encodeMenuItems.add(otherSignalsItem); JMenuItem otherItem = new JMenuItem(); otherItem.setAction(new BrowseEncodeAction("Other (peaks, calls, ...)", 0, BrowseEncodeAction.Type.OTHER, igv)); encodeMenuItems.add(otherItem); - for(JComponent item : encodeMenuItems) { + for (JComponent item : encodeMenuItems) { menuItems.add(item); item.setVisible(EncodeTrackChooser.genomeSupported(genomeId)); } @@ -481,7 +482,7 @@ private JMenu createGenomesMenu() { JMenu menu = new JMenu("Genomes"); loadGenomeFromServerMenuItem = new JMenuItem("Select Hosted Genome..."); - loadGenomeFromServerMenuItem.addActionListener(e -> GenomeSelectionDialog.selectGenomesFromServer()); + loadGenomeFromServerMenuItem.addActionListener(e -> HostedGenomeSelectionDialog.selectHostedGenome()); loadGenomeFromServerMenuItem.setToolTipText(LOAD_GENOME_SERVER_TOOLTIP); menu.add(loadGenomeFromServerMenuItem); @@ -499,7 +500,7 @@ private JMenu createGenomesMenu() { // If a file selection was made if (file != null) { - Genome newGenome = GenomeManager.getInstance().loadGenome(file.getAbsolutePath()); + GenomeManager.getInstance().loadGenome(file.getAbsolutePath()); } } catch (Exception ex) { MessageUtils.showErrorMessage(ex.getMessage(), ex); @@ -1001,7 +1002,7 @@ public void actionPerformed(ActionEvent actionEvent) { JMenuItem updateCS = new JMenuItem("Update chrom sizes"); updateCS.addActionListener(e -> { try { - GenomeUtils.main(new String[]{}); + ChromSizesUtils.main(new String[]{}); } catch (IOException ioException) { ioException.printStackTrace(); } @@ -1240,7 +1241,7 @@ public void receiveEvent(final IGVEvent event) { final Genome genome = ((GenomeChangeEvent) event).genome(); final String genomeId = genome.getId(); encodeUCSCMenuItem.setVisible(EncodeTrackChooser.genomeSupportedUCSC(genomeId)); - for(JComponent item : encodeMenuItems) { + for (JComponent item : encodeMenuItems) { item.setVisible(EncodeTrackChooser.genomeSupported(genomeId)); } @@ -1330,5 +1331,4 @@ public Foo() { } - } diff --git a/src/main/java/org/broad/igv/ui/action/SelectGenomeAnnotationTracksAction.java b/src/main/java/org/broad/igv/ui/action/SelectGenomeAnnotationTracksAction.java index 419b0a5056..d6671aa3bd 100644 --- a/src/main/java/org/broad/igv/ui/action/SelectGenomeAnnotationTracksAction.java +++ b/src/main/java/org/broad/igv/ui/action/SelectGenomeAnnotationTracksAction.java @@ -80,7 +80,7 @@ public void actionPerformed(ActionEvent evt) { List groups = hub.getGroupedTrackConfigurations(); for (TrackConfigGroup g : groups) { for (TrackConfig config : g.tracks) { - config.visible = loadedTrackPaths.contains(config.url); + config.setVisible(loadedTrackPaths.contains(config.getUrl())); } } @@ -93,13 +93,13 @@ public void actionPerformed(ActionEvent evt) { List selected = new ArrayList<>(); for (TrackConfigGroup g : groups) { for (TrackConfig config : g.tracks) { - if (config.visible) { + if (config.getVisible()) { selected.add(config); - if (!loadedTrackPaths.contains(config.url)) { + if (!loadedTrackPaths.contains(config.getUrl())) { tracksToLoad.add(config); } } else { - trackPathsToRemove.add(config.url); + trackPathsToRemove.add(config.getUrl()); } } } @@ -115,7 +115,7 @@ public void actionPerformed(ActionEvent evt) { // Update preferences String key = "hub:" + hub.getUrl(); - PreferencesManager.getPreferences().put(key, String.join(",", selected.stream().map(c -> c.name).toList())); + PreferencesManager.getPreferences().put(key, String.join(",", selected.stream().map(c -> c.getName()).toList())); } diff --git a/src/main/java/org/broad/igv/ui/commandbar/GenomeComboBox.java b/src/main/java/org/broad/igv/ui/commandbar/GenomeComboBox.java index 1b5fe42332..687eee0a18 100644 --- a/src/main/java/org/broad/igv/ui/commandbar/GenomeComboBox.java +++ b/src/main/java/org/broad/igv/ui/commandbar/GenomeComboBox.java @@ -3,7 +3,6 @@ import org.broad.igv.logging.*; import org.broad.igv.feature.genome.GenomeListItem; import org.broad.igv.feature.genome.GenomeManager; -import org.broad.igv.ui.IGV; import org.broad.igv.ui.UIConstants; import org.broad.igv.ui.util.MessageUtils; import org.broad.igv.util.LongRunningTask; @@ -31,22 +30,20 @@ public GenomeComboBox() { public void refreshGenomeListComboBox() { - setModel(getModelForGenomeListComboBox()); + + setModel(buildModel()); + String curId = GenomeManager.getInstance().getGenomeId(); - Object item = GenomeListManager.getInstance().getLoadedGenomeListItemById(curId); - if (item != null) { - setSelectedItem(item); - } - } + if (curId == null) return; - public boolean hasItem(Object item) { int c = this.getItemCount(); for (int i = 0; i < c; i++) { - if (item.equals(this.getItemAt(i))) { - return true; + final GenomeListItem item = this.getItemAt(i); + if (curId.equals(item.getId())) { + setSelectedItem(item); + break; } } - return false; } /** @@ -54,7 +51,7 @@ public boolean hasItem(Object item) { * * @return */ - private DefaultComboBoxModel getModelForGenomeListComboBox() { + private DefaultComboBoxModel buildModel() { Collection genomes; try { genomes = GenomeListManager.getInstance().getGenomeItemMap().values(); @@ -70,6 +67,15 @@ private DefaultComboBoxModel getModelForGenomeListComboBox() { return new DefaultComboBoxModel(vector); } + public boolean hasItem(Object item) { + int c = this.getItemCount(); + for (int i = 0; i < c; i++) { + if (item.equals(this.getItemAt(i))) { + return true; + } + } + return false; + } class GenomeBoxActionListener implements ActionListener { @@ -79,10 +85,6 @@ public void actionPerformed(ActionEvent actionEvent) { return; } GenomeListItem genomeListItem = (GenomeListItem) selItem; - loadGenomeListItem(genomeListItem); - } - - private void loadGenomeListItem(final GenomeListItem genomeListItem) { // If we haven't changed genomes do nothing if (genomeListItem.getId().equalsIgnoreCase(GenomeManager.getInstance().getGenomeId())) { @@ -94,23 +96,15 @@ private void loadGenomeListItem(final GenomeListItem genomeListItem) { if (genomeListItem != null && genomeListItem.getPath() != null) { if (genomeListItem == GenomeListItem.DOWNLOAD_ITEM) { - GenomeSelectionDialog.selectGenomesFromServer(); + HostedGenomeSelectionDialog.selectHostedGenome(); } else { try { - GenomeManager.getInstance().loadGenomeById(genomeListItem.getId()); + GenomeManager.getInstance().loadGenomeById(genomeListItem.getId()); } catch (Exception e) { log.error(e); - int choice = JOptionPane.showConfirmDialog( - IGV.getInstance().getMainFrame(), "The genome [" + genomeListItem.getId() + - "] could not be read. Would you like to remove the selected entry?", - "", JOptionPane.OK_CANCEL_OPTION); - - if (choice == JOptionPane.OK_OPTION) { - GenomeListManager.getInstance().removeGenomeListItem(genomeListItem); - refreshGenomeListComboBox(); - log.error("Error initializing genome", e); - } + MessageUtils.showErrorMessage("The genome '" + genomeListItem.getDisplayableName() + + "' could not be read.", e); } } } diff --git a/src/main/java/org/broad/igv/ui/commandbar/GenomeListManager.java b/src/main/java/org/broad/igv/ui/commandbar/GenomeListManager.java index b6bab49160..b883de0dfc 100644 --- a/src/main/java/org/broad/igv/ui/commandbar/GenomeListManager.java +++ b/src/main/java/org/broad/igv/ui/commandbar/GenomeListManager.java @@ -3,13 +3,13 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import org.broad.igv.event.GenomeResetEvent; +import org.broad.igv.event.IGVEventBus; import org.broad.igv.logging.*; import org.broad.igv.DirectoryManager; import org.broad.igv.Globals; import org.broad.igv.feature.genome.GenomeListItem; -import org.broad.igv.feature.genome.GenomeManager; import org.broad.igv.feature.genome.load.GenomeDescriptor; -import org.broad.igv.feature.genome.load.GenomeLoader; import org.broad.igv.prefs.Constants; import org.broad.igv.prefs.PreferencesManager; import org.broad.igv.ui.IGVMenuBar; @@ -27,7 +27,8 @@ import java.util.zip.ZipInputStream; /** - * Singleton class for the list of genomes presented in the command bar dropdown. + * Singleton class for managing the list of genomes presented in the command bar combo box. Also has methods for + * searching the hosted genome list. */ public class GenomeListManager { @@ -35,20 +36,23 @@ public class GenomeListManager { private static GenomeListManager theInstance; - private static final String ACT_USER_DEFINED_GENOME_LIST_FILE = "user-defined-genomes.txt"; + private static final String REMOTE_GENOMES_FILE = "remote-genomes.txt"; - public static final GenomeListItem DEFAULT_GENOME = new GenomeListItem( + private static final GenomeListItem DEFAULT_GENOME = new GenomeListItem( "Human (hg38)", - "https://igv.org/genomes/hg38/hg38.json", + "https://igv.org/genomes/json/hg38.json", "hg38"); + private Map genomeItemMap; - private Map userDefinedGenomeMap; + private Map remoteGenomesMap; + + private Map hostedGenomesMap; - private Map serverGenomeMap; + private Map downloadedGenomesMap; - private boolean serverGenomeListUnreachable = false; + private boolean hostedGenomeListUnreachable = false; private static GenomeListSorter sorter = new GenomeListSorter(); @@ -72,23 +76,15 @@ private GenomeListManager() { */ public Map getGenomeItemMap() throws IOException { if (genomeItemMap.isEmpty()) { - rebuildGenomeItemMap(); + rebuildGenomeItemMaps(); } return genomeItemMap; } - /** - * Completely rebuild the genome drop down info. - */ - public void rebuildGenomeItemMap() throws IOException { - serverGenomeMap = null; - userDefinedGenomeMap = null; - genomeItemMap.clear(); - genomeItemMap.putAll(getUserDefinedGenomeMap()); - genomeItemMap.putAll(getCachedGenomeList()); - if (genomeItemMap.isEmpty()) { - genomeItemMap.put(DEFAULT_GENOME.getId(), DEFAULT_GENOME); - } + public void resetForTests() throws IOException { + remoteGenomesMap = null; + new File(TEST_REMOTE_GENOMES_FILE).delete(); + rebuildGenomeItemMaps(); } public List getGenomeListItems() { @@ -99,31 +95,32 @@ public List getGenomeListItems() { /** - * Add an item to the selectable genomes map. If not from server update the user defined file. + * Add an item to the selectable genomes map and record in preferences. * * @param genomeListItem - * @param userDefined */ - public void addGenomeItem(GenomeListItem genomeListItem, boolean userDefined) { + public void addGenomeItem(GenomeListItem genomeListItem) { + + if(genomeItemMap.values().stream().anyMatch(item -> genomeListItem.equals(item))) { + return; + } + genomeItemMap.put(genomeListItem.getId(), genomeListItem); - if (userDefined) { - if (userDefinedGenomeMap == null) userDefinedGenomeMap = new HashMap<>(); - userDefinedGenomeMap.put(genomeListItem.getId(), genomeListItem); - exportUserDefinedGenomeList(); + + if(FileUtils.isRemote(genomeListItem.getPath()) || + !DirectoryManager.getGenomeCacheDirectory().getAbsoluteFile(). + equals(new File(genomeListItem.getPath()).getParentFile().getAbsolutePath())) { + if (remoteGenomesMap == null) { + remoteGenomesMap = new HashMap<>(); + } + remoteGenomesMap.put(genomeListItem.getId(), genomeListItem); + exportRemoteGenomesList(); } - } - /** - * Return genome list item from currently selectable set. To search through - * all server and user defined genomes, use {@link #getGenomeListItem(String)} - * - * @param genomeId - * @return - */ - public GenomeListItem getLoadedGenomeListItemById(String genomeId) { - return genomeItemMap.get(genomeId); + IGVEventBus.getInstance().post(new GenomeResetEvent()); } + /** * Searches through currently loaded GenomeListItems and returns * that with a matching ID. If not found, searches server and @@ -137,15 +134,15 @@ public GenomeListItem getGenomeListItem(String genomeId) { GenomeListItem matchingItem = genomeItemMap.get(genomeId); if (matchingItem == null) { - // If genome archive was not found, check server list - matchingItem = getServerGenomeMap().get(genomeId); + // If genome archive was not found, search hosted genomes + matchingItem = getHostedGenomesMap().get(genomeId); if (matchingItem != null) { return matchingItem; } - // If still not found rebuild the item map + // If not found rebuild the item map try { - rebuildGenomeItemMap(); + rebuildGenomeItemMaps(); } catch (IOException e) { log.error("Error rebuilding genome item map", e); } @@ -155,7 +152,24 @@ public GenomeListItem getGenomeListItem(String genomeId) { } /** - * Build and return a genome list item map from all locally cached genome archive files (.genome and .json files). + * Rebuild the genome drop down info. + */ + private void rebuildGenomeItemMaps() throws IOException { + // Rebuild the selectable genomes map. The order is imporant as downloaded genomes take precedence. + hostedGenomesMap = null; + remoteGenomesMap = null; + downloadedGenomesMap = null; + genomeItemMap.clear(); + genomeItemMap.putAll(getRemoteGenomesMap()); + genomeItemMap.putAll(getDownloadedGenomeMap()); + if (genomeItemMap.isEmpty()) { + genomeItemMap.put(DEFAULT_GENOME.getId(), DEFAULT_GENOME); + } + } + + /** + * Build and return a genome list item map from all local (downloaded) genome files (.genome and .json files) in + * the igv/genomes directory. *

* If both .genome and .json files are found for the same genome ID the .json file is preferred, and the .genome * file deleted. This complicates loading but is neccessary for a transition period. @@ -164,204 +178,160 @@ public GenomeListItem getGenomeListItem(String genomeId) { * @throws IOException * @see GenomeListItem */ - private static Map getCachedGenomeList() { + public Map getDownloadedGenomeMap() { - Map cachedGenomes = new HashMap<>(); - if (!DirectoryManager.getGenomeCacheDirectory().exists()) { - return cachedGenomes; - } + if(downloadedGenomesMap == null) { - File[] files = DirectoryManager.getGenomeCacheDirectory().listFiles(); - - // First loop - .genome files - for (File file : files) { - if (file.isDirectory()) { - continue; + downloadedGenomesMap = new HashMap<>(); + if (!DirectoryManager.getGenomeCacheDirectory().exists()) { + return downloadedGenomesMap; } - if (file.getName().toLowerCase().endsWith(Globals.GENOME_FILE_EXTENSION)) { - ZipFile zipFile = null; - FileInputStream fis = null; - ZipInputStream zipInputStream = null; - try { - zipFile = new ZipFile(file); - fis = new FileInputStream(file); - zipInputStream = new ZipInputStream(new BufferedInputStream(fis)); - ZipEntry zipEntry = zipFile.getEntry(GenomeDescriptor.GENOME_ARCHIVE_PROPERTY_FILE_NAME); - if (zipEntry == null) { - continue; // Should never happen - } + File[] files = DirectoryManager.getGenomeCacheDirectory().listFiles(); - InputStream inputStream = zipFile.getInputStream(zipEntry); - Properties properties = new Properties(); - properties.load(inputStream); + // First loop - .genome files + for (File file : files) { + if (file.isDirectory()) { + continue; + } + if (file.getName().toLowerCase().endsWith(Globals.GENOME_FILE_EXTENSION)) { + ZipFile zipFile = null; + FileInputStream fis = null; + ZipInputStream zipInputStream = null; + try { + zipFile = new ZipFile(file); + fis = new FileInputStream(file); + zipInputStream = new ZipInputStream(new BufferedInputStream(fis)); - GenomeListItem item = - new GenomeListItem(properties.getProperty(GenomeDescriptor.GENOME_ARCHIVE_NAME_KEY), - file.getAbsolutePath(), - properties.getProperty(GenomeDescriptor.GENOME_ARCHIVE_ID_KEY)); + ZipEntry zipEntry = zipFile.getEntry(GenomeDescriptor.GENOME_ARCHIVE_PROPERTY_FILE_NAME); + if (zipEntry == null) { + continue; // Should never happen + } - cachedGenomes.put(item.getId(), item); + InputStream inputStream = zipFile.getInputStream(zipEntry); + Properties properties = new Properties(); + properties.load(inputStream); - } catch (ZipException ex) { - log.error("\nZip error unzipping cached genome.", ex); - try { - file.delete(); - zipInputStream.close(); - } catch (Exception e) { - //ignore exception when trying to delete file - } - } catch (IOException ex) { - log.warn("\nIO error unzipping cached genome.", ex); - try { - file.delete(); - } catch (Exception e) { - //ignore exception when trying to delete file - } - } finally { - try { - if (zipInputStream != null) { + GenomeListItem item = + new GenomeListItem(properties.getProperty(GenomeDescriptor.GENOME_ARCHIVE_NAME_KEY), + file.getAbsolutePath(), + properties.getProperty(GenomeDescriptor.GENOME_ARCHIVE_ID_KEY)); + + downloadedGenomesMap.put(item.getId(), item); + + } catch (ZipException ex) { + log.error("\nZip error unzipping cached genome.", ex); + try { + file.delete(); zipInputStream.close(); + } catch (Exception e) { + //ignore exception when trying to delete file } - if (zipFile != null) { - zipFile.close(); + } catch (IOException ex) { + log.warn("\nIO error unzipping cached genome.", ex); + try { + file.delete(); + } catch (Exception e) { + //ignore exception when trying to delete file } - if (fis != null) { - fis.close(); + } finally { + try { + if (zipInputStream != null) { + zipInputStream.close(); + } + if (zipFile != null) { + zipFile.close(); + } + if (fis != null) { + fis.close(); + } + } catch (IOException ex) { + log.warn("Error closing genome zip stream", ex); } - } catch (IOException ex) { - log.warn("Error closing genome zip stream", ex); } } } - } - // Second loop - .json files - for (File file : files) { - if (file.isDirectory()) { - continue; - } - if (file.getName().toLowerCase().endsWith(".json")) { - try { - BufferedReader reader = new BufferedReader(new FileReader(file)); - JsonParser parser = new JsonParser(); - JsonElement rootElement = parser.parse(reader); - if (rootElement.isJsonObject()) { - JsonObject json = rootElement.getAsJsonObject(); - JsonElement id = json.get("id"); - JsonElement name = json.get("name"); - JsonElement fasta = json.get("fastaURL"); - JsonElement twobit = json.get("twoBitURL"); - if (id == null) { - log.error("Error parsing " + file.getName() + ". \"id\" is required"); - continue; - } - if (name == null) { - log.error("Error parsing " + file.getName() + ". \"name\" is required"); - continue; - } - if (id == null) { - log.error("Error parsing " + file.getName() + ". \"id\" is required"); - continue; - } - if (fasta == null && twobit == null) { - log.error("Error parsing " + file.getName() + ". One of either \"fastaURL\" or \"twoBitURL\" is required"); - continue; - } + // Second loop - .json files + for (File file : files) { + if (file.isDirectory()) { + continue; + } + if (file.getName().toLowerCase().endsWith(".json")) { + try { + BufferedReader reader = new BufferedReader(new FileReader(file)); + JsonParser parser = new JsonParser(); + JsonElement rootElement = parser.parse(reader); + if (rootElement.isJsonObject()) { + JsonObject json = rootElement.getAsJsonObject(); + JsonElement id = json.get("id"); + JsonElement name = json.get("name"); + JsonElement fasta = json.get("fastaURL"); + JsonElement twobit = json.get("twoBitURL"); + if (id == null) { + log.error("Error parsing " + file.getName() + ". \"id\" is required"); + continue; + } + if (name == null) { + log.error("Error parsing " + file.getName() + ". \"name\" is required"); + continue; + } + if (fasta == null && twobit == null) { + log.error("Error parsing " + file.getName() + ". One of either \"fastaURL\" or \"twoBitURL\" is required"); + continue; + } - GenomeListItem item = new GenomeListItem(name.getAsString(), file.getAbsolutePath(), id.getAsString()); - cachedGenomes.put(item.getId(), item); + GenomeListItem item = new GenomeListItem(name.getAsString(), file.getAbsolutePath(), id.getAsString()); + downloadedGenomesMap.put(item.getId(), item); + } + } catch (Exception e) { + log.error("Error parsing genome json: " + file.getAbsolutePath(), e); } - } catch (Exception e) { - log.error("Error parsing genome json: " + file.getAbsolutePath(), e); } } } - return cachedGenomes; - } - - - public Set getServerGenomeIDs() { - return getServerGenomeMap().keySet(); + return downloadedGenomesMap; } - - /** - * Gets the collection of genome list items ids currently in use. - * - * @return Set of ids. - */ - public Collection getSelectableGenomeIDs() { - return genomeItemMap.keySet(); - } - - - public void removeAllItems(List removedValuesList) { + public void removeItems(List removedValuesList) { boolean updateImportFile = false; for (GenomeListItem genomeListItem : removedValuesList) { final String id = genomeListItem.getId(); genomeItemMap.remove(id); - if (userDefinedGenomeMap != null && userDefinedGenomeMap.containsKey(id)) { - userDefinedGenomeMap.remove(id); + if (remoteGenomesMap != null && remoteGenomesMap.containsKey(id)) { + remoteGenomesMap.remove(id); updateImportFile = true; } } if (updateImportFile) { - exportUserDefinedGenomeList(); + exportRemoteGenomesList(); } } - - public void removeGenomeListItem(GenomeListItem genomeListItem) { - final String id = genomeListItem.getId(); - genomeItemMap.remove(id); - - // If this is a cached genome remove it from cache - Map cachedItems = getCachedGenomeList(); - if (cachedItems.containsKey(id)) { - try { - (new File(genomeListItem.getPath())).delete(); - } catch (Exception e) { - log.error("Error deleting genome file: " + genomeListItem.getPath() + ": " + e.getMessage()); - } - } - - removeUserDefinedGenome(id); - } - - public void removeUserDefinedGenome(String id) { - if (userDefinedGenomeMap != null && userDefinedGenomeMap.containsKey(id)) { - userDefinedGenomeMap.remove(id); - exportUserDefinedGenomeList(); - } - } - - - public List getServerGenomeList() { - List items = new ArrayList<>(getServerGenomeMap().values()); + List getHostedGenomeList() { + List items = new ArrayList<>(getHostedGenomesMap().values()); items.sort(sorter); return items; } /** - * Gets a list of all the server genome archive files that - * IGV knows about. + * Gets a list of all hosted genomes * * @return List * @throws IOException * @see GenomeListItem */ - public Map getServerGenomeMap() { + public Map getHostedGenomesMap() { - if (serverGenomeListUnreachable) { + if (hostedGenomeListUnreachable) { return Collections.emptyMap(); } - if (serverGenomeMap == null) { - serverGenomeMap = new HashMap<>(); + if (hostedGenomesMap == null) { + hostedGenomesMap = new HashMap<>(); BufferedReader dataReader = null; InputStream inputStream = null; String genomeListURLString = ""; @@ -388,15 +358,15 @@ public Map getServerGenomeMap() { String url = fields[1]; String id = fields[2]; GenomeListItem item = new GenomeListItem(name, url, id); - serverGenomeMap.put(item.getId(), item); + hostedGenomesMap.put(item.getId(), item); } else { log.error("Found invalid server genome list record: " + genomeRecord); } } } } catch (Exception e) { - serverGenomeListUnreachable = true; - serverGenomeMap = Collections.emptyMap(); + hostedGenomeListUnreachable = true; + hostedGenomesMap = Collections.emptyMap(); log.error("Error fetching genome list: ", e); ConfirmDialog.optionallyShowInfoDialog("Warning: could not connect to the genome server (" + genomeListURLString + "). Only locally defined genomes will be available.", @@ -421,28 +391,32 @@ public Map getServerGenomeMap() { } if (IGVMenuBar.getInstance() != null) { - IGVMenuBar.getInstance().notifyGenomeServerReachable(!serverGenomeListUnreachable); + IGVMenuBar.getInstance().notifyGenomeServerReachable(!hostedGenomeListUnreachable); } - return serverGenomeMap; + return hostedGenomesMap; } /** - * Gets a list of all the user-defined genome archive files that - * IGV knows about. + * Return the list of remote genomes for the dropdown * * @return LinkedHashSet * @throws IOException * @see GenomeListItem */ - public Map getUserDefinedGenomeMap() { + public Map getRemoteGenomesMap() { - if (userDefinedGenomeMap == null) { + if (remoteGenomesMap == null) { boolean updateClientGenomeListFile = false; - userDefinedGenomeMap = new HashMap<>(); + remoteGenomesMap = new HashMap<>(); - File listFile = new File(DirectoryManager.getGenomeCacheDirectory(), getUserDefinedGenomeListFile()); + File listFile = new File(DirectoryManager.getGenomeCacheDirectory(), getRemoteGenomesFilename()); + + if(!listFile.exists()) { + // Try old filename + listFile = new File(DirectoryManager.getGenomeCacheDirectory(), "user-defined-genomes.txt"); + } if (listFile.exists()) { @@ -480,7 +454,7 @@ public Map getUserDefinedGenomeMap() { try { GenomeListItem item = new GenomeListItem(fields[0], file, fields[2]); - userDefinedGenomeMap.put(item.getId(), item); + remoteGenomesMap.put(item.getId(), item); } catch (Exception e) { log.error("Error updating user genome list line '" + nextLine + "'", e); } @@ -499,26 +473,26 @@ public Map getUserDefinedGenomeMap() { } } if (updateClientGenomeListFile) { - exportUserDefinedGenomeList(); + exportRemoteGenomesList(); } } } - return userDefinedGenomeMap; + return remoteGenomesMap; } /** - * Export the user-define genome property file. + * Export the list of selected remote genomes (genomes not in local cache) * * @throws IOException */ - public void exportUserDefinedGenomeList() { + public void exportRemoteGenomesList() { - if (userDefinedGenomeMap == null) { + if (remoteGenomesMap == null) { return; } - File listFile = new File(DirectoryManager.getGenomeCacheDirectory(), getUserDefinedGenomeListFile()); + File listFile = new File(DirectoryManager.getGenomeCacheDirectory(), getRemoteGenomesFilename()); File backup = null; if (listFile.exists()) { backup = new File(listFile.getAbsolutePath() + ".bak"); @@ -533,7 +507,7 @@ public void exportUserDefinedGenomeList() { PrintWriter writer = null; try { writer = new PrintWriter(new BufferedWriter(new FileWriter(listFile))); - for (GenomeListItem genomeListItem : userDefinedGenomeMap.values()) { + for (GenomeListItem genomeListItem : remoteGenomesMap.values()) { writer.print(genomeListItem.getDisplayableName()); writer.print("\t"); writer.print(genomeListItem.getPath()); @@ -558,23 +532,15 @@ public void exportUserDefinedGenomeList() { } - private String getUserDefinedGenomeListFile() { + private String getRemoteGenomesFilename() { if (Globals.isTesting()) { - return TEST_USER_DEFINED_GENOME_LIST_FILE; + return TEST_REMOTE_GENOMES_FILE; } else { - return ACT_USER_DEFINED_GENOME_LIST_FILE; + return REMOTE_GENOMES_FILE; } } - /** - * Added for unit tests - */ - public void clearUserDefinedGenomes() { - userDefinedGenomeMap = null; - new File(TEST_USER_DEFINED_GENOME_LIST_FILE).delete(); - } - private static class GenomeListSorter implements Comparator { @Override @@ -586,7 +552,7 @@ public int compare(GenomeListItem o1, GenomeListItem o2) { // Tacking on a timestamp & random number to avoid file collisions with parallel testing JVMs. Not guaranteed unique // but highly unlikely to be repeated. - public static final String TEST_USER_DEFINED_GENOME_LIST_FILE = "test-user-defined-genomes_" + + public static final String TEST_REMOTE_GENOMES_FILE = "test-remote-genomes_" + System.currentTimeMillis() + "_" + Math.random() + ".txt"; diff --git a/src/main/java/org/broad/igv/ui/commandbar/GenomeSelectionDialog.java b/src/main/java/org/broad/igv/ui/commandbar/GenomeSelectionDialog.java deleted file mode 100644 index 8a9a0b570a..0000000000 --- a/src/main/java/org/broad/igv/ui/commandbar/GenomeSelectionDialog.java +++ /dev/null @@ -1,361 +0,0 @@ -/* - * 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. - */ - -/* - * GenomeSelectionDialog.java - * - * Created on November 8, 2007, 3:51 PM - */ - -package org.broad.igv.ui.commandbar; - -import org.broad.igv.DirectoryManager; -import org.broad.igv.event.GenomeResetEvent; -import org.broad.igv.event.IGVEventBus; -import org.broad.igv.feature.genome.GenomeListItem; -import org.broad.igv.feature.genome.GenomeManager; -import org.broad.igv.logging.LogManager; -import org.broad.igv.logging.Logger; -import org.broad.igv.ui.IGV; -import org.broad.igv.ui.util.IGVMouseInputAdapter; -import org.broad.igv.ui.util.MessageUtils; -import org.broad.igv.ui.util.UIUtilities; -import org.broad.igv.util.LongRunningTask; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import java.awt.*; -import java.awt.event.*; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -/** - * Dialog box for selecting genomes. User can choose from a list, - * which is filtered according to text box input - */ -public class GenomeSelectionDialog extends org.broad.igv.ui.IGVDialog { - - private static Logger log = LogManager.getLogger(GenomeSelectionDialog.class); - - private JPanel dialogPane; - private JPanel contentPanel; - private JTextArea textArea1; - private JPanel filterPanel; - private JLabel label1; - private JTextField genomeFilter; - private JScrollPane scrollPane1; - private JList genomeList; - private JCheckBox downloadDataCB; - private JPanel buttonBar; - private JButton okButton; - private JButton cancelButton; - private boolean isCanceled = true; - - private List selectedValues = null; - private List allListItems; - private DefaultListModel genomeListModel; - - - /** - * Open a selection list to load a genome from the server. This method is static because its used by multiple - * UI elements (menu bar and genome selection pulldown). - */ - public static void selectGenomesFromServer() { - - Runnable showDialog = () -> { - - Collection inputListItems = GenomeListManager.getInstance().getServerGenomeList(); - if (inputListItems == null) { - return; - } - GenomeSelectionDialog dialog = new GenomeSelectionDialog(IGV.getInstance().getMainFrame(), inputListItems); - UIUtilities.invokeAndWaitOnEventThread(() -> dialog.setVisible(true)); - - if (dialog.isCanceled()) { - IGVEventBus.getInstance().post(new GenomeResetEvent()); - } else { - List selectedValueList = dialog.getSelectedValues(); - - for (GenomeListItem selectedValue : selectedValueList) { - if (selectedValue != null) { - - boolean downloadData = dialog.isDownloadData(); - boolean success = GenomeManager.getInstance().downloadGenome(selectedValue, downloadData); - - if (success) { - - // If this is a single selection load it - if(selectedValueList.size() == 1) { - - try { - GenomeManager.getInstance().loadGenome(selectedValue.getPath()); - // If the user has previously defined this genome, remove it. - GenomeListManager.getInstance().removeUserDefinedGenome(selectedValue.getId()); - - // If this is a .json genome, attempt to remove existing .genome files - if (selectedValue.getPath().endsWith(".json")) { - removeDotGenomeFile(selectedValue.getId()); - } - - - } catch (IOException e) { - GenomeListManager.getInstance().removeGenomeListItem(selectedValue); - MessageUtils.showErrorMessage("Error loading genome " + selectedValue.getDisplayableName(), e); - log.error("Error loading genome " + selectedValue.getDisplayableName(), e); - } - } - } - } - } - } - }; - - if (SwingUtilities.isEventDispatchThread()) { - LongRunningTask.submit(showDialog); - } else { - showDialog.run(); - } - } - - private static void removeDotGenomeFile(String id) { - try { - File dotGenomeFile = new File(DirectoryManager.getGenomeCacheDirectory(), id + ".genome"); - if (dotGenomeFile.exists()) { - dotGenomeFile.delete(); - } - } catch (Exception e) { - // If anything goes wrong, just log it, this cleanup is not essential - log.error("Error deleting .genome file", e); - } - } - - /** - * @param parent - */ - private GenomeSelectionDialog(java.awt.Frame parent, Collection inputListItems) { - super(parent); - initComponents(); - initData(inputListItems); - downloadDataCB.setVisible(true); - } - - private void initData(Collection inputListItems) { - this.allListItems = new ArrayList<>(inputListItems); - String filterText = genomeFilter.getText().trim().toLowerCase(); - rebuildGenomeList(filterText); - } - - /** - * Filter the list of displayed genomes with the text the user entered. - */ - private void rebuildGenomeList(String filterText) { - if (genomeListModel == null) { - genomeListModel = new DefaultListModel(); - UIUtilities.invokeOnEventThread(() -> genomeList.setModel(genomeListModel)); - } - genomeListModel.clear(); - filterText = filterText.toLowerCase().trim(); - for (GenomeListItem listItem : allListItems) { - if (listItem.getDisplayableName().toLowerCase().contains(filterText)) { - genomeListModel.addElement(listItem); - } - } - } - - /** - * Double-clicking will trigger the OK action. - * - * @param e - */ - private void genomeListMouseClicked(MouseEvent e) { - if (e.getClickCount() == 2) { - okButtonActionPerformed(null); - } - } - - private void genomeEntryKeyReleased(KeyEvent e) { - rebuildGenomeList(genomeFilter.getText()); - } - - public List getSelectedValues() { - return selectedValues; - } - - public boolean isDownloadData(){ - return downloadDataCB.isSelected(); - } - - public boolean isCanceled() { - return isCanceled; - } - - private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) { - isCanceled = true; - selectedValues = null; - setVisible(false); - dispose(); - } - - private void okButtonActionPerformed(java.awt.event.ActionEvent evt) { - isCanceled = false; - selectedValues = genomeList.getSelectedValuesList(); - setVisible(false); - dispose(); - } - - - private void initComponents() { - dialogPane = new JPanel(); - contentPanel = new JPanel(); - textArea1 = new JTextArea(); - filterPanel = new JPanel(); - label1 = new JLabel(); - genomeFilter = new JTextField(); - scrollPane1 = new JScrollPane(); - genomeList = new JList(); - downloadDataCB = new JCheckBox(); - buttonBar = new JPanel(); - okButton = new JButton(); - cancelButton = new JButton(); - - //======== this ======== - setModal(true); - setTitle("Hosted Genomes"); - Container contentPane = getContentPane(); - contentPane.setLayout(new BorderLayout()); - - //======== dialogPane ======== - { - dialogPane.setBorder(new EmptyBorder(12, 12, 12, 12)); - dialogPane.setPreferredSize(new Dimension(350, 500)); - dialogPane.setLayout(new BorderLayout()); - - //======== contentPanel ======== - { - contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS)); - - //---- textArea1 ---- - textArea1.setText("Selected genomes will be downloaded and added to the genome dropdown list."); - textArea1.setLineWrap(true); - textArea1.setWrapStyleWord(true); - textArea1.setBackground(UIManager.getColor("Button.background")); - textArea1.setRows(2); - textArea1.setMaximumSize(new Dimension(2147483647, 60)); - textArea1.setRequestFocusEnabled(false); - textArea1.setEditable(false); - contentPanel.add(textArea1); - - //======== filterPanel ======== - { - filterPanel.setMaximumSize(new Dimension(2147483647, 28)); - filterPanel.setLayout(new GridBagLayout()); - ((GridBagLayout) filterPanel.getLayout()).columnWidths = new int[]{0, 0, 0}; - ((GridBagLayout) filterPanel.getLayout()).rowHeights = new int[]{0, 0}; - ((GridBagLayout) filterPanel.getLayout()).columnWeights = new double[]{1.0, 1.0, 1.0E-4}; - ((GridBagLayout) filterPanel.getLayout()).rowWeights = new double[]{1.0, 1.0E-4}; - - //---- label1 ---- - label1.setText("Filter:"); - label1.setLabelFor(genomeFilter); - label1.setRequestFocusEnabled(false); - filterPanel.add(label1, new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, - GridBagConstraints.WEST, GridBagConstraints.VERTICAL, - new Insets(0, 0, 0, 0), 0, 0)); - - //---- genomeFilter ---- - genomeFilter.setToolTipText("Filter genome list"); - genomeFilter.setPreferredSize(new Dimension(220, 28)); - genomeFilter.setMinimumSize(new Dimension(180, 28)); - genomeFilter.setAlignmentX(0.0F); - genomeFilter.addKeyListener(new KeyAdapter() { - @Override - public void keyReleased(KeyEvent e) { - genomeEntryKeyReleased(e); - } - }); - filterPanel.add(genomeFilter, new GridBagConstraints(1, 0, 1, 1, 1.0, 0.0, - GridBagConstraints.CENTER, GridBagConstraints.BOTH, - new Insets(0, 0, 0, 0), 0, 0)); - } - contentPanel.add(filterPanel); - - //======== scrollPane1 ======== - { - - //---- genomeList ---- - //genomeList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - genomeList.addMouseListener(new IGVMouseInputAdapter() { - @Override - public void igvMouseClicked(MouseEvent e) { - genomeListMouseClicked(e); - } - }); - scrollPane1.setViewportView(genomeList); - } - contentPanel.add(scrollPane1); - - //---- downloadSequenceCB ---- - downloadDataCB.setText("Download data files"); - downloadDataCB.setAlignmentX(1.0F); - downloadDataCB.setToolTipText("Download all files referenced by the genome definition, including the full sequence for this organism. Note that these files can be very large (human is about 3 gigabytes)"); - downloadDataCB.setMaximumSize(new Dimension(1000, 23)); - downloadDataCB.setPreferredSize(new Dimension(300, 23)); - downloadDataCB.setMinimumSize(new Dimension(300, 23)); - contentPanel.add(downloadDataCB); - } - dialogPane.add(contentPanel, BorderLayout.CENTER); - - //======== buttonBar ======== - { - buttonBar.setBorder(new EmptyBorder(12, 0, 0, 0)); - buttonBar.setLayout(new GridBagLayout()); - ((GridBagLayout) buttonBar.getLayout()).columnWidths = new int[]{0, 85, 80}; - ((GridBagLayout) buttonBar.getLayout()).columnWeights = new double[]{1.0, 0.0, 0.0}; - - //---- okButton ---- - okButton.setText("OK"); - okButton.addActionListener(e -> okButtonActionPerformed(e)); - buttonBar.add(okButton, new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0, - GridBagConstraints.CENTER, GridBagConstraints.BOTH, - new Insets(0, 0, 5, 5), 0, 0)); - - //---- cancelButton ---- - cancelButton.setText("Cancel"); - cancelButton.addActionListener(e -> cancelButtonActionPerformed(e)); - buttonBar.add(cancelButton, new GridBagConstraints(2, 0, 1, 1, 0.0, 0.0, - GridBagConstraints.CENTER, GridBagConstraints.BOTH, - new Insets(0, 0, 5, 0), 0, 0)); - } - dialogPane.add(buttonBar, BorderLayout.SOUTH); - } - contentPane.add(dialogPane, BorderLayout.CENTER); - pack(); - setLocationRelativeTo(getOwner()); - }// //GEN-END:initComponents - -} diff --git a/src/main/java/org/broad/igv/ui/commandbar/HostedGenomeSelectionDialog.java b/src/main/java/org/broad/igv/ui/commandbar/HostedGenomeSelectionDialog.java new file mode 100644 index 0000000000..7a9b9ccd8b --- /dev/null +++ b/src/main/java/org/broad/igv/ui/commandbar/HostedGenomeSelectionDialog.java @@ -0,0 +1,352 @@ +/* + * 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. + */ + +/* + * GenomeSelectionDialog.java + * + * Created on November 8, 2007, 3:51 PM + */ + +package org.broad.igv.ui.commandbar; + +import org.broad.igv.DirectoryManager; +import org.broad.igv.event.GenomeResetEvent; +import org.broad.igv.event.IGVEventBus; +import org.broad.igv.feature.genome.GenomeDownloadUtils; +import org.broad.igv.feature.genome.GenomeListItem; +import org.broad.igv.feature.genome.GenomeManager; +import org.broad.igv.logging.LogManager; +import org.broad.igv.logging.Logger; +import org.broad.igv.ui.IGV; +import org.broad.igv.ui.util.MessageUtils; +import org.broad.igv.ui.util.UIUtilities; +import org.broad.igv.util.LongRunningTask; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.awt.event.*; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Dialog box for selecting genomes. User can choose from a list, + * which is filtered according to text box input + */ +public class HostedGenomeSelectionDialog extends org.broad.igv.ui.IGVDialog { + + private static Logger log = LogManager.getLogger(HostedGenomeSelectionDialog.class); + + private JTextField genomeFilter; + private JList genomeList; + private JCheckBox downloadSequenceCB; + private JCheckBox downloadAnnotationsCB; + private boolean isCanceled; + private List allListItems; + private DefaultListModel genomeListModel; + + + /** + * Open a selection list to load a genome from the server. This method is static because its used by multiple + * UI elements (menu bar and genome selection pulldown). + */ + public static void selectHostedGenome() { + + Runnable showDialog = () -> { + + Collection inputListItems = GenomeListManager.getInstance().getHostedGenomeList(); + if (inputListItems == null) { + return; + } + HostedGenomeSelectionDialog dialog = new HostedGenomeSelectionDialog(IGV.getInstance().getMainFrame(), inputListItems); + UIUtilities.invokeAndWaitOnEventThread(() -> dialog.setVisible(true)); + + if (dialog.isCanceled()) { + IGVEventBus.getInstance().post(new GenomeResetEvent()); + } else { + + GenomeListItem selectedItem = dialog.getSelectedValue(); + if (selectedItem == null) return; + + boolean downloadSequence = dialog.isDownloadSequence(); + boolean downloadAnnotations = dialog.isDownloadAnnotations(); + + File downloadPath = null; + if (downloadSequence || downloadAnnotations || selectedItem.getPath().endsWith(".genome")) { + downloadPath = GenomeManager.getInstance().downloadGenome(selectedItem, downloadSequence, downloadAnnotations); + } + + + try { + if (downloadPath != null) { + GenomeManager.getInstance().loadGenome(downloadPath.getAbsolutePath()); + } else { + GenomeManager.getInstance().loadGenome(selectedItem.getPath()); + } + + // Legacy cleanup - json takes precedence over ".genome" + if (selectedItem.getPath().endsWith(".json")) { + removeDotGenomeFile(selectedItem.getId()); + } + } catch (IOException e) { + MessageUtils.showErrorMessage("Error loading genome " + selectedItem.getDisplayableName(), e); + log.error("Error loading genome " + selectedItem.getDisplayableName(), e); + } + } + }; + + if (SwingUtilities.isEventDispatchThread()) { + LongRunningTask.submit(showDialog); + } else { + showDialog.run(); + } + } + + private static void removeDotGenomeFile(String id) { + try { + File dotGenomeFile = new File(DirectoryManager.getGenomeCacheDirectory(), id + ".genome"); + if (dotGenomeFile.exists()) { + dotGenomeFile.delete(); + } + } catch (Exception e) { + // If anything goes wrong, just log it, this cleanup is not essential + log.error("Error deleting .genome file", e); + } + } + + /** + * @param parent + */ + private HostedGenomeSelectionDialog(java.awt.Frame parent, Collection inputListItems) { + super(parent); + initComponents(); + initData(inputListItems); + } + + private void initData(Collection inputListItems) { + this.allListItems = new ArrayList<>(inputListItems); + String filterText = genomeFilter.getText().trim().toLowerCase(); + rebuildHostedGenomeList(filterText); + } + + /** + * Filter the list of displayed genomes with the text the user entered. + */ + private void rebuildHostedGenomeList(String filterText) { + if (genomeListModel == null) { + genomeListModel = new DefaultListModel(); + UIUtilities.invokeOnEventThread(() -> genomeList.setModel(genomeListModel)); + } + genomeListModel.clear(); + filterText = filterText.toLowerCase().trim(); + for (GenomeListItem listItem : allListItems) { + if (listItem.getDisplayableName().toLowerCase().contains(filterText)) { + genomeListModel.addElement(listItem); + } + } + } + + + private void genomeEntryKeyReleased(KeyEvent e) { + rebuildHostedGenomeList(genomeFilter.getText()); + } + + public GenomeListItem getSelectedValue() { + return isCanceled ? null : genomeList.getSelectedValue(); + } + + public boolean isDownloadSequence() { + return downloadSequenceCB.isSelected(); + } + + public boolean isDownloadAnnotations() { + return downloadAnnotationsCB.isSelected(); + } + + public boolean isCanceled() { + return isCanceled; + } + + private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) { + isCanceled = true; + setVisible(false); + dispose(); + } + + private void okButtonActionPerformed(java.awt.event.ActionEvent evt) { + isCanceled = false; + setVisible(false); + dispose(); + } + + + private void configureDownloadButtons(GenomeListItem item) { + if (item != null) { + final boolean sequenceDownloadable = GenomeDownloadUtils.isSequenceDownloadable(item); + final boolean annotationsDownloadable = GenomeDownloadUtils.isAnnotationsDownloadable(item); + downloadSequenceCB.setEnabled(sequenceDownloadable); + downloadAnnotationsCB.setEnabled(annotationsDownloadable); + } + } + + + private void initComponents() { + + //======== this ======== + setModal(true); + setTitle("Hosted Genomes"); + Container contentPane = getContentPane(); + contentPane.setLayout(new BorderLayout()); + + //======== dialogPane ======== + + JPanel dialogPane = new JPanel(); + dialogPane.setBorder(new EmptyBorder(12, 12, 12, 12)); + dialogPane.setPreferredSize(new Dimension(350, 500)); + dialogPane.setLayout(new BorderLayout()); + + //======== contentPanel ======== + JPanel contentPanel = new JPanel(); + contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS)); + + //---- textArea1 ---- + JTextArea textArea1 = new JTextArea(); + textArea1.setText("Selected genomes will be downloaded and added to the genome dropdown list."); + textArea1.setLineWrap(true); + textArea1.setWrapStyleWord(true); + textArea1.setBackground(UIManager.getColor("Button.background")); + textArea1.setRows(2); + textArea1.setMaximumSize(new Dimension(2147483647, 60)); + textArea1.setRequestFocusEnabled(false); + textArea1.setEditable(false); + contentPanel.add(textArea1); + + //======== filterPanel ======== + JPanel filterPanel = new JPanel(); + filterPanel.setMaximumSize(new Dimension(2147483647, 28)); + filterPanel.setLayout(new GridBagLayout()); + ((GridBagLayout) filterPanel.getLayout()).columnWidths = new int[]{0, 0, 0}; + ((GridBagLayout) filterPanel.getLayout()).rowHeights = new int[]{0, 0}; + ((GridBagLayout) filterPanel.getLayout()).columnWeights = new double[]{1.0, 1.0, 1.0E-4}; + ((GridBagLayout) filterPanel.getLayout()).rowWeights = new double[]{1.0, 1.0E-4}; + + //---- label1 ---- + JLabel filterLabel = new JLabel(); + filterLabel.setText("Filter:"); + filterLabel.setLabelFor(genomeFilter); + filterLabel.setRequestFocusEnabled(false); + filterPanel.add(filterLabel, new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, + GridBagConstraints.WEST, GridBagConstraints.VERTICAL, + new Insets(0, 0, 0, 0), 0, 0)); + + //---- genomeFilter ---- + genomeFilter = new JTextField(); + genomeFilter.setToolTipText("Filter genome list"); + genomeFilter.setPreferredSize(new Dimension(220, 28)); + genomeFilter.setMinimumSize(new Dimension(180, 28)); + genomeFilter.setAlignmentX(0.0F); + genomeFilter.addKeyListener(new KeyAdapter() { + @Override + public void keyReleased(KeyEvent e) { + genomeEntryKeyReleased(e); + } + }); + filterPanel.add(genomeFilter, new GridBagConstraints(1, 0, 1, 1, 1.0, 0.0, + GridBagConstraints.CENTER, GridBagConstraints.BOTH, + new Insets(0, 0, 0, 0), 0, 0)); + + contentPanel.add(filterPanel); + + //---- genomeList ---- + genomeList = new JList(); + genomeList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + genomeList.addListSelectionListener(e -> { + GenomeListItem item = genomeList.getSelectedValue(); + if (item != null) { + configureDownloadButtons(item); + } + }); + + JScrollPane scrollPane1 = new JScrollPane(); + scrollPane1.setViewportView(genomeList); + + contentPanel.add(scrollPane1); + + //---- downloadSequenceCB ---- + downloadSequenceCB = new JCheckBox("Download sequence"); + downloadSequenceCB.setAlignmentX(1.0F); + downloadSequenceCB.setToolTipText("Download the full sequence data for the genome. Note that these files can be large (human is about 1 gigabyte)."); + downloadSequenceCB.setMaximumSize(new Dimension(1000, 23)); + downloadSequenceCB.setPreferredSize(new Dimension(300, 23)); + downloadSequenceCB.setMinimumSize(new Dimension(300, 23)); + downloadSequenceCB.setEnabled(false); // Disabled until a genome is selected. + contentPanel.add(downloadSequenceCB); + + //---- downloadAnnotationsCB ---- + downloadAnnotationsCB = new JCheckBox("Download annotations"); + downloadAnnotationsCB.setAlignmentX(1.0F); + downloadAnnotationsCB.setToolTipText("Download all annotation files referenced by the genome definition."); + downloadAnnotationsCB.setMaximumSize(new Dimension(1000, 23)); + downloadAnnotationsCB.setPreferredSize(new Dimension(300, 23)); + downloadAnnotationsCB.setMinimumSize(new Dimension(300, 23)); + downloadAnnotationsCB.setEnabled(false); // Disabled until a genome is selected + contentPanel.add(downloadAnnotationsCB); + + + dialogPane.add(contentPanel, BorderLayout.CENTER); + + //======== buttonBar ======== + JPanel buttonBar = new JPanel(); + buttonBar.setBorder(new EmptyBorder(12, 0, 0, 0)); + buttonBar.setLayout(new GridBagLayout()); + ((GridBagLayout) buttonBar.getLayout()).columnWidths = new int[]{0, 85, 80}; + ((GridBagLayout) buttonBar.getLayout()).columnWeights = new double[]{1.0, 0.0, 0.0}; + + //---- okButton ---- + JButton okButton = new JButton("OK"); + okButton.addActionListener(e -> okButtonActionPerformed(e)); + buttonBar.add(okButton, new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0, + GridBagConstraints.CENTER, GridBagConstraints.BOTH, + new Insets(0, 0, 5, 5), 0, 0)); + + //---- cancelButton --- + JButton cancelButton = new JButton("Cancel"); + cancelButton.addActionListener(e -> cancelButtonActionPerformed(e)); + buttonBar.add(cancelButton, new GridBagConstraints(2, 0, 1, 1, 0.0, 0.0, + GridBagConstraints.CENTER, GridBagConstraints.BOTH, + new Insets(0, 0, 5, 0), 0, 0)); + + dialogPane.add(buttonBar, BorderLayout.SOUTH); + + contentPane.add(dialogPane, BorderLayout.CENTER); + pack(); + setLocationRelativeTo(getOwner()); + } + + +} diff --git a/src/main/java/org/broad/igv/ui/commandbar/IGVCommandBar.java b/src/main/java/org/broad/igv/ui/commandbar/IGVCommandBar.java index 8c988de069..911e5f8d1c 100644 --- a/src/main/java/org/broad/igv/ui/commandbar/IGVCommandBar.java +++ b/src/main/java/org/broad/igv/ui/commandbar/IGVCommandBar.java @@ -150,33 +150,6 @@ public void setGeneListMode(boolean geneListMode) { zoomControl.setEnabled(!geneListMode); } - - /** - * Selects the first genome from the list which matches this genomeId. - * If not found, checks genomes from the server/user-defined list - * - * @param genomeId - */ - public void selectGenome(String genomeId) { - - //log.warn("Selecting genome " + genomeId); - - GenomeListItem selectedItem = GenomeListManager.getInstance().getGenomeListItem(genomeId); - - if (selectedItem == null || !genomeComboBox.hasItem(selectedItem)) { - try { - GenomeManager.getInstance().loadGenomeById(genomeId); - } catch (IOException e) { - MessageUtils.showErrorMessage("Error loading genome: " + genomeId, e); - log.error("Error loading genome: " + genomeId, e); - } - } - - if (selectedItem != null) { - UIUtilities.invokeAndWaitOnEventThread(() -> genomeComboBox.setSelectedItem(selectedItem)); - } - } - public void updateCurrentCoordinates() { if (IGV.hasInstance()) { @@ -236,13 +209,8 @@ private void adjustPopupWidth(JComboBox box) { } if (scrollPane == null) return; - //Loop through and set width to widest component, plus some padding - int rendererWidth = box.getWidth(); - for (int index = 0; index < box.getItemCount(); index++) { - Object value = box.getItemAt(index); - Component rendererComp = box.getRenderer().getListCellRendererComponent(null, value, index, false, false); - } + int rendererWidth = box.getWidth(); Dimension size = scrollPane.getPreferredSize(); size.width = Math.max(size.width, rendererWidth); scrollPane.setPreferredSize(size); @@ -250,7 +218,7 @@ private void adjustPopupWidth(JComboBox box) { scrollPane.revalidate(); } - // + private void homeButtonActionPerformed(java.awt.event.ActionEvent evt) { Genome genome = GenomeManager.getInstance().getCurrentGenome(); if (FrameManager.isGeneListMode()) { @@ -284,7 +252,6 @@ private void roiToggleButtonActionPerformed(java.awt.event.ActionEvent evt) { } } - // public void receiveEvent(IGVEvent e) { @@ -315,8 +282,6 @@ public void receiveEvent(IGVEvent e) { } } - - // // Set the focus in the search box public void focusSearchBox() { diff --git a/src/main/java/org/broad/igv/ui/commandbar/RemoveGenomesDialog.java b/src/main/java/org/broad/igv/ui/commandbar/RemoveGenomesDialog.java index 68c6e9787a..ff7f90e587 100644 --- a/src/main/java/org/broad/igv/ui/commandbar/RemoveGenomesDialog.java +++ b/src/main/java/org/broad/igv/ui/commandbar/RemoveGenomesDialog.java @@ -29,8 +29,8 @@ package org.broad.igv.ui.commandbar; +import org.broad.igv.feature.genome.DotGenomeUtils; import org.broad.igv.logging.*; -import org.broad.igv.DirectoryManager; import org.broad.igv.event.GenomeResetEvent; import org.broad.igv.event.IGVEventBus; import org.broad.igv.feature.genome.GenomeListItem; @@ -46,7 +46,6 @@ import java.awt.event.ActionEvent; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; -import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -55,12 +54,12 @@ public class RemoveGenomesDialog extends org.broad.igv.ui.IGVDialog { + public static final String LOCAL_SEQUENCE_CHAR = "\u002A"; private static Logger log = LogManager.getLogger(RemoveGenomesDialog.class); private List allListItems; private boolean haveLocalSequence = false; - private static final String LOCAL_SEQUENCE_CHAR = "\u002A"; public RemoveGenomesDialog(Frame owner) { super(owner); @@ -76,7 +75,7 @@ private void initData() { allListItems = new ArrayList<>(GenomeListManager.getInstance().getGenomeListItems()); for (GenomeListItem item : allListItems) { - if (GenomeManager.getInstance().getLocalFasta(item.getId()) != null) { + if (DotGenomeUtils.getLocalFasta(item.getId()) != null) { haveLocalSequence = true; break; } @@ -104,13 +103,19 @@ private void saveButtonActionPerformed(ActionEvent event) { Runnable runnable = () -> { List selectedValuesList = genomeList.getSelectedValuesList(); if (selectedValuesList != null && !selectedValuesList.isEmpty()) { + + // Remove from the dropdown list + GenomeListManager.getInstance().removeItems(selectedValuesList); + + // Remove downloaded genomes, if any, and associated files try { - deleteDownloadedGenomes(selectedValuesList); + GenomeManager.getInstance().deleteDownloadedGenomes(selectedValuesList); } catch (IOException e) { log.error("Error deleting genome files", e); MessageUtils.showErrorMessage("Error deleting genome files", e); } + // If the last genome selected (DEFAULT_GENOME) was removed reset the key String lastGenomeKey = PreferencesManager.getPreferences().get(Constants.DEFAULT_GENOME); for (GenomeListItem item : selectedValuesList) { if (lastGenomeKey.equals(item.getId())) { @@ -130,38 +135,6 @@ private void saveButtonActionPerformed(ActionEvent event) { } - /** - * Delete the specified .genome files. - * - * @param removedValuesList - */ - public void deleteDownloadedGenomes(List removedValuesList) throws IOException { - - for (GenomeListItem item : removedValuesList) { - String loc = item.getPath(); - File genFile = new File(loc); - if (DirectoryManager.isChildOf(DirectoryManager.getGenomeCacheDirectory(), genFile)) { - genFile.delete(); - } - - File localFasta = GenomeManager.getInstance().getLocalFasta(item.getId()); - if (localFasta != null) { - // If fasta file is in the "igv/genomes" directory delete it - GenomeManager.getInstance().removeLocalFasta(item.getId()); - if (DirectoryManager.isChildOf(DirectoryManager.getGenomeCacheDirectory(), localFasta)) { - if (MessageUtils.confirm("Delete fasta file: " + localFasta.getAbsolutePath() + "?")) { - localFasta.delete(); - File indexFile = new File(localFasta.getAbsolutePath() + ".fai"); - if (indexFile.exists()) { - indexFile.delete(); - } - } - } - } - } - GenomeListManager.getInstance().removeAllItems(removedValuesList); - } - private void removeSelected() { List selectedValuesList = genomeList.getSelectedValuesList(); allListItems.removeAll(selectedValuesList); @@ -195,7 +168,7 @@ public Component getListCellRendererComponent(JList list, Object value, int inde comp.setOpaque(isSelected); } - if (GenomeManager.getInstance().getLocalFasta(item.getId()) != null) { + if (DotGenomeUtils.getLocalFasta(item.getId()) != null) { displayableName += LOCAL_SEQUENCE_CHAR; } diff --git a/src/main/java/org/broad/igv/ui/panel/DataPanel.java b/src/main/java/org/broad/igv/ui/panel/DataPanel.java index 028344d4d8..ee7e88be5b 100644 --- a/src/main/java/org/broad/igv/ui/panel/DataPanel.java +++ b/src/main/java/org/broad/igv/ui/panel/DataPanel.java @@ -567,12 +567,12 @@ class DataPanelMouseAdapter extends MouseInputAdapter { @Override public void mouseMoved(MouseEvent e) { String position = null; - if (!frame.getChrName().equals(Globals.CHR_ALL)) { - int location = (int) frame.getChromosomePosition(e) + 1; - position = frame.getChrName() + ":" + locationFormatter.format(location); - IGV.getInstance().setStatusBarMessag2(position); - } - updateTooltipText(e.getX(), e.getY()); +// if (!frame.getChrName().equals(Globals.CHR_ALL)) { +// int location = (int) frame.getChromosomePosition(e) + 1; +// position = frame.getChrName() + ":" + locationFormatter.format(location); +// IGV.getInstance().setStatusBarMessag2(position); +// } +// updateTooltipText(e.getX(), e.getY()); if (IGV.getInstance().isRulerEnabled()) { IGV.getInstance().repaint(); diff --git a/src/main/java/org/broad/igv/ui/util/download/Downloader.java b/src/main/java/org/broad/igv/ui/util/download/Downloader.java index 1be6349879..aa26570711 100644 --- a/src/main/java/org/broad/igv/ui/util/download/Downloader.java +++ b/src/main/java/org/broad/igv/ui/util/download/Downloader.java @@ -177,7 +177,7 @@ public static boolean download(URL url, File localFile, Component frame) throws int max = 100; final javax.swing.ProgressMonitor monitor; - if(IGV.hasInstance()) { + if(IGV.hasInstance() && frame != null) { monitor = new javax.swing.ProgressMonitor(frame, message, "", min, max); monitor.setMillisToDecideToPopup(100); } else { diff --git a/src/main/java/org/broad/igv/util/ResourceLocator.java b/src/main/java/org/broad/igv/util/ResourceLocator.java index 7fb3e4b030..ecf8a42a19 100644 --- a/src/main/java/org/broad/igv/util/ResourceLocator.java +++ b/src/main/java/org/broad/igv/util/ResourceLocator.java @@ -635,31 +635,31 @@ public void setMetadata(Map metadata) { } public static ResourceLocator fromTrackConfig(TrackConfig trackConfig) { - String trackPath = trackConfig.url; + String trackPath = trackConfig.getUrl(); ResourceLocator res = new ResourceLocator(trackPath); - res.setName(trackConfig.name); - res.setIndexPath(trackConfig.indexURL); - res.setFormat(trackConfig.format); - Integer vw = trackConfig.visibilityWindow; + res.setName(trackConfig.getName()); + res.setIndexPath(trackConfig.getIndexURL()); + res.setFormat(trackConfig.getFormat()); + Integer vw = trackConfig.getVisibilityWindow(); if (vw != null) { res.setVisibilityWindow(vw); } - if(trackConfig.panelName != null) { - res.setPanelName(trackConfig.panelName); + if(trackConfig.getPanelName() != null) { + res.setPanelName(trackConfig.getPanelName()); } - if (trackConfig.searchTrix != null) { - res.setTrixURL(trackConfig.searchTrix); + if (trackConfig.getTrixURL() != null) { + res.setTrixURL(trackConfig.getTrixURL()); } - res.setFeatureInfoURL(trackConfig.infoURL); - Boolean indexed = trackConfig.indexed; + res.setFeatureInfoURL(trackConfig.getInfoURL()); + Boolean indexed = trackConfig.getIndexed(); if (indexed != null) { res.setIndexed(indexed); } // Track properties TrackProperties properties = new TrackProperties(); - String color = trackConfig.color; + String color = trackConfig.getColor(); if (color != null) { try { properties.setColor(ColorUtilities.stringToColor(color.toString())); @@ -667,7 +667,7 @@ public static ResourceLocator fromTrackConfig(TrackConfig trackConfig) { log.error("Error parsing color string: " + color, e); } } - String altColor = trackConfig.altColor; + String altColor = trackConfig.getAltColor(); if (altColor != null) { try { properties.setAltColor(ColorUtilities.stringToColor(altColor.toString())); @@ -675,7 +675,7 @@ public static ResourceLocator fromTrackConfig(TrackConfig trackConfig) { log.error("Error parsing color string: " + altColor, e); } } - String displayMode = trackConfig.displayMode; + String displayMode = trackConfig.getDisplayMode(); if (displayMode != null) { try { Track.DisplayMode dp = Track.DisplayMode.valueOf(stripQuotes(displayMode.toString())); @@ -684,7 +684,7 @@ public static ResourceLocator fromTrackConfig(TrackConfig trackConfig) { log.error("Error parsing displayMode " + displayMode, e); } } - Integer vizwindow = trackConfig.visibilityWindow; + Integer vizwindow = trackConfig.getVisibilityWindow(); if (vizwindow != null) { properties.setFeatureVisibilityWindow(vizwindow); } else { @@ -692,11 +692,11 @@ public static ResourceLocator fromTrackConfig(TrackConfig trackConfig) { properties.setFeatureVisibilityWindow(-1); } - if (trackConfig.min != null) { - properties.setMinValue(trackConfig.min); + if (trackConfig.getMin() != null) { + properties.setMinValue(trackConfig.getMin()); } - if (trackConfig.max != null) { - properties.setMaxValue(trackConfig.max); + if (trackConfig.getMax() != null) { + properties.setMaxValue(trackConfig.getMax()); } res.setTrackProperties(properties); diff --git a/src/test/java/org/broad/igv/feature/genome/GenomeManagerTest.java b/src/test/java/org/broad/igv/feature/genome/GenomeManagerTest.java index ed3ce1ca70..c2c77d06a8 100644 --- a/src/test/java/org/broad/igv/feature/genome/GenomeManagerTest.java +++ b/src/test/java/org/broad/igv/feature/genome/GenomeManagerTest.java @@ -49,7 +49,6 @@ public GenomeManagerTest() { @BeforeClass public static void setUpClass() throws Exception { - GenomeManager.getInstance().clearGenomeCache(); AbstractHeadlessTest.setUpClass(); genomeManager = GenomeManager.getInstance(); } diff --git a/src/test/java/org/broad/igv/feature/genome/GenomeTest.java b/src/test/java/org/broad/igv/feature/genome/GenomeTest.java index 9ff8839e81..dc133a3ff2 100644 --- a/src/test/java/org/broad/igv/feature/genome/GenomeTest.java +++ b/src/test/java/org/broad/igv/feature/genome/GenomeTest.java @@ -30,7 +30,6 @@ import org.broad.igv.feature.genome.fasta.FastaIndex; import org.broad.igv.feature.genome.load.GenomeConfig; import org.broad.igv.util.TestUtils; -import org.junit.Assume; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; @@ -65,9 +64,9 @@ public void testGetLongChromosomeNames() throws Exception { String mockIndexPath = TestUtils.DATA_DIR + "fasta/bosTau9.fa.fai"; Sequence sequence = new MockSequence(mockIndexPath); GenomeConfig genomeConfig = new GenomeConfig(); - genomeConfig.id = "bosTau9"; - genomeConfig.name = "bosTau9"; - genomeConfig.sequence = sequence; + genomeConfig.setId("bosTau9"); + genomeConfig.setName("bosTau9"); + genomeConfig.setSequence(sequence); Genome genome = new Genome(genomeConfig); List longChrs = genome.getLongChromosomeNames(); assertEquals(30, longChrs.size()); @@ -78,9 +77,9 @@ public void testGetLongChromosomeNames2() throws Exception { String mockIndexPath = TestUtils.DATA_DIR + "fasta/hg19.fa.fai"; Sequence sequence = new MockSequence(mockIndexPath); GenomeConfig genomeConfig = new GenomeConfig(); - genomeConfig.id = "hg19"; - genomeConfig.name = "hg19"; - genomeConfig.sequence = sequence; + genomeConfig.setId("hg19"); + genomeConfig.setName("hg19"); + genomeConfig.setSequence(sequence); Genome genome = new Genome(genomeConfig); List longChrs = genome.getLongChromosomeNames(); assertEquals(24, longChrs.size()); @@ -91,9 +90,9 @@ public void testGetLongChromosomeNames3() throws Exception { String mockIndexPath = TestUtils.DATA_DIR + "fasta/musa_pseudochromosome.fa.fai"; Sequence sequence = new MockSequence(mockIndexPath); GenomeConfig genomeConfig = new GenomeConfig(); - genomeConfig.id = "musa_pseudochromosome"; - genomeConfig.name = "musa_pseudochromosome"; - genomeConfig.sequence = sequence; + genomeConfig.setId("musa_pseudochromosome"); + genomeConfig.setName("musa_pseudochromosome"); + genomeConfig.setSequence(sequence); Genome genome = new Genome(genomeConfig); List longChrs = genome.getLongChromosomeNames(); assertEquals(12, longChrs.size()); @@ -104,9 +103,9 @@ public void testGetLongChromosomeNames_manySmall() throws Exception { String mockIndexPath = TestUtils.DATA_DIR + "fasta/mock_many_small.fa.fai"; Sequence sequence = new MockSequence(mockIndexPath); GenomeConfig genomeConfig = new GenomeConfig(); - genomeConfig.id = "mock_many_small"; - genomeConfig.name = "mock_many_small"; - genomeConfig.sequence = sequence; + genomeConfig.setId("mock_many_small"); + genomeConfig.setName("mock_many_small"); + genomeConfig.setSequence(sequence); Genome genome = new Genome(genomeConfig); assertNotNull(genome.getLongChromosomeNames()); assertTrue("No 'Long' chromosome names found", genome.getLongChromosomeNames().size() > 0); diff --git a/src/test/java/org/broad/igv/tools/IGVToolsCountTest.java b/src/test/java/org/broad/igv/tools/IGVToolsCountTest.java index 3c4a891964..e3d415ed23 100644 --- a/src/test/java/org/broad/igv/tools/IGVToolsCountTest.java +++ b/src/test/java/org/broad/igv/tools/IGVToolsCountTest.java @@ -30,7 +30,6 @@ import org.broad.igv.data.WiggleParser; import org.broad.igv.feature.LocusScore; import org.broad.igv.feature.genome.Genome; -import org.broad.igv.feature.genome.GenomeUtils; import org.broad.igv.feature.tribble.CodecFactory; import org.broad.igv.tdf.TDFDataSource; import org.broad.igv.tdf.TDFDataset; diff --git a/src/test/java/org/broad/igv/ucsc/HubTest.java b/src/test/java/org/broad/igv/ucsc/HubTest.java index d5bca2906b..c2960b8ef8 100644 --- a/src/test/java/org/broad/igv/ucsc/HubTest.java +++ b/src/test/java/org/broad/igv/ucsc/HubTest.java @@ -1,7 +1,5 @@ package org.broad.igv.ucsc; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; import org.broad.igv.feature.genome.load.GenomeConfig; import org.broad.igv.util.TestUtils; import org.junit.Test; @@ -24,12 +22,12 @@ public void testGetGenomeConfig() throws IOException { GenomeConfig genomeConfig = hub.getGenomeConfig(true); assertNotNull(genomeConfig); - assertEquals("GCF_000186305.1", genomeConfig.id); - assertEquals("Python bivittatus (GCF_000186305.1)", genomeConfig.name); - assertNotNull(genomeConfig.twoBitBptURL); - assertNotNull(genomeConfig.twoBitURL); - assertNotNull(genomeConfig.chromAliasBbURL); - assertNotNull(genomeConfig.cytobandBbURL); + assertEquals("GCF_000186305.1", genomeConfig.getId()); + assertEquals("Python bivittatus (GCF_000186305.1)", genomeConfig.getName()); + assertNotNull(genomeConfig.getTwoBitBptURL()); + assertNotNull(genomeConfig.getTwoBitURL()); + assertNotNull(genomeConfig.getChromAliasBbURL()); + assertNotNull(genomeConfig.getCytobandBbURL()); } @Test diff --git a/src/test/java/org/broad/igv/util/TestUtils.java b/src/test/java/org/broad/igv/util/TestUtils.java index 377a4c683a..99f21fddaf 100644 --- a/src/test/java/org/broad/igv/util/TestUtils.java +++ b/src/test/java/org/broad/igv/util/TestUtils.java @@ -570,11 +570,10 @@ public static File replaceTestPaths(File inputPath) throws Exception { } public static void resetTestUserDefinedGenomes() throws IOException { - File userDefinedGenomeListFile = new File(DirectoryManager.getGenomeCacheDirectory(), GenomeListManager.TEST_USER_DEFINED_GENOME_LIST_FILE); + File userDefinedGenomeListFile = new File(DirectoryManager.getGenomeCacheDirectory(), GenomeListManager.TEST_REMOTE_GENOMES_FILE); userDefinedGenomeListFile.delete(); userDefinedGenomeListFile.deleteOnExit(); - GenomeListManager.getInstance().clearUserDefinedGenomes(); - GenomeListManager.getInstance().rebuildGenomeItemMap(); + GenomeListManager.getInstance().resetForTests(); } } diff --git a/test/batch/genome.txt b/test/batch/genome.txt new file mode 100644 index 0000000000..1df8f2ac56 --- /dev/null +++ b/test/batch/genome.txt @@ -0,0 +1,2 @@ +#genome "/Users/jrobinso/igv-team Dropbox/James Robinson/projects/igv/test/data/fasta/ecoli_out.padded.fasta" +genome hg19 From 2ed14fc61a673e45d8ba15e008dac39400c3af8a Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Fri, 18 Oct 2024 21:44:03 -0700 Subject: [PATCH 073/130] bug fix for fasta genome --- .../org/broad/igv/feature/genome/load/FastaGenomeLoader.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/broad/igv/feature/genome/load/FastaGenomeLoader.java b/src/main/java/org/broad/igv/feature/genome/load/FastaGenomeLoader.java index 2664c2c079..ee56bd688b 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/FastaGenomeLoader.java +++ b/src/main/java/org/broad/igv/feature/genome/load/FastaGenomeLoader.java @@ -45,12 +45,13 @@ public Genome loadGenome() throws IOException { config.setTwoBitURL(genomePath); } else { - String fastaPath = null; + String fastaPath; String fastaIndexPath; if (genomePath.endsWith(".fai")) { fastaPath = genomePath.substring(0, genomePath.length() - 4); fastaIndexPath = genomePath; } else { + fastaPath = genomePath; fastaIndexPath = genomePath + ".fai"; } From 20da885008f1bc039f4035630cad3b05ecc7460c Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Sun, 20 Oct 2024 22:42:42 -0700 Subject: [PATCH 074/130] "Copy read details" issue #1604 --- src/main/java/org/broad/igv/sam/SAMAlignment.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/broad/igv/sam/SAMAlignment.java b/src/main/java/org/broad/igv/sam/SAMAlignment.java index a5c8b59722..5c7a0eb6be 100644 --- a/src/main/java/org/broad/igv/sam/SAMAlignment.java +++ b/src/main/java/org/broad/igv/sam/SAMAlignment.java @@ -692,8 +692,8 @@ public String getAlignmentValueString(double position, int mouseX, AlignmentTrac buf.append("Dist: " + getClusterDistance() + "
"); } - boolean hideSmallIndels = renderOptions.isHideSmallIndels(); - int smallIndelThreshold = renderOptions.getSmallIndelThreshold(); + boolean hideSmallIndels = renderOptions == null ? false : renderOptions.isHideSmallIndels(); + int smallIndelThreshold = renderOptions == null ? 0 : renderOptions.getSmallIndelThreshold(); boolean atInsertion = false; boolean atBaseMod = false; From af07c3b1be8806cfd77343ee04982aeff17d2beb Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Mon, 21 Oct 2024 09:26:33 -0700 Subject: [PATCH 075/130] Refactor "copy read details" code, and minor reformatting of output. --- .../java/org/broad/igv/sam/Alignment.java | 4 +++- .../org/broad/igv/sam/AlignmentTrackMenu.java | 15 +++++--------- .../broad/igv/sam/DotAlignedAlignment.java | 4 ---- .../igv/sam/FeatureWrappedAlignment.java | 4 ---- .../org/broad/igv/sam/LinkedAlignment.java | 7 +------ .../org/broad/igv/sam/PairedAlignment.java | 13 ------------ .../broad/igv/sam/ReducedMemoryAlignment.java | 4 ---- .../java/org/broad/igv/sam/SAMAlignment.java | 20 +++++++++++++++++-- .../broad/igv/sam/BisulfiteBaseInfoTest.java | 4 ---- 9 files changed, 27 insertions(+), 48 deletions(-) diff --git a/src/main/java/org/broad/igv/sam/Alignment.java b/src/main/java/org/broad/igv/sam/Alignment.java index 835973795c..1717a13088 100644 --- a/src/main/java/org/broad/igv/sam/Alignment.java +++ b/src/main/java/org/broad/igv/sam/Alignment.java @@ -139,7 +139,9 @@ default int getLeadingHardClipLength() { default String getLibrary(){ return null;} - String getClipboardString(double location, int mouseX); + default String getClipboardString(double location, int mouseX) { + return getValueString(location, mouseX, null); + } default void finish(){}; diff --git a/src/main/java/org/broad/igv/sam/AlignmentTrackMenu.java b/src/main/java/org/broad/igv/sam/AlignmentTrackMenu.java index eeb2ecda45..830105d5f9 100644 --- a/src/main/java/org/broad/igv/sam/AlignmentTrackMenu.java +++ b/src/main/java/org/broad/igv/sam/AlignmentTrackMenu.java @@ -211,8 +211,8 @@ private void addDuplicatesMenuItem() { mi.setSelected(previous == option); mi.addActionListener(aEvt -> { renderOptions.setDuplicatesOption(option); - if(previous != option) { - if (previous.filtered != option.filtered){ + if (previous != option) { + if (previous.filtered != option.filtered) { // duplicates are filtered out when loading the read data so a reload has to be performed in this case IGVEventBus.getInstance().post(new AlignmentTrackEvent(AlignmentTrackEvent.Type.RELOAD)); } else { @@ -1220,19 +1220,14 @@ void addThirdGenItems(Alignment clickedAlignment, final TrackClickEvent tce) { private void copyToClipboard(final TrackClickEvent e, Alignment alignment, double location, int mouseX) { if (alignment != null) { - StringBuilder buf = new StringBuilder(); - buf.append(alignment.getClipboardString(location, mouseX) + final String clipboardString = alignment.getClipboardString(location, mouseX) .replace("", "") .replace("", "") .replace("
", "\n") .replace("
", "\n") .replace("


", "\n------------------\n") - .replace("
", "\n------------------\n")); - buf.append("\n"); - buf.append("Alignment start position = ").append(alignment.getChr()).append(":").append(alignment.getAlignmentStart() + 1); - buf.append("\n"); - buf.append(alignment.getReadSequence()); - StringSelection stringSelection = new StringSelection(buf.toString()); + .replace("
", "\n------------------\n"); + StringSelection stringSelection = new StringSelection(clipboardString); Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); clipboard.setContents(stringSelection, null); } diff --git a/src/main/java/org/broad/igv/sam/DotAlignedAlignment.java b/src/main/java/org/broad/igv/sam/DotAlignedAlignment.java index 786ef6325b..07af63a37d 100644 --- a/src/main/java/org/broad/igv/sam/DotAlignedAlignment.java +++ b/src/main/java/org/broad/igv/sam/DotAlignedAlignment.java @@ -151,10 +151,6 @@ public LocusScore copy() { return this; } - public String getClipboardString(double location, int mouseX) { - return getValueString(location, mouseX, (WindowFunction) null); - } - public String getValueString(double position, int mouseX, WindowFunction windowFunction) { return readName + "
Read length = " + (getEnd() - getStart()); } diff --git a/src/main/java/org/broad/igv/sam/FeatureWrappedAlignment.java b/src/main/java/org/broad/igv/sam/FeatureWrappedAlignment.java index e0519622ec..2e47366062 100644 --- a/src/main/java/org/broad/igv/sam/FeatureWrappedAlignment.java +++ b/src/main/java/org/broad/igv/sam/FeatureWrappedAlignment.java @@ -143,10 +143,6 @@ public LocusScore copy() { return this; } - public String getClipboardString(double location, int mouseX) { - return getValueString(location, mouseX, (WindowFunction) null); - } - public String getValueString(double position, int mouseX, WindowFunction windowFunction) { return readName + "
Read length = " + (getEnd() - getStart()); } diff --git a/src/main/java/org/broad/igv/sam/LinkedAlignment.java b/src/main/java/org/broad/igv/sam/LinkedAlignment.java index e90eb6dc50..0f86b07a14 100644 --- a/src/main/java/org/broad/igv/sam/LinkedAlignment.java +++ b/src/main/java/org/broad/igv/sam/LinkedAlignment.java @@ -9,7 +9,7 @@ import java.util.*; /** - * Class for experimenting with 10X linked reads. + * Represents a collection of alignments linked by a tag or read name */ public class LinkedAlignment implements Alignment { @@ -363,11 +363,6 @@ public String getSample() { return sample; } - @Override - public String getClipboardString(double location, int mouseX) { - return null; - } - @Override public void finish() { alignments.sort(ALIGNMENT_START_COMPARATOR); diff --git a/src/main/java/org/broad/igv/sam/PairedAlignment.java b/src/main/java/org/broad/igv/sam/PairedAlignment.java index 831d9e1bb2..0c69560527 100644 --- a/src/main/java/org/broad/igv/sam/PairedAlignment.java +++ b/src/main/java/org/broad/igv/sam/PairedAlignment.java @@ -195,19 +195,6 @@ public String getAlignmentValueString(double position, int mouseX, AlignmentTrac return buf.toString(); } - - public String getClipboardString(double position, int mouseX) { - StringBuffer buf = new StringBuffer(); - buf.append("Left alignment
"); - buf.append(firstAlignment.getClipboardString(position, mouseX)); - if (secondAlignment != null) { - buf.append("
Right alignment
"); - buf.append(secondAlignment.getClipboardString(position, mouseX)); - } - return buf.toString(); - } - //////////////////////////////////////////////////////////// - public boolean contains(double location) { return location >= start && location <= end; } diff --git a/src/main/java/org/broad/igv/sam/ReducedMemoryAlignment.java b/src/main/java/org/broad/igv/sam/ReducedMemoryAlignment.java index 24421b73d9..041024bbfc 100644 --- a/src/main/java/org/broad/igv/sam/ReducedMemoryAlignment.java +++ b/src/main/java/org/broad/igv/sam/ReducedMemoryAlignment.java @@ -206,10 +206,6 @@ public LocusScore copy() { return this; } - public String getClipboardString(double location, int mouseX) { - return getValueString(location, mouseX, (WindowFunction) null); - } - public String getValueString(double position, int mouseX, WindowFunction ignored) { StringBuffer buf = new StringBuffer(); diff --git a/src/main/java/org/broad/igv/sam/SAMAlignment.java b/src/main/java/org/broad/igv/sam/SAMAlignment.java index 5c7a0eb6be..81b5b8a9f0 100644 --- a/src/main/java/org/broad/igv/sam/SAMAlignment.java +++ b/src/main/java/org/broad/igv/sam/SAMAlignment.java @@ -34,6 +34,7 @@ import htsjdk.samtools.SAMReadGroupRecord; import htsjdk.samtools.SAMRecord; import htsjdk.samtools.SAMTag; +import htsjdk.samtools.util.SequenceUtil; import org.broad.igv.logging.*; import org.broad.igv.Globals; import org.broad.igv.feature.Strand; @@ -658,7 +659,6 @@ private static List buildOperators(String cigarString) { prevOp = new CigarOperator(nBases, op); operators.add(prevOp); } - } } return operators; @@ -668,7 +668,22 @@ private static List buildOperators(String cigarString) { @Override public String getClipboardString(double location, int mouseX) { - return getAlignmentValueString(location, mouseX, null); + + StringBuilder buf = new StringBuilder(); + + String popupTextString = getAlignmentValueString(location, mouseX, null); + buf.append(popupTextString); + + buf.append("----------------------"); + final String readSequence = getReadSequence(); + final String origSequence = isNegativeStrand() ? SequenceUtil.reverseComplement(readSequence) : readSequence; + buf.append("\n\nRead sequence = "); + buf.append(readSequence); + buf.append("\n\nOrig sequence = "); + buf.append(origSequence); + buf.append("\n"); + + return buf.toString(); } private Integer positionToReadIndex(double position) { @@ -697,6 +712,7 @@ public String getAlignmentValueString(double position, int mouseX, AlignmentTrac boolean atInsertion = false; boolean atBaseMod = false; + // First check insertions. Position is zero based, block coords 1 based if (this.insertions != null) { for (AlignmentBlock block : this.insertions) { diff --git a/src/test/java/org/broad/igv/sam/BisulfiteBaseInfoTest.java b/src/test/java/org/broad/igv/sam/BisulfiteBaseInfoTest.java index a0cd9636e0..5e8103df98 100644 --- a/src/test/java/org/broad/igv/sam/BisulfiteBaseInfoTest.java +++ b/src/test/java/org/broad/igv/sam/BisulfiteBaseInfoTest.java @@ -315,9 +315,5 @@ public String getLibrary() { return library; } - @Override - public String getClipboardString(double location, int mouseX) { - return null; - } } } From 07a8c59ef348f78b72e2733f65d7a6b39fe28fce Mon Sep 17 00:00:00 2001 From: Jim Robinson <933148+jrobinso@users.noreply.github.com> Date: Mon, 18 Nov 2024 12:07:02 -0800 Subject: [PATCH 076/130] GFF3 parsing error -- fixes #1617 (#1618) --- src/main/java/org/broad/igv/feature/Exon.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/broad/igv/feature/Exon.java b/src/main/java/org/broad/igv/feature/Exon.java index d23b38dab6..a19521dc51 100644 --- a/src/main/java/org/broad/igv/feature/Exon.java +++ b/src/main/java/org/broad/igv/feature/Exon.java @@ -229,12 +229,14 @@ private void computeAminoAcidSequence(Genome genome, Exon prevExon, Exon nextExo // Grab nucleotides from next exon if needed for last codon int phase = (3 - readingFrame) % 3; int diff = 3 - ((readEnd - (codingStart + phase)) % 3); - if (diff > 0 && nextExon != null) { + if (diff > 0 && diff < 3 && nextExon != null && !nextExon.isNonCoding()) { byte[] d = genome.getSequence(chr, nextExon.getCdStart(), nextExon.getCdStart() + diff); - byte[] tmp = new byte[d.length + seqBytes.length]; - System.arraycopy(seqBytes, 0, tmp, 0, seqBytes.length); - System.arraycopy(d, 0, tmp, seqBytes.length, d.length); - seqBytes = tmp; + if(d != null) { + byte[] tmp = new byte[d.length + seqBytes.length]; + System.arraycopy(seqBytes, 0, tmp, 0, seqBytes.length); + System.arraycopy(d, 0, tmp, seqBytes.length, d.length); + seqBytes = tmp; + } } } else { From 004130eafc629643a6c268ad5b831f325872bcdc Mon Sep 17 00:00:00 2001 From: Jim Robinson <933148+jrobinso@users.noreply.github.com> Date: Tue, 19 Nov 2024 11:55:38 -0800 Subject: [PATCH 077/130] Add case-insensitive search to chrom aliasing code (#1621) --- .../igv/feature/genome/ChromAliasBB.java | 26 ------------------- .../feature/genome/ChromAliasDefaults.java | 11 -------- .../igv/feature/genome/ChromAliasFile.java | 10 ------- .../igv/feature/genome/ChromAliasSource.java | 2 -- .../org/broad/igv/feature/genome/Genome.java | 10 ++++++- .../igv/feature/genome/ChromAliasBBTest.java | 6 ++--- .../genome/ChromAliasDefaultsTest.java | 10 ++++--- .../feature/genome/ChromAliasFileTest.java | 6 ++--- 8 files changed, 21 insertions(+), 60 deletions(-) diff --git a/src/main/java/org/broad/igv/feature/genome/ChromAliasBB.java b/src/main/java/org/broad/igv/feature/genome/ChromAliasBB.java index da99a698c4..27260fbbae 100644 --- a/src/main/java/org/broad/igv/feature/genome/ChromAliasBB.java +++ b/src/main/java/org/broad/igv/feature/genome/ChromAliasBB.java @@ -3,8 +3,6 @@ import org.broad.igv.feature.BasicFeature; import org.broad.igv.ucsc.bb.BBFile; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; public class ChromAliasBB extends ChromAliasSource { @@ -16,30 +14,6 @@ public ChromAliasBB(String path, Genome genome) throws IOException { } - /** - * Return the canonical chromosome name for the alias. If none found return the alias - * - * @param alias - * @returns {*} - */ - public String getChromosomeName(String alias) { - if(aliasCache.containsKey(alias)) { - return aliasCache.get(alias).getChr(); - } else { - try { - ChromAlias aliasRecord = search(alias); - if(aliasRecord == null) { - aliasRecord.put(alias, null); // Prevents future attempts - } else { - return aliasRecord.getChr(); - } - } catch (IOException e) { - // TODO -- log - } - } - return null; - } - /** * Return an alternate chromosome name (alias). * diff --git a/src/main/java/org/broad/igv/feature/genome/ChromAliasDefaults.java b/src/main/java/org/broad/igv/feature/genome/ChromAliasDefaults.java index f930e43539..7f4130a794 100644 --- a/src/main/java/org/broad/igv/feature/genome/ChromAliasDefaults.java +++ b/src/main/java/org/broad/igv/feature/genome/ChromAliasDefaults.java @@ -118,17 +118,6 @@ private void init(String id, List chromosomeNames) { } } - /** - * Return the canonical chromosome name for the alias. If none found return the alias - * - * @param alias - * @returns {*} - */ - @Override - public String getChromosomeName(String alias) { - return this.aliasCache.containsKey(alias) ? this.aliasCache.get(alias).getChr() : alias; - } - /** * Return an alternate chromosome name (alias). * diff --git a/src/main/java/org/broad/igv/feature/genome/ChromAliasFile.java b/src/main/java/org/broad/igv/feature/genome/ChromAliasFile.java index 7189d12069..9602e170c0 100644 --- a/src/main/java/org/broad/igv/feature/genome/ChromAliasFile.java +++ b/src/main/java/org/broad/igv/feature/genome/ChromAliasFile.java @@ -99,16 +99,6 @@ public ChromAliasFile(List> chromAliases, List chromosomeNa } - /** - * Return the canonical chromosome name for the alias. If none found return the alias - * - * @param alias - * @returns {*} - */ - public String getChromosomeName(String alias) { - return this.aliasCache.containsKey(alias) ? this.aliasCache.get(alias).getChr() : alias; - } - /** * Return an alternate chromosome name (alias). * diff --git a/src/main/java/org/broad/igv/feature/genome/ChromAliasSource.java b/src/main/java/org/broad/igv/feature/genome/ChromAliasSource.java index 1992f289cd..d8a24d923d 100644 --- a/src/main/java/org/broad/igv/feature/genome/ChromAliasSource.java +++ b/src/main/java/org/broad/igv/feature/genome/ChromAliasSource.java @@ -12,8 +12,6 @@ public ChromAliasSource() { aliasCache = new HashMap<>(); } - public abstract String getChromosomeName(String alias); - public abstract String getChromosomeAlias(String chr, String nameSet); public abstract ChromAlias search(String alias) throws IOException; diff --git a/src/main/java/org/broad/igv/feature/genome/Genome.java b/src/main/java/org/broad/igv/feature/genome/Genome.java index 64da4e7e70..2d5772d8bb 100644 --- a/src/main/java/org/broad/igv/feature/genome/Genome.java +++ b/src/main/java/org/broad/igv/feature/genome/Genome.java @@ -293,16 +293,24 @@ private void addTracks(GenomeConfig config) { public String getCanonicalChrName(String str) { if (str == null) { return str; - } else if (chrAliasCache.containsKey(str)) { + } + + if (chrAliasCache.containsKey(str)) { return chrAliasCache.get(str); } else if (chromAliasSource != null) { try { ChromAlias aliasRecord = chromAliasSource.search(str); + + if(aliasRecord == null && !str.equals(str.toLowerCase())) { + aliasRecord = chromAliasSource.search(str.toLowerCase()); + } + if (aliasRecord != null) { String chr = aliasRecord.getChr(); for (String a : aliasRecord.values()) { chrAliasCache.put(a, chr); } + chrAliasCache.put(str, chr); // Usually redundant, but will catch lowercase search case return chr; } } catch (IOException e) { diff --git a/src/test/java/org/broad/igv/feature/genome/ChromAliasBBTest.java b/src/test/java/org/broad/igv/feature/genome/ChromAliasBBTest.java index f39f8316f9..1e7d4d9c6c 100644 --- a/src/test/java/org/broad/igv/feature/genome/ChromAliasBBTest.java +++ b/src/test/java/org/broad/igv/feature/genome/ChromAliasBBTest.java @@ -23,9 +23,9 @@ public static void setup() { public void getChromosomeName() throws IOException { String path = "test/data/genomes/GCF_000002655.1.chromAlias.bb"; ChromAliasSource chromAlias = new ChromAliasBB(path, mockGenome); - assertEquals("NC_007194.1", chromAlias.getChromosomeName("CM000169.1")) ; - assertEquals("NC_007194.1", chromAlias.getChromosomeName( "1")); - assertEquals("NC_007194.1", chromAlias.getChromosomeName( "chr1")); + assertEquals("NC_007194.1", chromAlias.search("CM000169.1").getChr()); + assertEquals("NC_007194.1", chromAlias.search( "1").getChr()); + assertEquals("NC_007194.1", chromAlias.search( "chr1").getChr()); } @Test diff --git a/src/test/java/org/broad/igv/feature/genome/ChromAliasDefaultsTest.java b/src/test/java/org/broad/igv/feature/genome/ChromAliasDefaultsTest.java index a941d7bfa6..cf3298e87d 100644 --- a/src/test/java/org/broad/igv/feature/genome/ChromAliasDefaultsTest.java +++ b/src/test/java/org/broad/igv/feature/genome/ChromAliasDefaultsTest.java @@ -48,11 +48,13 @@ public static void setup() { @Test public void getCanonicalChromosomeName() { - assertEquals(ncibMockGenome.getCanonicalChrName("chrX"), "23"); - assertEquals(ncibMockGenome.getCanonicalChrName("chrM"), "MT"); + assertEquals("23", ncibMockGenome.getCanonicalChrName("chrX")); + assertEquals("MT", ncibMockGenome.getCanonicalChrName("chrM")); - assertEquals(ucscMockGenome.getCanonicalChrName("23"), "chrX"); - assertEquals(ucscMockGenome.getCanonicalChrName("MT"), "chrM"); + assertEquals("chrX", ucscMockGenome.getCanonicalChrName("23")); + assertEquals("chrM", ucscMockGenome.getCanonicalChrName("MT")); + + assertEquals("chr1", ucscMockGenome.getCanonicalChrName("Chr1")); } diff --git a/src/test/java/org/broad/igv/feature/genome/ChromAliasFileTest.java b/src/test/java/org/broad/igv/feature/genome/ChromAliasFileTest.java index c4f1e32087..1346a9c774 100644 --- a/src/test/java/org/broad/igv/feature/genome/ChromAliasFileTest.java +++ b/src/test/java/org/broad/igv/feature/genome/ChromAliasFileTest.java @@ -25,9 +25,9 @@ public static void setup() { public void getChromosomeName() throws IOException { String path = TestUtils.DATA_DIR + "genomes/GCF_000002655.1.chromAlias.txt"; ChromAliasFile chromAlias = new ChromAliasFile(path, mockGenome.getChromosomeNames()); - assertEquals("NC_007194.1", chromAlias.getChromosomeName("CM000169.1")) ; - assertEquals("NC_007194.1", chromAlias.getChromosomeName( "1")); - assertEquals("NC_007194.1", chromAlias.getChromosomeName( "chr1")); + assertEquals("NC_007194.1", chromAlias.search("CM000169.1").getChr()) ; + assertEquals("NC_007194.1", chromAlias.search( "1").getChr()); + assertEquals("NC_007194.1", chromAlias.search( "chr1").getChr()); } @Test From 5a9d821d83b3c0c60d5c4158ea6d4324494cf2f3 Mon Sep 17 00:00:00 2001 From: Louis Bergelson Date: Tue, 19 Nov 2024 18:21:22 -0500 Subject: [PATCH 078/130] Add Recent Files Menu and Improve Recent Sessions (#1616) * Refactoring of the load files menu option # Conflicts: # src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java * Made the list of recent sessions update dynamically as sessions are create/loaded * previously it only updated once when IGV was loaded * Adding a "Recent Files" menu option * This dynamically tracks the most recently loaded files/urls and allows reopening them * Fixes #https://github.com/igvteam/igv/issues/1547 * Add comments and MenuSelectedListener * Fix null check --- .../java/org/broad/igv/prefs/Constants.java | 1 + .../org/broad/igv/prefs/IGVPreferences.java | 26 ++- .../DynamicMenuItemsAdjustmentListener.java | 76 ++++++++ src/main/java/org/broad/igv/ui/IGV.java | 77 +++++--- .../java/org/broad/igv/ui/IGVMenuBar.java | 120 +++--------- .../broad/igv/ui/MenuSelectedListener.java | 19 ++ .../java/org/broad/igv/ui/RecentFileSet.java | 45 +++++ .../java/org/broad/igv/ui/RecentUrlsSet.java | 75 +++++++ src/main/java/org/broad/igv/ui/StackSet.java | 137 +++++++++++++ .../igv/ui/action/LoadFilesMenuAction.java | 46 ++--- .../igv/ui/action/LoadFromURLMenuAction.java | 184 +++++++++--------- .../igv/ui/action/OpenSessionMenuAction.java | 25 ++- .../org/broad/igv/ui/util/AutosaveMenu.java | 22 +-- .../org/broad/igv/ui/util/HistoryMenu.java | 13 +- .../broad/igv/ui/util/LoadFromURLDialog.java | 23 ++- .../org/broad/igv/ui/util/RecentUrlsMenu.java | 71 +++++++ .../org/broad/igv/util/ResourceLocator.java | 4 +- .../broad/igv/util/collections/LRUCache.java | 4 - .../broad/igv/sam/BisulfiteBaseInfoTest.java | 2 + .../org/broad/igv/ui/RecentFileSetTest.java | 56 ++++++ .../org/broad/igv/ui/RecentUrlsSetTest.java | 57 ++++++ .../java/org/broad/igv/ui/StackSetTest.java | 95 +++++++++ .../java/org/broad/igv/util/TestUtils.java | 1 + 23 files changed, 881 insertions(+), 298 deletions(-) create mode 100644 src/main/java/org/broad/igv/ui/DynamicMenuItemsAdjustmentListener.java create mode 100644 src/main/java/org/broad/igv/ui/MenuSelectedListener.java create mode 100644 src/main/java/org/broad/igv/ui/RecentFileSet.java create mode 100644 src/main/java/org/broad/igv/ui/RecentUrlsSet.java create mode 100644 src/main/java/org/broad/igv/ui/StackSet.java create mode 100644 src/main/java/org/broad/igv/ui/util/RecentUrlsMenu.java create mode 100644 src/test/java/org/broad/igv/ui/RecentFileSetTest.java create mode 100644 src/test/java/org/broad/igv/ui/RecentUrlsSetTest.java create mode 100644 src/test/java/org/broad/igv/ui/StackSetTest.java diff --git a/src/main/java/org/broad/igv/prefs/Constants.java b/src/main/java/org/broad/igv/prefs/Constants.java index f87eb6c403..28f12ead80 100644 --- a/src/main/java/org/broad/igv/prefs/Constants.java +++ b/src/main/java/org/broad/igv/prefs/Constants.java @@ -42,6 +42,7 @@ private Constants() { // public static final String RECENT_SESSIONS = "IGV.Session.recent.sessions"; + public static final String RECENT_URLS = "IGV.Session.recent.urls"; public static final String LAST_EXPORTED_REGION_DIRECTORY = "LAST_EXPORTED_REGION_DIRECTORY"; public static final String LAST_TRACK_DIRECTORY = "LAST_TRACK_DIRECTORY"; public static final String LAST_SNAPSHOT_DIRECTORY = "LAST_SNAPSHOT_DIRECTORY"; diff --git a/src/main/java/org/broad/igv/prefs/IGVPreferences.java b/src/main/java/org/broad/igv/prefs/IGVPreferences.java index 58cafca3d6..497b9e2ca6 100644 --- a/src/main/java/org/broad/igv/prefs/IGVPreferences.java +++ b/src/main/java/org/broad/igv/prefs/IGVPreferences.java @@ -44,9 +44,7 @@ import org.broad.igv.renderer.SequenceRenderer; import org.broad.igv.sam.mods.BaseModificationColors; import org.broad.igv.track.TrackType; -import org.broad.igv.ui.IGV; -import org.broad.igv.ui.IGVMenuBar; -import org.broad.igv.ui.UIConstants; +import org.broad.igv.ui.*; import org.broad.igv.ui.color.ColorUtilities; import org.broad.igv.ui.color.PaletteColorTable; import org.broad.igv.ui.util.MessageUtils; @@ -280,7 +278,7 @@ public void put(String key, String value) { // Explicitly setting removes override overrideKeys.remove(key); - if (value == null || value.trim().length() == 0) { + if (value == null || value.isBlank()) { userPreferences.remove(key); } else { userPreferences.put(key, value); @@ -297,7 +295,7 @@ public void put(String key, boolean b) { public void putAll(Map updatedPrefs) { for (Map.Entry entry : updatedPrefs.entrySet()) { - if (entry.getValue() == null || entry.getValue().trim().length() == 0) { + if (entry.getValue() == null || entry.getValue().isBlank()) { remove(entry.getKey()); } else { put(entry.getKey(), entry.getValue()); @@ -610,14 +608,28 @@ public Rectangle getApplicationFrameBounds() { * @param recentSessions */ public void setRecentSessions(String recentSessions) { + remove(RECENT_SESSIONS); put(RECENT_SESSIONS, recentSessions); } - public String getRecentSessions() { - return get(RECENT_SESSIONS, null); + public RecentFileSet getRecentSessions() { + String sessionsString = get(RECENT_SESSIONS, null); + return RecentFileSet.fromString(sessionsString, UIConstants.NUMBER_OF_RECENT_SESSIONS_TO_LIST); } + public void setRecentUrls(String recentUrls) { + remove(RECENT_URLS); + put(RECENT_URLS, recentUrls); + } + + + public RecentUrlsSet getRecentUrls() { + String sessionsString = get(RECENT_URLS, null); + return RecentUrlsSet.fromString(sessionsString, UIConstants.NUMBER_OF_RECENT_SESSIONS_TO_LIST); + } + + public String getDataServerURL() { String masterResourceFile = get(DATA_SERVER_URL_KEY); return masterResourceFile; diff --git a/src/main/java/org/broad/igv/ui/DynamicMenuItemsAdjustmentListener.java b/src/main/java/org/broad/igv/ui/DynamicMenuItemsAdjustmentListener.java new file mode 100644 index 0000000000..b6b12d9996 --- /dev/null +++ b/src/main/java/org/broad/igv/ui/DynamicMenuItemsAdjustmentListener.java @@ -0,0 +1,76 @@ +package org.broad.igv.ui; + +import javax.swing.*; +import javax.swing.event.MenuEvent; +import javax.swing.event.MenuListener; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.function.Function; + +/** + * MenuListener which updates the availble menu items based on the contents of a changeable collection. + * + * Whenever the menu is selected the relevant JMenuItems are regenerated according to the current values in the backing + * collection. + * + * Items are inserted in a row beneath a given separator. + * + * @param element type of the collection + */ +public class DynamicMenuItemsAdjustmentListener implements MenuSelectedListener { + private final JMenu menu; + + private final JSeparator insertionPoint; + private final Collection values; + private final Function itemConstructor; + + //Store the currently visible components here so they can be removed when necessary + private final List activeComponents; + + /** + * + * @param menu the menu to modify + * @param insertionPoint a JSeparator which acts as an anchor to always insert elements below. This element is hidden + * when the collection is empty + * @param values a collection which is used to generate menu items + * @param itemConstructor a function to create a JMenuItem from an element in the collection + */ + public DynamicMenuItemsAdjustmentListener(JMenu menu, JSeparator insertionPoint, Collection values, Function itemConstructor) { + this.menu = menu; + this.insertionPoint = insertionPoint; + this.values = values; + this.itemConstructor = itemConstructor; + this.activeComponents = new ArrayList<>(); + } + + private List getCurrentItems() { + return values.stream().map(itemConstructor).toList(); + } + + @Override + public void menuSelected(MenuEvent e) { + List newComponents = getCurrentItems(); + + // We definitely don't want to be doing this while other things are also changing the menu + // this should at least protect against multiple of these listeners modifying the same menu at once. + synchronized (menu) { + activeComponents.forEach(menu::remove); + if (newComponents.isEmpty()) { + insertionPoint.setVisible(false); + } else { + insertionPoint.setVisible(true); + + final int componentIndex = Arrays.asList(menu.getMenuComponents()).indexOf(insertionPoint); + for (int i = 0; i < newComponents.size(); i++) { + menu.insert(newComponents.get(i), componentIndex + i + 1); + } + activeComponents.addAll(newComponents); + } + } + + menu.revalidate(); + menu.repaint(); + } +} diff --git a/src/main/java/org/broad/igv/ui/IGV.java b/src/main/java/org/broad/igv/ui/IGV.java index 9b71424e3e..ba830bdd58 100644 --- a/src/main/java/org/broad/igv/ui/IGV.java +++ b/src/main/java/org/broad/igv/ui/IGV.java @@ -134,9 +134,10 @@ public class IGV implements IGVEventObserver { private Timer sessionAutosaveTimer = new Timer(); // Misc state - private Map> overlayTracksMap = new HashMap(); - private Set overlaidTracks = new HashSet(); - private LinkedList recentSessionList = new LinkedList(); + private Map> overlayTracksMap = new HashMap<>(); + private Set overlaidTracks = new HashSet<>(); + private RecentFileSet recentSessionList; + private RecentUrlsSet recentUrlsList; // Vertical line that follows the mouse private boolean rulerEnabled; @@ -514,25 +515,15 @@ final public void doViewPreferences() { 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 += ";"; - } + RecentFileSet recentSessions = getRecentSessionList(); + if (!recentSessions.isEmpty()) { + PreferencesManager.getPreferences().setRecentSessions(recentSessions.asString()); + } - } - PreferencesManager.getPreferences().remove(RECENT_SESSIONS); - PreferencesManager.getPreferences().setRecentSessions(recentSessions); + // Store recent files + RecentUrlsSet recentUrls = getRecentUrls(); + if (!recentUrls.isEmpty()) { + PreferencesManager.getPreferences().setRecentUrls(recentUrls.asString()); } // Stop the timer that is triggering the timed autosave @@ -991,9 +982,8 @@ public boolean loadSession(String sessionPath, String locus) { } mainFrame.setTitle(UIConstants.APPLICATION_NAME + " - Session: " + sessionPath); - if (!recentSessionList.contains(sessionPath)) { - recentSessionList.addFirst(sessionPath); - } + + getRecentSessionList().add(sessionPath); this.menuBar.enableReloadSession(); //If there's a RegionNavigatorDialog, kill it. @@ -1006,7 +996,7 @@ public boolean loadSession(String sessionPath, String locus) { } catch (Exception e) { String message = "Error loading session session: " + e.getMessage(); MessageUtils.showMessage(message); - recentSessionList.remove(sessionPath); + getRecentSessionList().remove(sessionPath); log.error(e); return false; } finally { @@ -1068,9 +1058,8 @@ public void saveSession(File targetFile) throws IOException { String sessionPath = targetFile.getAbsolutePath(); session.setPath(sessionPath); mainFrame.setTitle(UIConstants.APPLICATION_NAME + " - Session: " + sessionPath); - if (!recentSessionList.contains(sessionPath)) { - recentSessionList.addFirst(sessionPath); - } + + getRecentSessionList().add(sessionPath); this.menuBar.enableReloadSession(); // No errors so save last location @@ -1130,10 +1119,36 @@ public MainPanel getMainPanel() { return contentPane.getMainPanel(); } - public LinkedList getRecentSessionList() { + public RecentFileSet getRecentSessionList() { + if(recentSessionList == null){ + recentSessionList = PreferencesManager.getPreferences().getRecentSessions(); + //remove sessions that no longer exist + recentSessionList.removeIf(file -> !(new File(file)).exists()); + } return recentSessionList; } + public RecentUrlsSet getRecentUrls() { + if(recentUrlsList == null){ + recentUrlsList = PreferencesManager.getPreferences().getRecentUrls(); + } + return recentUrlsList; + } + + /** + * Add new values to the recent URLS set. Calling this method rather than adding them directly + * allows showing the menu when the first URL is added to the collection. + * @param toAdd + */ + public void addToRecentUrls(Collection toAdd){ + RecentUrlsSet recentFiles = getRecentUrls(); + recentFiles.addAll(toAdd); + if(!recentFiles.isEmpty()){ + menuBar.showRecentFilesMenu(); + } + } + + public IGVContentPane getContentPane() { return contentPane; } @@ -1173,7 +1188,7 @@ public void loadResources(Collection locators) { for (final ResourceLocator locator : locators) { - // If its a local file, check explicitly for existence (rather than rely on exception) + // If it's a local file, check explicitly for existence (rather than rely on exception) if (locator.isLocal()) { File trackSetFile = new File(locator.getPath()); if (!trackSetFile.exists()) { @@ -1182,9 +1197,11 @@ public void loadResources(Collection locators) { } } + try { List tracks = load(locator); addTracks(tracks); + } catch (Exception e) { log.error("Error loading track", e); messages.append("Error loading " + locator + ": " + e.getMessage()); diff --git a/src/main/java/org/broad/igv/ui/IGVMenuBar.java b/src/main/java/org/broad/igv/ui/IGVMenuBar.java index c97b9e1424..6e36bc4496 100644 --- a/src/main/java/org/broad/igv/ui/IGVMenuBar.java +++ b/src/main/java/org/broad/igv/ui/IGVMenuBar.java @@ -36,7 +36,6 @@ import org.broad.igv.event.IGVEventBus; import org.broad.igv.event.IGVEventObserver; import org.broad.igv.feature.genome.Genome; -import org.broad.igv.feature.genome.GenomeDownloadUtils; import org.broad.igv.feature.genome.GenomeManager; import org.broad.igv.feature.genome.ChromSizesUtils; import org.broad.igv.track.AttributeManager; @@ -70,15 +69,13 @@ import javax.swing.event.MenuEvent; import javax.swing.event.MenuListener; import javax.swing.plaf.basic.BasicBorders; -import javax.swing.plaf.basic.BasicMenuItemUI; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.io.*; import java.net.URI; -import java.util.ArrayList; -import java.util.Collection; +import java.util.*; import java.util.List; import static org.broad.igv.prefs.Constants.*; @@ -114,10 +111,12 @@ public class IGVMenuBar extends JMenuBar implements IGVEventObserver { private JMenuItem loadGenomeFromServerMenuItem; private JMenuItem loadTracksFromServerMenuItem; private JMenuItem selectGenomeAnnotationsItem; - private JMenuItem encodeUCSCMenuItem; + private JMenuItem encodeUCSCMenuItem; private List encodeMenuItems = new ArrayList<>(); + private JMenuItem reloadSessionItem; + private JMenuItem recentFilesMenu; static IGVMenuBar createInstance(IGV igv) { @@ -299,6 +298,9 @@ JMenu createFileMenu() { loadTracksFromServerMenuItem = MenuAndToolbarUtils.createMenuItem(menuAction); menuItems.add(loadTracksFromServerMenuItem); + recentFilesMenu = new RecentUrlsMenu(); + menuItems.add(recentFilesMenu); + if (PreferencesManager.getPreferences().getAsBoolean(DB_ENABLED)) { menuAction = new LoadFromDatabaseAction("Load from Database...", 0, igv); menuItems.add(MenuAndToolbarUtils.createMenuItem(menuAction)); @@ -407,39 +409,22 @@ public void actionPerformed(ActionEvent e) { menuAction.setToolTipText(EXIT_TOOLTIP); menuItems.add(MenuAndToolbarUtils.createMenuItem(menuAction)); - - - // Empty the recent sessions list before we start to do - // anything with it - igv.getRecentSessionList().clear(); - - // Retrieve the stored session paths - String recentSessions = PreferencesManager.getPreferences().getRecentSessions(); - if (recentSessions != null) { - String[] sessions = recentSessions.split(";"); - for (String sessionPath : sessions) { - if (!sessionPath.equals("null") && - !igv.getRecentSessionList().contains(sessionPath) && - (new File(sessionPath)).exists()) { - igv.getRecentSessionList().add(sessionPath); - } - - } - } - - if (!igv.getRecentSessionList().isEmpty()) { - menuItems.add(new JSeparator()); - // Now add menu items - for (final String session : igv.getRecentSessionList()) { - OpenSessionMenuAction osMenuAction = new OpenSessionMenuAction(session, IGV.getInstance()); - menuItems.add(MenuAndToolbarUtils.createMenuItem(osMenuAction)); - } - - } - + JSeparator recentSessionsSep = new JSeparator(); + recentSessionsSep.setVisible(false); + menuItems.add(recentSessionsSep); + //menuItems.addAll(addRecentSessionMenuItems()); + menuItems.add(new JSeparator());; MenuAction fileMenuAction = new MenuAction("File", null, KeyEvent.VK_F); JMenu fileMenu = MenuAndToolbarUtils.createMenu(menuItems, fileMenuAction); + //Add dynamic list of recent sessions + fileMenu.addMenuListener(new DynamicMenuItemsAdjustmentListener<>( + fileMenu, + recentSessionsSep, + IGV.getInstance().getRecentSessionList(), + session -> MenuAndToolbarUtils.createMenuItem(new OpenSessionMenuAction(session, IGV.getInstance()))) + ); + return fileMenu; } @@ -539,22 +524,9 @@ public void actionPerformed(ActionEvent event) { menuAction.setToolTipText("Remove genomes which appear in the dropdown list"); menu.add(MenuAndToolbarUtils.createMenuItem(menuAction)); - menu.addMenuListener(new MenuListener() { - @Override - public void menuSelected(MenuEvent e) { - Genome genome = GenomeManager.getInstance().getCurrentGenome(); - selectGenomeAnnotationsItem.setEnabled(genome != null && genome.getHub() != null); - } - - @Override - public void menuDeselected(MenuEvent e) { - - } - - @Override - public void menuCanceled(MenuEvent e) { - - } + menu.addMenuListener((MenuSelectedListener) e -> { + Genome genome1 = GenomeManager.getInstance().getCurrentGenome(); + selectGenomeAnnotationsItem.setEnabled(genome1 != null && genome1.getHub() != null); }); return menu; @@ -1060,7 +1032,7 @@ private JMenu createAWSMenu() { loadS3.setEnabled(!usingCognito); // If using Cognito, disalbe initially menu.add(loadS3); - menu.addMenuListener(new MenuListener() { + menu.addMenuListener(new MenuSelectedListener() { @Override public void menuSelected(MenuEvent e) { if (AmazonUtils.GetCognitoConfig() != null) { @@ -1083,14 +1055,6 @@ public void menuSelected(MenuEvent e) { LongRunningTask.submit(runnable); } } - - @Override - public void menuDeselected(MenuEvent e) { - } - - @Override - public void menuCanceled(MenuEvent e) { - } }); @@ -1132,7 +1096,7 @@ private JMenu createGoogleMenu() { projectID.addActionListener(e -> GoogleUtils.enterGoogleProjectID()); googleMenu.add(projectID); - googleMenu.addMenuListener(new MenuListener() { + googleMenu.addMenuListener(new MenuSelectedListener() { @Override public void menuSelected(MenuEvent e) { boolean loggedIn = googleProvider.isLoggedIn(); @@ -1144,17 +1108,6 @@ public void menuSelected(MenuEvent e) { login.setEnabled(!loggedIn); logout.setEnabled(loggedIn); } - - @Override - public void menuDeselected(MenuEvent e) { - - } - - @Override - public void menuCanceled(MenuEvent e) { - - } - }); return googleMenu; @@ -1253,6 +1206,10 @@ public void enableReloadSession() { this.reloadSessionItem.setEnabled(true); } + public void showRecentFilesMenu(){ + this.recentFilesMenu.setVisible(true); + } + public void disableReloadSession() { this.reloadSessionItem.setEnabled(false); } @@ -1289,9 +1246,7 @@ private void exportTrackNames(final Collection selectedTracks) { return; } - PrintWriter pw = null; - try { - pw = new PrintWriter(new BufferedWriter(new FileWriter(file))); + try (PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(file)))) { List attributes = AttributeManager.getInstance().getVisibleAttributes(); @@ -1311,24 +1266,11 @@ private void exportTrackNames(final Collection selectedTracks) { } pw.println(); } - - } catch (IOException e) { MessageUtils.showErrorMessage("Error writing to file", e); log.error(e); - } finally { - if (pw != null) pw.close(); } - } -} - -class Foo extends BasicMenuItemUI { - - - public Foo() { - this.disabledForeground = Color.black; - } - } + diff --git a/src/main/java/org/broad/igv/ui/MenuSelectedListener.java b/src/main/java/org/broad/igv/ui/MenuSelectedListener.java new file mode 100644 index 0000000000..831f05dcdb --- /dev/null +++ b/src/main/java/org/broad/igv/ui/MenuSelectedListener.java @@ -0,0 +1,19 @@ +package org.broad.igv.ui; + +import javax.swing.event.MenuEvent; +import javax.swing.event.MenuListener; + +/** + * A menu listener which provides default noop implementations of menuDeselected and menuCancelled + */ +public interface MenuSelectedListener extends MenuListener { + + @Override + void menuSelected(MenuEvent e); + + @Override + default void menuDeselected(MenuEvent e) {} + + @Override + default void menuCanceled(MenuEvent e) {} +} diff --git a/src/main/java/org/broad/igv/ui/RecentFileSet.java b/src/main/java/org/broad/igv/ui/RecentFileSet.java new file mode 100644 index 0000000000..dcd27e171c --- /dev/null +++ b/src/main/java/org/broad/igv/ui/RecentFileSet.java @@ -0,0 +1,45 @@ +package org.broad.igv.ui; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +/** + * An implement of StackSet for local file paths which supports serializing/deserializing itself to a string. + * + * The serialized form matches the format which already existed to store recent session files. + * This is used by the by the + */ +public class RecentFileSet extends StackSet { + + private static final String DELIMITER = ";"; + + public RecentFileSet(int maxSize) { + super(maxSize); + } + + public RecentFileSet(Collection c, int maxSize) { + super(c, maxSize); + } + + public String asString() { + return String.join(DELIMITER, this); + } + + public static RecentFileSet fromString(String string, int maxSize) { + if(string == null || string.isBlank()){ + return new RecentFileSet(maxSize); + } + String[] files = string.split(DELIMITER); + List fileList = Arrays.stream(files) + .filter(s -> !s.isBlank()) + // "null" was previously accounted for in older code so it's handled here + // it doesn't seem like it should be possible to produce now though + .filter(s -> !s.equals("null")) + .map(String::strip) + .toList(); + return new RecentFileSet(fileList, maxSize); + } + + +} diff --git a/src/main/java/org/broad/igv/ui/RecentUrlsSet.java b/src/main/java/org/broad/igv/ui/RecentUrlsSet.java new file mode 100644 index 0000000000..e5019a0927 --- /dev/null +++ b/src/main/java/org/broad/igv/ui/RecentUrlsSet.java @@ -0,0 +1,75 @@ +package org.broad.igv.ui; + +import org.broad.igv.ui.action.LoadFilesMenuAction; +import org.broad.igv.ui.action.LoadFromURLMenuAction; +import org.broad.igv.util.ResourceLocator; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * An implementation of StackSet which supports {@link ResourceLocator}s which include a path and an index + * This matches the form used by the {@link LoadFilesMenuAction} and {@link LoadFromURLMenuAction} + * + * + */ +public class RecentUrlsSet extends StackSet { + private static final String INDEX_DELIM = "index:"; + private static final Pattern INDEX_SPLITTER = Pattern.compile("\\s" + INDEX_DELIM); + + public RecentUrlsSet(int maxSize) { + super(maxSize); + } + + public RecentUrlsSet(Collection c, int maxSize) { + super(c, maxSize); + } + + public String asString(){ + return this.stream() + .map(RecentUrlsSet::locatorToString) + .collect(Collectors.joining("|")); + } + + private static String locatorToString(ResourceLocator locator) { + StringBuilder builder = new StringBuilder(); + builder.append(locator.getPath()); + if(locator.getIndexPath() != null) { + builder.append(" " + INDEX_DELIM); + builder.append(locator.getIndexPath()); + } + return builder.toString(); + } + + private static ResourceLocator stringToLocator(String locationString) { + String[] split = INDEX_SPLITTER.split(locationString); + + if (split.length != 1 && split.length != 2){ + return null; + } + final ResourceLocator result = new ResourceLocator(split[0].strip()); + if(split.length == 2) { + result.setIndexPath(split[1].strip()); + } + return result; + } + + public static RecentUrlsSet fromString(String urls, int maxLength){ + if(urls == null) { + return new RecentUrlsSet(maxLength); + } + + String[] elements = urls.split("\\|"); + List locators = Arrays.stream(elements) + .filter(Objects::nonNull) + .filter(elem -> !elem.isBlank()) + .map(RecentUrlsSet::stringToLocator) + .filter(Objects::nonNull) + .toList(); + return new RecentUrlsSet(locators, maxLength); + } +} diff --git a/src/main/java/org/broad/igv/ui/StackSet.java b/src/main/java/org/broad/igv/ui/StackSet.java new file mode 100644 index 0000000000..d33d6348be --- /dev/null +++ b/src/main/java/org/broad/igv/ui/StackSet.java @@ -0,0 +1,137 @@ +package org.broad.igv.ui; + +import java.util.*; + +/** + * This is a very specific collection which is designed to support "recent item" lists. + * + * It behaves like a stack, the most recently added item is always the first, second most recent is the second, and so forth + * It disallows duplicate items. Adding a duplicate item will move it to the top of the list. + * There is also size limit, adding additional items beyond the maximum size will remove the oldest items from the collection + * to make room. + * + * @implNote This is implemented in an extremely inefficient way, do not use this for large collections. + */ +public class StackSet extends AbstractCollection implements Set, SequencedCollection { + final private LinkedList values; + final int maxSize; + + /** + * Create an empty StackSet with maxSize + */ + public StackSet(int maxSize) { + this(new LinkedList<>(), maxSize); + } + + /** + * Constructs a StackSet containing the elements of the specified collection, in the order + * they are returned by the collection's iterator. (The first element returned by the collection's + * iterator becomes the first element, or top of the stack.) + * + * Duplicate values appear at their earliest position. + * + * This order consistent with the way {@link ArrayDeque} is implemented; + * + * @param initialValues + * @param maxSize + */ + public StackSet(Collection initialValues, int maxSize) { + this(new LinkedList<>(), maxSize); + Collection unique = new LinkedHashSet<>(initialValues); + List limited = unique.stream().limit(maxSize).toList(); + values.addAll(limited); + } + + + /** + * private constructor allowing direct setting of the values in order to support reverse views + */ + private StackSet(LinkedList values, int maxSize) { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize must be > 0"); + } + this.maxSize = maxSize; + this.values = values; + } + + /** + * Add an element to the top of the stack. If it is already present it will be + * moved to the top. + * @param t element to add + * @return always true + */ + @Override + public boolean add(T t) { + values.remove(t); + if (values.size() >= maxSize) { + values.removeLast(); + } + values.addFirst(t); + return true; + } + + /** + * Add the elements of this collection to the top of the stack in the order of the collections + * iterator. + * + * This is different from the implementation of {@link Deque} which adds to the end of the queue + * @param c collection containing elements to be added to this collection + * @return always true + */ + @Override + public boolean addAll(Collection c) { + ArrayList list = new ArrayList<>(c); + list.reversed().forEach(this::add); + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean remove(Object o) { + return values.remove(o); + } + + /** + * {@inheritDoc} + */ + @Override + public void clear() { + values.clear(); + } + + /** + * @return an iterator over the elements of the Stack from most recently to oldest + */ + @Override + public Iterator iterator() { + return values.iterator(); + } + + /** + * {@inheritDoc} + */ + @Override + public int size() { + return values.size(); + } + + /** + * @return the maximum allowed size for this collection + */ + public int getMaxSize(){ + return maxSize; + } + + /** + * {@inheritDoc} + */ + @Override + public StackSet reversed() { + return new StackSet<>(values.reversed(), maxSize); + } + +} + + diff --git a/src/main/java/org/broad/igv/ui/action/LoadFilesMenuAction.java b/src/main/java/org/broad/igv/ui/action/LoadFilesMenuAction.java index 4ef4ab0716..d3bdd82012 100644 --- a/src/main/java/org/broad/igv/ui/action/LoadFilesMenuAction.java +++ b/src/main/java/org/broad/igv/ui/action/LoadFilesMenuAction.java @@ -30,9 +30,9 @@ package org.broad.igv.ui.action; import org.broad.igv.logging.*; -import org.broad.igv.Globals; import org.broad.igv.prefs.IGVPreferences; import org.broad.igv.prefs.PreferencesManager; +import org.broad.igv.session.SessionReader; import org.broad.igv.ui.IGV; import org.broad.igv.ui.util.FileDialogUtils; import org.broad.igv.ui.util.MessageUtils; @@ -41,16 +41,16 @@ import java.awt.event.ActionEvent; import java.io.File; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; /** * @author jrobinso */ public class LoadFilesMenuAction extends MenuAction { - static Logger log = LogManager.getLogger(LoadFilesMenuAction.class); - IGV igv; + private static final Logger log = LogManager.getLogger(LoadFilesMenuAction.class); + private final IGV igv; public LoadFilesMenuAction(String label, int mnemonic, IGV igv) { super(label, null, mnemonic); @@ -59,9 +59,7 @@ public LoadFilesMenuAction(String label, int mnemonic, IGV igv) { @Override public void actionPerformed(ActionEvent e) { - loadFiles(chooseTrackFiles()); - } private File[] chooseTrackFiles() { @@ -85,25 +83,19 @@ private File[] chooseTrackFiles() { return trackFiles; } - private void loadFiles(File[] files) { + private void loadFiles(final File[] files) { if (files != null && files.length > 0) { - List validFileList = new ArrayList(); - StringBuffer buffer = new StringBuffer(); - buffer.append("File(s) not found: "); - boolean allFilesExist = true; - for (File file : files) { + final List validFiles = new ArrayList<>(); + final List missingFiles = new ArrayList<>(); + for (File file : files) { if (!file.exists()) { - allFilesExist = false; - buffer.append("\n\t"); - buffer.append(file.getAbsolutePath()); + missingFiles.add(file); } else { - String path = file.getAbsolutePath(); - if (path.endsWith(Globals.SESSION_FILE_EXTENSION)) { - // TODO -- a better test for session file than just the extension! + if (SessionReader.isSessionFile(path)) { final String msg = "File " + path + " appears to be an IGV Session file - " + "please use the Open Session menu item " + @@ -111,23 +103,25 @@ private void loadFiles(File[] files) { log.error(msg); MessageUtils.showMessage(msg); } else { - validFileList.add(file); + validFiles.add(file); } } } - files = validFileList.toArray(new File[validFileList.size()]); - if (!allFilesExist) { - final String msg = buffer.toString(); + if (!missingFiles.isEmpty()) { + String msg = missingFiles.stream() + .map(File::getAbsolutePath) + .collect(Collectors.joining("\n\t", "File(s) not found: \n\t", "")); log.error(msg); MessageUtils.showMessage(msg); } - if (files.length > 0) { - // Create DataResouceLocators for the selected files - final List locators = ResourceLocator.getLocators(Arrays.asList(files)); - igv.loadTracks(locators); + if (!validFiles.isEmpty()) { + // Create DataResourceLocators for the selected files + final List locators = ResourceLocator.getLocators(validFiles); + igv.addToRecentUrls(locators); + igv.loadTracks(locators); } } } diff --git a/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java b/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java index 355bd8ff59..12823f77a4 100644 --- a/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java +++ b/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java @@ -34,21 +34,18 @@ import org.broad.igv.feature.genome.load.HubGenomeLoader; import org.broad.igv.logging.*; import org.broad.igv.feature.genome.GenomeManager; -import org.broad.igv.util.GoogleUtils; -import org.broad.igv.prefs.Constants; -import org.broad.igv.prefs.PreferencesManager; import org.broad.igv.session.SessionReader; import org.broad.igv.ui.IGV; import org.broad.igv.ui.util.LoadFromURLDialog; import org.broad.igv.ui.util.MessageUtils; import org.broad.igv.util.*; -import org.broad.igv.ui.IGVMenuBar; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.io.IOException; import java.util.ArrayList; +import java.util.List; import static org.broad.igv.util.AmazonUtils.isObjectAccessible; @@ -57,12 +54,13 @@ */ public class LoadFromURLMenuAction extends MenuAction { - static Logger log = LogManager.getLogger(LoadFilesMenuAction.class); public static final String LOAD_FROM_URL = "Load from URL..."; public static final String LOAD_GENOME_FROM_URL = "Load Genome from URL..."; public static final String LOAD_FROM_HTSGET = "Load from htsget Server..."; public static final String LOAD_TRACKHUB = "Load Track Hub..."; - private IGV igv; + + private static final Logger log = LogManager.getLogger(LoadFromURLMenuAction.class); + private final IGV igv; public LoadFromURLMenuAction(String label, int mnemonic, IGV igv) { super(label, null, mnemonic); @@ -74,114 +72,106 @@ public void actionPerformed(ActionEvent e) { JPanel ta = new JPanel(); ta.setPreferredSize(new Dimension(600, 20)); - boolean isHtsGet = e.getActionCommand().equalsIgnoreCase(LOAD_FROM_HTSGET); - if (e.getActionCommand().equalsIgnoreCase(LOAD_FROM_URL) || isHtsGet) { + String command = e.getActionCommand(); + boolean isHtsGet = command.equalsIgnoreCase(LOAD_FROM_HTSGET); + if (command.equalsIgnoreCase(LOAD_FROM_URL) || isHtsGet) { LoadFromURLDialog dlg = new LoadFromURLDialog(IGV.getInstance().getMainFrame(), isHtsGet); dlg.setVisible(true); if (!dlg.isCanceled()) { - - - String inputURLs = dlg.getFileURL(); - if (inputURLs != null && inputURLs.trim().length() > 0) { - - String[] inputs = Globals.whitespacePattern.split(inputURLs.trim()); - checkURLs(inputs); - if (inputs.length == 1 && HubGenomeLoader.isHubURL(inputs[0])) { - LongRunningTask.submit(() -> { - try { - Genome newGenome = GenomeManager.getInstance().loadGenome(inputs[0]); - } catch (IOException ex) { - log.error("Error loading tack hub", ex); - MessageUtils.showMessage("Error loading track hub: " + ex.getMessage()); - - } - }); - } - else if (inputs.length == 1 && SessionReader.isSessionFile(inputs[0])) { - // Session URL - String url = inputs[0]; - if (url.startsWith("s3://")) { - checkAWSAccessbility(url); - } - try { - LongRunningTask.submit(() -> this.igv.loadSession(url, null)); - } catch (Exception ex) { - MessageUtils.showMessage("Error loading url: " + url + " (" + ex.toString() + ")"); - } - } else { - // Files, possibly indexed - String[] indexes = null; - String indexURLs = dlg.getIndexURL(); - if (indexURLs != null && indexURLs.trim().length() > 0) { - indexes = Globals.whitespacePattern.split(indexURLs.trim()); - if (indexes.length != inputs.length) { - throw new RuntimeException("The number of Index URLs must equal the number of File URLs"); - } - checkURLs(indexes); - } - - ArrayList locators = new ArrayList<>(); - for (int i = 0; i < inputs.length; i++) { - String url = inputs[i]; - ResourceLocator rl = new ResourceLocator(url.trim()); - if (indexes != null) { - String indexUrl = indexes[i]; - rl.setIndexPath(indexUrl); - } - if (isHtsGet) { - rl.setHtsget(true); - } - locators.add(rl); - } - igv.loadTracks(locators); - } - } + loadUrls(dlg.getFileURLs(), dlg.getIndexURLs(), isHtsGet); } - } else if ((e.getActionCommand().equalsIgnoreCase(LOAD_GENOME_FROM_URL))) { + } else if ((command.equalsIgnoreCase(LOAD_GENOME_FROM_URL))) { String url = JOptionPane.showInputDialog(IGV.getInstance().getMainFrame(), ta, "Enter URL to .genome or FASTA file", JOptionPane.QUESTION_MESSAGE); - if (url != null && url.trim().length() > 0) { - url = url.trim(); + loadGenomeFromUrl(url); + + } else if ((command.equalsIgnoreCase(LOAD_TRACKHUB))) { + loadTrackHub(ta); + } + } + + private void loadUrls(List inputs, List indexes, boolean isHtsGet) { + checkURLs(inputs); + if (inputs.size() == 1 && HubGenomeLoader.isHubURL(inputs.getFirst())) { + LongRunningTask.submit(() -> { try { - checkURLs(new String[]{url}); - GenomeManager.getInstance().loadGenome(url); - } catch (Exception e1) { - MessageUtils.showMessage("Error loading genome: " + e1.getMessage()); + GenomeManager.getInstance().loadGenome(inputs.getFirst()); + } catch (IOException ex) { + log.error("Error loading tack hub", ex); + MessageUtils.showMessage("Error loading track hub: " + ex.getMessage()); + } + }); + } else if (inputs.size() == 1 && SessionReader.isSessionFile(inputs.getFirst())) { + // Session URL + String url = inputs.getFirst(); + if (url.startsWith("s3://")) { + checkAWSAccessbility(url); + } + try { + LongRunningTask.submit(() -> this.igv.loadSession(url, null)); + } catch (Exception ex) { + MessageUtils.showMessage("Error loading url: " + url + " (" + ex + ")"); + } + } else { + if (!indexes.isEmpty() && indexes.size() != inputs.size()) { + throw new RuntimeException("The number of Index URLs must equal the number of File URLs"); + } + checkURLs(indexes); + List locators = getResourceLocators(inputs, indexes, isHtsGet); + igv.addToRecentUrls(locators); + igv.loadTracks(locators); + } + } + private static void loadTrackHub(JPanel ta) { + String urlOrAccension = JOptionPane.showInputDialog(IGV.getInstance().getMainFrame(), ta, "Enter GCA or GCF accession, or URL to hub.txt file", + JOptionPane.QUESTION_MESSAGE); + + if(urlOrAccension == null) return; + urlOrAccension = urlOrAccension.trim(); + final String url; + if(urlOrAccension.startsWith("GC")) { + url = HubGenomeLoader.convertToHubURL(urlOrAccension); + if(!FileUtils.resourceExists(url)) { + MessageUtils.showMessage("Unrecognized hub identifier: " + urlOrAccension); } - } else if ((e.getActionCommand().equalsIgnoreCase(LOAD_TRACKHUB))) { + } else { + url = urlOrAccension; + } - String urlOrAccension = JOptionPane.showInputDialog(IGV.getInstance().getMainFrame(), ta, "Enter GCA or GCF accension, or URL to hub.txt file", - JOptionPane.QUESTION_MESSAGE); + loadGenomeFromUrl(url); + } - if(urlOrAccension == null) return; - urlOrAccension = urlOrAccension.trim(); - String url; - if(urlOrAccension.startsWith("GC")) { - url = HubGenomeLoader.convertToHubURL(urlOrAccension); - if(url == null || !FileUtils.resourceExists(url)) { - MessageUtils.showMessage("Unrecognized hub identifier: " + urlOrAccension); - } - } else { - url = urlOrAccension; + private static void loadGenomeFromUrl(String url) { + if (url != null && !url.isBlank()) { + url = url.trim(); + try { + checkURLs(List.of(url)); + GenomeManager.getInstance().loadGenome(url); + } catch (Exception e) { + MessageUtils.showMessage("Error loading genome: " + e.getMessage()); } + } + } - if (url != null && url.trim().length() > 0) { - url = url.trim(); - try { - checkURLs(new String[]{url}); - GenomeManager.getInstance().loadGenome(url); - } catch (Exception e1) { - MessageUtils.showMessage("Error loading genome: " + e1.getMessage()); - } - + private static List getResourceLocators(Listinputs, List indexes, boolean isHtsGet) { + List locators = new ArrayList<>(); + for (int i = 0; i < inputs.size(); i++) { + final String url = inputs.get(i); + final ResourceLocator rl = new ResourceLocator(url.trim()); + if (!indexes.isEmpty()) { + final String indexUrl = indexes.get(i); + rl.setIndexPath(indexUrl); } + rl.setHtsget(isHtsGet); + locators.add(rl); } + return locators; } /** @@ -190,11 +180,11 @@ else if (inputs.length == 1 && SessionReader.isSessionFile(inputs[0])) { * @param input * @return */ - private boolean isHubURL(String input) { + private static boolean isHubURL(String input) { return input.endsWith("/hub.txt"); } - private void checkURLs(String[] urls) { + private static void checkURLs(List urls) { for (String url : urls) { if (url.startsWith("s3://")) { checkAWSAccessbility(url); @@ -204,7 +194,7 @@ private void checkURLs(String[] urls) { } } - private void checkAWSAccessbility(String url) { + private static void checkAWSAccessbility(String url) { try { // If AWS support is active, check if objects are in accessible tiers via Load URL menu... if (AmazonUtils.isAwsS3Path(url)) { diff --git a/src/main/java/org/broad/igv/ui/action/OpenSessionMenuAction.java b/src/main/java/org/broad/igv/ui/action/OpenSessionMenuAction.java index 3b1189627b..1bff4d9dd9 100644 --- a/src/main/java/org/broad/igv/ui/action/OpenSessionMenuAction.java +++ b/src/main/java/org/broad/igv/ui/action/OpenSessionMenuAction.java @@ -64,7 +64,7 @@ public OpenSessionMenuAction(String sessionFile, IGV igv) { super(sessionFile); this.sessionFile = sessionFile; this.igv = igv; - autoload = true; + this.autoload = true; } public OpenSessionMenuAction(String label, int mnemonic, IGV igv) { @@ -76,18 +76,25 @@ public OpenSessionMenuAction(String label, int mnemonic, IGV igv) { public void actionPerformed(ActionEvent e) { if (sessionFile == null || autoload == false) { - File lastSessionDirectory = PreferencesManager.getPreferences().getLastTrackDirectory(); - File tmpFile = FileDialogUtils.chooseFile("Open Session", lastSessionDirectory, JFileChooser.FILES_ONLY); - - if (tmpFile == null) { - return; - } - sessionFile = tmpFile.getAbsolutePath(); - PreferencesManager.getPreferences().setLastTrackDirectory(tmpFile.getParentFile()); + sessionFile = pickSessionFile(); } if (sessionFile != null) { LongRunningTask.submit(() -> this.igv.loadSession(sessionFile, null)); } } + + private static String pickSessionFile() { + File lastSessionDirectory = PreferencesManager.getPreferences().getLastTrackDirectory(); + File tmpFile = FileDialogUtils.chooseFile("Open Session", lastSessionDirectory, JFileChooser.FILES_ONLY); + + final String result; + if (tmpFile == null) { + result = null; + } else { + result = tmpFile.getAbsolutePath(); + PreferencesManager.getPreferences().setLastTrackDirectory(tmpFile.getParentFile()); + } + return result; + } } diff --git a/src/main/java/org/broad/igv/ui/util/AutosaveMenu.java b/src/main/java/org/broad/igv/ui/util/AutosaveMenu.java index b6069c9264..0c2579f77a 100644 --- a/src/main/java/org/broad/igv/ui/util/AutosaveMenu.java +++ b/src/main/java/org/broad/igv/ui/util/AutosaveMenu.java @@ -2,6 +2,7 @@ import org.broad.igv.session.autosave.SessionAutosaveManager; import org.broad.igv.ui.IGV; +import org.broad.igv.ui.MenuSelectedListener; import org.broad.igv.ui.action.OpenSessionMenuAction; import javax.swing.*; @@ -24,22 +25,7 @@ public AutosaveMenu() { private AutosaveMenu(String name) { super(name); - this.addMenuListener(new MenuListener() { - @Override - public void menuSelected(MenuEvent e) { - fillAutosaveList(); - } - - @Override - public void menuDeselected(MenuEvent e) { - - } - - @Override - public void menuCanceled(MenuEvent e) { - - } - }); + this.addMenuListener((MenuSelectedListener) e -> fillAutosaveList()); } /** @@ -64,9 +50,9 @@ private void fillAutosaveList() { } } // Create a menu item for each of the timed autosave files and add it to the menu - for(int i = 0; i < timedAutosaves.length; i++) { + for (File timedAutosave : timedAutosaves) { add(MenuAndToolbarUtils.createMenuItem( - new OpenSessionMenuAction(timedAutosaves[i].getAbsolutePath(), IGV.getInstance()) + new OpenSessionMenuAction(timedAutosave.getAbsolutePath(), IGV.getInstance()) )); } } diff --git a/src/main/java/org/broad/igv/ui/util/HistoryMenu.java b/src/main/java/org/broad/igv/ui/util/HistoryMenu.java index 449264a9b2..140c126b9d 100644 --- a/src/main/java/org/broad/igv/ui/util/HistoryMenu.java +++ b/src/main/java/org/broad/igv/ui/util/HistoryMenu.java @@ -27,6 +27,7 @@ import org.broad.igv.ui.IGV; import org.broad.igv.session.History; +import org.broad.igv.ui.MenuSelectedListener; import javax.swing.*; import javax.swing.event.MenuEvent; @@ -77,14 +78,14 @@ public void actionPerformed(ActionEvent actionEvent) { }); - this.addMenuListener(new MenuListener() { + this.addMenuListener(new MenuSelectedListener() { public void menuSelected(MenuEvent menuEvent) { final History history = IGV.getInstance().getSession().getHistory(); List allLoci = IGV.getInstance().getSession().getAllHistory(); - + boolean hasBack = history.peekBack() != null; boolean hasForward = history.peekForward() != null; backItem.setEnabled(hasBack); @@ -121,14 +122,6 @@ public void actionPerformed(ActionEvent actionEvent) { } - - public void menuDeselected(MenuEvent menuEvent) { - //To change body of implemented methods use File | Settings | File Templates. - } - - public void menuCanceled(MenuEvent menuEvent) { - //To change body of implemented methods use File | Settings | File Templates. - } }); } diff --git a/src/main/java/org/broad/igv/ui/util/LoadFromURLDialog.java b/src/main/java/org/broad/igv/ui/util/LoadFromURLDialog.java index 7c75c448e8..de905c9923 100644 --- a/src/main/java/org/broad/igv/ui/util/LoadFromURLDialog.java +++ b/src/main/java/org/broad/igv/ui/util/LoadFromURLDialog.java @@ -29,10 +29,13 @@ package org.broad.igv.ui.util; +import org.broad.igv.Globals; + import java.awt.*; import java.awt.event.*; -import java.net.MalformedURLException; -import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import javax.swing.*; import javax.swing.border.*; @@ -74,12 +77,20 @@ public boolean isCanceled() { return canceled; } - public String getFileURL() { - return fileURL; + public List getFileURLs() { + return splitOnWhiteSpace(fileURL); + } + + private static List splitOnWhiteSpace(String string) { + if (string != null && !string.isBlank()) { + String[] inputs = Globals.whitespacePattern.split(string.trim()); + return Arrays.asList(inputs); + } + return Collections.emptyList(); } - public String getIndexURL() { - return indexURL; + public List getIndexURLs() { + return splitOnWhiteSpace(indexURL); } private void initComponents(boolean isHtsget) { diff --git a/src/main/java/org/broad/igv/ui/util/RecentUrlsMenu.java b/src/main/java/org/broad/igv/ui/util/RecentUrlsMenu.java new file mode 100644 index 0000000000..055fcba687 --- /dev/null +++ b/src/main/java/org/broad/igv/ui/util/RecentUrlsMenu.java @@ -0,0 +1,71 @@ +package org.broad.igv.ui.util; + +import org.broad.igv.ui.IGV; +import org.broad.igv.ui.MenuSelectedListener; +import org.broad.igv.ui.RecentUrlsSet; +import org.broad.igv.ui.action.MenuAction; +import org.broad.igv.util.ResourceLocator; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.util.List; + +public class RecentUrlsMenu extends JMenu { + + public RecentUrlsMenu() { + this("Recent Files"); + } + + private RecentUrlsMenu(String name) { + super(name); + + this.addMenuListener((MenuSelectedListener) e -> { + RecentUrlsSet recentFileList = IGV.getInstance().getRecentUrls(); + if (recentFileList.isEmpty()) { + RecentUrlsMenu.this.setVisible(false); + } else { + RecentUrlsMenu.this.setVisible(true); + // Remove what's in the menu right now + RecentUrlsMenu.this.removeAll(); + // Create a menu item for each of the timed autosave files and add it to the menu + for (ResourceLocator resourceLocator : recentFileList) { + JMenuItem menuItem = createMenuItem(resourceLocator); + add(menuItem); + } + + addSeparator(); + JMenuItem clearButton = MenuAndToolbarUtils.createMenuItem(new MenuAction("Clear Recent Files") { + @Override + public void actionPerformed(ActionEvent event) { + IGV.getInstance().getRecentUrls().clear(); + RecentUrlsMenu.this.setVisible(false); + } + }); + add(clearButton); + } + }); + } + + private static JMenuItem createMenuItem(ResourceLocator resourceLocator) { + MenuAction menuItemAction = new MenuAction(resourceLocator.getPath()) { + @Override + public void actionPerformed(ActionEvent event) { + IGV igv = IGV.getInstance(); + List resource = List.of(resourceLocator); + igv.loadTracks(resource); + igv.addToRecentUrls(resource); + } + }; + + String toolTipText = resourceLocator.getIndexPath() == null + ? "Load track from " + resourceLocator.getPath() + : "Load track from
path: " + resourceLocator.getPath() + + "
index: " + resourceLocator.getIndexPath() + + ""; + + menuItemAction.setToolTipText(toolTipText); + return MenuAndToolbarUtils.createMenuItem(menuItemAction); + } +} + diff --git a/src/main/java/org/broad/igv/util/ResourceLocator.java b/src/main/java/org/broad/igv/util/ResourceLocator.java index ecf8a42a19..bb485cdde4 100644 --- a/src/main/java/org/broad/igv/util/ResourceLocator.java +++ b/src/main/java/org/broad/igv/util/ResourceLocator.java @@ -53,7 +53,7 @@ */ public class ResourceLocator { - private static Logger log = LogManager.getLogger(ResourceLocator.class); + private static final Logger log = LogManager.getLogger(ResourceLocator.class); /** * Display name @@ -736,7 +736,7 @@ public enum AttributeType { INDEX("index"), HTSGET("htsget"); - private String name; + private final String name; AttributeType(String name) { this.name = name; diff --git a/src/main/java/org/broad/igv/util/collections/LRUCache.java b/src/main/java/org/broad/igv/util/collections/LRUCache.java index d6fc90df99..097515d90d 100644 --- a/src/main/java/org/broad/igv/util/collections/LRUCache.java +++ b/src/main/java/org/broad/igv/util/collections/LRUCache.java @@ -46,10 +46,6 @@ public LRUCache(int max) { this.maxEntries = new AtomicInteger(max); } - public void setMaxEntries(int max) { - this.maxEntries.set(max); - } - private void createMap() { map = Collections.synchronizedMap( new LinkedHashMap(16, 0.75f, true) { diff --git a/src/test/java/org/broad/igv/sam/BisulfiteBaseInfoTest.java b/src/test/java/org/broad/igv/sam/BisulfiteBaseInfoTest.java index 5e8103df98..9b65801bc9 100644 --- a/src/test/java/org/broad/igv/sam/BisulfiteBaseInfoTest.java +++ b/src/test/java/org/broad/igv/sam/BisulfiteBaseInfoTest.java @@ -29,9 +29,11 @@ import org.broad.igv.feature.LocusScore; import org.broad.igv.feature.Strand; import org.broad.igv.track.WindowFunction; +import org.junit.Assert; import org.junit.Test; import java.awt.*; +import java.util.LinkedHashSet; import java.util.List; /** diff --git a/src/test/java/org/broad/igv/ui/RecentFileSetTest.java b/src/test/java/org/broad/igv/ui/RecentFileSetTest.java new file mode 100644 index 0000000000..5591883ca7 --- /dev/null +++ b/src/test/java/org/broad/igv/ui/RecentFileSetTest.java @@ -0,0 +1,56 @@ +package org.broad.igv.ui; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; + +public class RecentFileSetTest { + + @Test + public void testAdds() { + RecentFileSet set1 = new RecentFileSet(3); + set1.add("a"); + set1.add("b"); + set1.add("c"); + set1.add("d"); + set1.add("e"); + StackSetTest.assertEquals(set1, List.of("e","d","c")); + } + + @Test + public void testAsListRoundTrip(){ + RecentFileSet set = new RecentFileSet(List.of("a", "b", "c", "d", "b"), 3); + StackSetTest.assertEquals(set, List.of("a", "b", "c")); + String string = set.asString(); + Assert.assertEquals("a;b;c", string); + RecentFileSet set2 = RecentFileSet.fromString(string, 5); + StackSetTest.assertEquals(set2, List.of("a", "b", "c")); + } + + @Test + public void testNullString(){ + RecentFileSet set = RecentFileSet.fromString(null, 5); + StackSetTest.assertEquals(set, List.of()); + } + + @Test + public void testEmptyString(){ + RecentFileSet set = RecentFileSet.fromString("", 5); + StackSetTest.assertEquals(set, List.of()); + } + + @Test + public void testWhiteSpaceString(){ + RecentFileSet set = RecentFileSet.fromString(" a; b ; c; ", 5); + StackSetTest.assertEquals(set, List.of("a", "b", "c")); + } + + @Test + public void testInternalWhiteSpaceString(){ + RecentFileSet set = RecentFileSet.fromString("this file has spaces;thisonedoesnt", 5); + StackSetTest.assertEquals(set, List.of("this file has spaces", "thisonedoesnt")); + } + + +} \ No newline at end of file diff --git a/src/test/java/org/broad/igv/ui/RecentUrlsSetTest.java b/src/test/java/org/broad/igv/ui/RecentUrlsSetTest.java new file mode 100644 index 0000000000..e21c16af2e --- /dev/null +++ b/src/test/java/org/broad/igv/ui/RecentUrlsSetTest.java @@ -0,0 +1,57 @@ +package org.broad.igv.ui; + +import org.broad.igv.util.ResourceLocator; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Iterator; +import java.util.List; + + +public class RecentUrlsSetTest { + + @Test + public void testRoundTrip() { + ResourceLocator withIndex1 = new ResourceLocator("path"); + withIndex1.setIndexPath("index"); + + ResourceLocator withIndex2 = new ResourceLocator("i have an index"); + withIndex2.setIndexPath("yes i do"); + + ResourceLocator noIndex1 = new ResourceLocator("path/no/index"); + ResourceLocator noIndex2 = new ResourceLocator("path 2"); + + RecentUrlsSet resourceLocators = new RecentUrlsSet(List.of(withIndex1, withIndex2, noIndex1, noIndex2, withIndex1), 5); + String serialized = resourceLocators.asString(); + Assert.assertEquals(serialized, "path index:index|i have an index index:yes i do|path/no/index|path 2"); + + RecentUrlsSet roundTrip = RecentUrlsSet.fromString(serialized, 5); + Assert.assertEquals(roundTrip.size(), resourceLocators.size()); + Iterator actual = roundTrip.iterator(); + Iterator expected = resourceLocators.iterator(); + while (actual.hasNext() && expected.hasNext()) { + assertEqualLocator(expected.next(), actual.next()); + } + } + + @Test + public void testEmpty(){ + RecentUrlsSet empty = new RecentUrlsSet(10); + Assert.assertEquals("", empty.asString()); + RecentUrlsSet fromEmptyString = RecentUrlsSet.fromString("", 5); + Assert.assertEquals(0, fromEmptyString.size()); + } + + @Test + public void testNull(){ + RecentUrlsSet empty = RecentUrlsSet.fromString(null, 5); + Assert.assertEquals(empty.size(),0); + } + + public static void assertEqualLocator(ResourceLocator expected, ResourceLocator actual){ + Assert.assertEquals(expected.getPath(), actual.getPath()); + Assert.assertEquals(expected.getIndexPath(), actual.getIndexPath()); + } + + +} \ No newline at end of file diff --git a/src/test/java/org/broad/igv/ui/StackSetTest.java b/src/test/java/org/broad/igv/ui/StackSetTest.java new file mode 100644 index 0000000000..ddcb993a25 --- /dev/null +++ b/src/test/java/org/broad/igv/ui/StackSetTest.java @@ -0,0 +1,95 @@ +package org.broad.igv.ui; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +public class StackSetTest { + + + @Test + public void testAdds(){ + StackSet set = new StackSet<>(5); + set.add(1); + assertEquals(set, List.of(1)); + set.add(2); + assertEquals(set, List.of(2, 1)); + set.add(3); + assertEquals(set, List.of(3, 2, 1)); + set.add(4); + assertEquals(set, List.of(4, 3, 2, 1)); + set.add(5); + assertEquals(set, List.of(5, 4, 3, 2, 1)); + set.add(6); + assertEquals(set, List.of(6, 5, 4, 3, 2)); + set.add(7); + assertEquals(set, List.of(7, 6, 5, 4, 3)); + set.add(7); + assertEquals(set, List.of(7, 6, 5, 4, 3)); + set.add(1); + assertEquals(set, List.of(1, 7, 6, 5, 4)); + set.add(7); + assertEquals(set, List.of(7, 1, 6, 5, 4)); + set.remove(1); + assertEquals(set, List.of(7, 6, 5, 4)); + set.add(7); + assertEquals(set, List.of(7, 6, 5, 4)); + } + + @Test + public void testAddAll(){ + StackSet set = new StackSet<>(5); + set.add(1); + set.add(2); + set.add(3); + set.addAll(List.of(3,3,4,5,3)); + assertEquals(set, List.of(3,4,5,2,1)); + } + + @Test + public void testNewCollection(){ + StackSet set = new StackSet<>(List.of(1, 1, 2, 3, 4, 5, 6, 7, 3), 5); + assertEquals(set, List.of(1,2,3,4,5)); + set = new StackSet<>(List.of(1,1,1,1,1,1,1,1,1,1), 10); + assertEquals(set, List.of(1)); + set = new StackSet<>(Collections.emptySet(), 1); + assertEquals(set, List.of()); + set.add(1); + assertEquals(set, List.of(1)); + set.add(2); + assertEquals(set, List.of(2)); + } + + @Test + public void testDuplicateValuesPosition(){ + StackSet set = new StackSet<>(List.of(1, 2, 1, 3, 1, 4, 1, 5, 1), 5); + assertEquals(set, List.of(1,2,3,4,5)); + } + + @Test + public void testReverse(){ + StackSet set = new StackSet<>(List.of(1,2,3,4,5),5); + StackSet reversed = set.reversed(); + assertEquals(reversed, List.of(5,4,3,2,1)); + + reversed.add(1); + assertEquals(reversed, List.of(1,5,4,3,2)); + assertEquals(set, List.of(2,3,4,5,1)); + + set.remove((4)); + assertEquals(set, List.of(2,3,5,1)); + assertEquals(reversed, List.of(1,5,3,2)); + } + + + public static void assertEquals(Collection actual, List expected){ + Assert.assertEquals(expected.size(), actual.size()); + Assert.assertArrayEquals(expected.toArray(), actual.toArray()); + } + + +} \ No newline at end of file diff --git a/src/test/java/org/broad/igv/util/TestUtils.java b/src/test/java/org/broad/igv/util/TestUtils.java index 99f21fddaf..36629cea1f 100644 --- a/src/test/java/org/broad/igv/util/TestUtils.java +++ b/src/test/java/org/broad/igv/util/TestUtils.java @@ -42,6 +42,7 @@ import htsjdk.tribble.readers.AsciiLineReader; import org.junit.Assert; import org.junit.Ignore; +import org.junit.Test; import java.awt.*; import java.io.*; From f88feb436302c51c8274db1f085151fe2716bb08 Mon Sep 17 00:00:00 2001 From: Louis Bergelson Date: Tue, 19 Nov 2024 18:21:43 -0500 Subject: [PATCH 079/130] Re-enable tooltip updates on DataPanel (#1615) * This was accidentally commented out, putting it back in. --- src/main/java/org/broad/igv/ui/panel/DataPanel.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/broad/igv/ui/panel/DataPanel.java b/src/main/java/org/broad/igv/ui/panel/DataPanel.java index ee7e88be5b..028344d4d8 100644 --- a/src/main/java/org/broad/igv/ui/panel/DataPanel.java +++ b/src/main/java/org/broad/igv/ui/panel/DataPanel.java @@ -567,12 +567,12 @@ class DataPanelMouseAdapter extends MouseInputAdapter { @Override public void mouseMoved(MouseEvent e) { String position = null; -// if (!frame.getChrName().equals(Globals.CHR_ALL)) { -// int location = (int) frame.getChromosomePosition(e) + 1; -// position = frame.getChrName() + ":" + locationFormatter.format(location); -// IGV.getInstance().setStatusBarMessag2(position); -// } -// updateTooltipText(e.getX(), e.getY()); + if (!frame.getChrName().equals(Globals.CHR_ALL)) { + int location = (int) frame.getChromosomePosition(e) + 1; + position = frame.getChrName() + ":" + locationFormatter.format(location); + IGV.getInstance().setStatusBarMessag2(position); + } + updateTooltipText(e.getX(), e.getY()); if (IGV.getInstance().isRulerEnabled()) { IGV.getInstance().repaint(); From a524c97f53588b6a22a6df68e2d713d1f6e200f3 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Tue, 19 Nov 2024 19:07:44 -0800 Subject: [PATCH 080/130] update registry URL --- src/main/resources/org/broad/igv/prefs/preferences.tab | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/org/broad/igv/prefs/preferences.tab b/src/main/resources/org/broad/igv/prefs/preferences.tab index 4ca38fe9e9..8cae827f6b 100644 --- a/src/main/resources/org/broad/igv/prefs/preferences.tab +++ b/src/main/resources/org/broad/igv/prefs/preferences.tab @@ -269,7 +269,7 @@ PORT_ENABLED Enable port boolean TRUE PORT_NUMBER Port number integer 60151 --- IGV.genome.sequence.dir Genome server URL string https://igv.org/genomes/genomes.tsv -MASTER_RESOURCE_FILE_KEY Data registry URL string https://data.broadinstitute.org/igvdata/$$_dataServerRegistry.txt +MASTER_RESOURCE_FILE_KEY Data registry URL string https://igv.org/genomes/registry/$$_dataServerRegistry.txt --- PROVISIONING.URL OAuth provisioning URL string null --- From 36a952ba7107473b3bd227c79958d4728e1d4407 Mon Sep 17 00:00:00 2001 From: Devang Thakkar Date: Thu, 21 Nov 2024 22:17:24 -0700 Subject: [PATCH 081/130] Add READ_ORDER to ColorBy list (#1622) --- src/main/resources/org/broad/igv/prefs/preferences.tab | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/org/broad/igv/prefs/preferences.tab b/src/main/resources/org/broad/igv/prefs/preferences.tab index 8cae827f6b..47ed001796 100644 --- a/src/main/resources/org/broad/igv/prefs/preferences.tab +++ b/src/main/resources/org/broad/igv/prefs/preferences.tab @@ -105,7 +105,7 @@ SAM.ALLELE_USE_QUALITY Quality weight allele fraction boolean TRUE ##Alignment Track Defaults -SAM.COLOR_BY Color alignments by select NONE|READ_STRAND|FIRST_OF_PAIR_STRAND|PAIR_ORIENTATION|UNEXPECTED_PAIR|INSERT_SIZE|BASE_MODIFICATION|BASE_MODIFICATION_2COLOR|SAMPLE|READ_GROUP|LIBRARY|MOVIE|ZMW|BISULFITE|NOMESEQ|TAG|MAPPED_SIZE|LINK_STRAND|YC_TAG UNEXPECTED_PAIR +SAM.COLOR_BY Color alignments by select NONE|READ_STRAND|FIRST_OF_PAIR_STRAND|PAIR_ORIENTATION|UNEXPECTED_PAIR|INSERT_SIZE|BASE_MODIFICATION|BASE_MODIFICATION_2COLOR|SAMPLE|READ_GROUP|LIBRARY|MOVIE|ZMW|BISULFITE|NOMESEQ|TAG|MAPPED_SIZE|LINK_STRAND|YC_TAG|READ_ORDER UNEXPECTED_PAIR SAM.COLOR_BY_TAG Color by TAG string SAM.GROUP_OPTION Group alignments by select NONE|STRAND|SAMPLE|READ_GROUP|LIBRARY|FIRST_OF_PAIR_STRAND|TAG|PAIR_ORIENTATION|MATE_CHROMOSOME|SV_ALIGNMENT|SUPPLEMENTARY|BASE_AT_POS|MOVIE|ZMW|HAPLOTYPE|READ_ORDER|LINKED|PHASE|MAPPING_QUALITY|DUPLICATE SAM.GROUP_BY_TAG Group by TAG string From 772ee4b433f8ed53e005141b080145e1ea0ccdba Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Tue, 26 Nov 2024 20:18:11 -0800 Subject: [PATCH 082/130] ignore htsget bam test -- server no longer active --- src/test/java/org/broad/igv/htsget/HtsgetBAMReaderTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/broad/igv/htsget/HtsgetBAMReaderTest.java b/src/test/java/org/broad/igv/htsget/HtsgetBAMReaderTest.java index f1e96787cd..32d68a30ed 100644 --- a/src/test/java/org/broad/igv/htsget/HtsgetBAMReaderTest.java +++ b/src/test/java/org/broad/igv/htsget/HtsgetBAMReaderTest.java @@ -31,7 +31,7 @@ public class HtsgetBAMReaderTest { * * @throws Exception */ - @Test + @Test @Ignore public void testQueryAlignments() throws Exception { String url = "https://htsget.demo.umccr.org/reads/org.umccr.demo.htsget-rs-data/bam/htsnexus_test_NA12878"; From d628f7114d7e86e4781bd45cdb9637239f6771c1 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Mon, 2 Dec 2024 17:01:13 -0800 Subject: [PATCH 083/130] update mac IGV launcher to universal build --- scripts/mac.app/Contents/MacOS/IGV | Bin 28944 -> 135776 bytes scripts/mac.app/Contents/MacOS/main.c | 146 -------------------------- 2 files changed, 146 deletions(-) delete mode 100644 scripts/mac.app/Contents/MacOS/main.c diff --git a/scripts/mac.app/Contents/MacOS/IGV b/scripts/mac.app/Contents/MacOS/IGV index 121f61cb797beb5ddbd26fc7b335c1c36f6246b3..895624615bda7decf1b38075c626815d18b20203 100755 GIT binary patch delta 7233 zcmb_g3v`r4mae}amUpkkM`k zIlw|Rzdt)O;_RVWbxagzqQRX62Mmh}jv$XYqadIO5ABw53BouuBlLc^{`y1HXLj(M z**dAZ_q%m()vfBPzwS-jS*h=p(Ck0l+`X4Erjr;80o5~R0aU8t(|jsaRIb)oXvDb9>YRchxl#s~+NPy=%j z9wLM`TSMc@3R{h%w$4!xna#Ff#-atH6Qo6q*C|AVZ!2OvAyei`qhlsUsXjPp6(|#x zwbGZ8Nh#OO9BwkHeO9%L5jH5YAxRmKqV5txB4$;Op`RDClBi2`5^J;7mfIRuFRNHl zYpZLl2Yz7Mf@!Ljs;Zj0G?AI_vWEDs#||PIud2MU{FWeiJQwUGbYs86xfTiZ!e@hW zS~uL8Eu3}ne#U^Q*ypK~tf^`xl`$7rL4#3QD5x;uDg^*07+7;uxen41s4n2UK~Ll{ zRs-Qs@TI83QQ1gQNjV8MmOM0;LN!&MH%w{Qk4ydOP2HTnA$bFD7lnL~+Vs2s%7vO4 zmHTGIXp-`sevGazR7o_<8PkN>$h+~VCRCc8$|zLAmV8)0)Fm6X=@fHVQN%Mih}QA% zTQ?i6Axd>vVN5Or&DZ?VEt$}U_=eIM7Vk_lGM3Z(9?j`VBa_xW1Ap`T_mbb9@wg6w zCOtT(*3J0)muuF-gV8P5p=E8I_!;c9F)qMFjNv&PWd3xJ`MWyoBkX$u?A_eH+6W20s;DVMm?xnF) zMZZ+hZ&l=0QMZcPRrHpMI#u+xin9cQdG61yc9hulAa3- z#8fks52B|jZbNv<6|)vN1YHOoysx114q3=oIIGDZ4iW zNB1Ix6s5Rvn&?CLH-YU4TL{OZq5h)+XAlm<+hJDw0a+C_qU3?E(g5hjApAUVwO&&G zIyz=B;E9J`I^^_Br6#9paS$Zz9yFS3Wmo{IxkHslpr(m6HO*sM0jb#|lxvPfgZ=Tu;3RFJ)I|TD%lVmk^y~#F(-+eVDQ!Ju7SiKniM>CXps6AEcj& z4myg8J7aPbf)F~O`0<`9Z*+8>%9t$u%eq{hsSz zM4(@~z;yV2a$U&*x(JoRObGKWW*v(_R4_()VQfN5VYMSWH_ze7&YpB%UUhaw?tSIi z`IQrMbMp%-@+&4O2gmk1ee>2lx}*5&Cx0F#XT0#6ku}L>o)a%b8-Abr?yFy?CC3k$ zQ+)H&(_@Ceyu9?eotXviwi#c~`RF%KwAalT=Q*)`OW8NaOJ7Rad^P>S+%NB`*_9dL zI&!eN_l463?!TC8*!t4~-O7%FUtametcwpUG9~`&!W|*W#g>=O-}T_eOXt0b>(;+` zNt)XgHa^7Kbu`mGCH~0uZ*PiI#m{C8Ry7zC-(|J{1{RA7X#aRc$UHo&K>dxj@GZg* z1FVxT6g#^os~x+<2!yelgGGy)F+VSNikfRn)bS1peZqm`;pcFPE+P~)#}-UcZM3Ml zV2ZL%nljuyRTbNXK4F~*K1YtUsGSUymqDte>hjZp=txrN$cyFnK$voXXyqB*gyAF5 zLz-4pDulHNH;M<{T#S1hG#@Z87M0+M$2a5FTJIRDyVUWTRC7K)*=vPPZmdN^pm&PZ zn_TLE_&o7KSX+o1s`rW&po5oQmDtNvO1q$5V-=^ur)*+-MW_yH0o01Mm$3nWf_=A# z>UF?(Lw@jm+(G>GKO$7OKy|bhs=uTR8)jUcZQO19;`obko6>SImKl}%b}LlRD&V1d z4Q8VyYoR*LPDKmV%?IH1!+5V3<~se&?AVPpQpb(yOnz^FN_XE-*3++NzP_OrP>avN z*w!0srgcD1?$z5q{b9x44oyGo*Xzqnh+s`Ri;wB1e(E1;Ip*IVcMNlPh8iutP|4zJ zZ%)3}*NiczGGG56B{|gPD)X#jJ^lvPgE^csa~ou2zguysay%j16A$9kgk{B{~k{LBYk~<{`G&PTjea2{{jVUP8&O4DF1 zak|`>#(WZ6d%S-F^PPoWUz){tZi2<<)ic=x=R$WFVl8-|M=i^Y@LO&{!2m4MJ$sePScD3(iEF?uNB!T&yo#^0Rr5DSzt>+RjsGhyv zmjZepL%rPrzOsC=vVvmy7qIEixAc(yHt>A+=w}=vcJE~@Y?zoZn)xu<)^pKV*f2h> zQ})0kZFMv_U*Xnbc`Xz@Q0kwqmk~0wjmL_-OPP=JuNKSR6@otrzCrL4C(GVdf}aO| zjo@3sI|Y9je6yzGERnq%u*%KM$G7cdiR^iv`TPoQK)%Jd5>E8tqs#?h7@TqPM-Nv5m!!;1fIr>p1RPghmfaRXP;-JEi5$MlqLe+X?Yi)){5>kc!r zCp#bum2iMJY)|w^>X`0k+qHh9t84+rrk~aW8#r;9=5@Av;g5~DIQQC zb`-aa{yj6|d+>J&{)!WTKIYUh0G@J0;^`+?B=~Q@Q}9Oeuff*~{yccf8cE&{ev{z8 z0FP_Q<;ao~UKRWa@Lhuc6Zn1L?D&_jZr5cIsD1A_h_XsB={PEfO;S%QjXc72?= zb}Ew25tMXBBZ;yFWr;lH{5z@W-_1jRL=xH%kZe-oX2P7#z)V+5HYo|cm28@UDxP-K zuOOTM@67Yzz+BUgnQrBpJo7}ZNz$0y7X_Ht#cA1jM$8gp_ z*kyr_7ONOvCf;wy`J?_M!gwW`Tq-766*Zz{g0Ip5=(9n%1-P0?X@e?cOR1!mFK8Jj zMO&%KPED+-X@1VcC7~Qq!_=RS+DI1yh47r9ews#G7p;@$m|~rpeH$602>a=S1|*;X zw2h#Rp#d~Z{p0|BSEvvs2keu1_KM|Xrj03|uJ%wTFZ0m?3}X5c1>&Y4>a zp$_&UPv4h5S?My*HuBq}+%RV;Iiur7GW-WcJ;^^RhmVo;<@L*yfiX#sAwBQK=kEfh zF``n*L&b^F7lJ~jp8*;I9hbWGhBInPuQ&c5DLwvAcED^FDg7i1ta*`U&0Ki*M3lV3 zyu67&Cd>b0l5XSGM+(kzJ&vyOn>6Xv;Y_@L7dhJgAvwY(d+uyV&eD~|mx1EfAv*vJS`d54W zz2|RkoVxDeohIj|x|o>1F5Iy>efWtN!#>)T{`~sK%liN4SYzq(*6Fi8s$KBLohJC_ z`kh&?eK#}yF7h;pgD zC?S`CYs5Bf1F1Hp2?@la2&E8&gdc`zn>LL#wrZ8r+VrfZq4dY~ncep&x-c_4GtbP< z?z=np>zUA=TgnN0jILeJWLqoIt&|DF#;=zCf@Ma^UX1NAN}ofz80q?6HGvz;*RL~6 zpUedSbQ(rpS{`4U&zK8fVXa5zJWa1pVJsJB9cUe>2l^(+c?)9>7}sE41EN8GBEoI} znXt0K4q!^J`V9A#XQtN8#H9znE#thgP<3$zeI-JX%ikH)m zW35owvOrWDtxe-u5b4GYu&NRDgr2uMiXx8aQBu~dquVeR%+EUtO?faJUGhh{vw?%T z?L=i<=RO$K=_y*#YoQ3wr0-w9|63~Bn-%nAL{=~b;{T;3N8x;wdl@K)ryz4xj=Koa za(EY>3cqi7GJHaZ^Fe(0s`vpBUmVYeD$PnCU#Lv5-6T`j!Z7o070yxLBfsC_DgGYX zxG20;*e89z`~B+Q<8(;g?L4_E;f*sZq4!0pZh|GYF|oCC$(Xz1xam;wC6kqgH)%bbDh-Y zh358==yG3?z9jy6wMFUX^VOClA8_OJe!P2e)Q;{+d32+8K)sbb+-Xrt_-5xubTkw3rZGvwRyh!kEf|m$x7radHa>2I?PS4Dr zR;9aOD^d0;{F5Dtk<>`+sFCOgKDVQ^)99j$fj8j-T1f0eNZd6caTkTel@>BYlk~zU z4k?8U*Cf4cvPWnVO-ktWO35CjNqUK_f|*@H2OAa$BU{wuV@<9R5k?lR$rMehnmiCS zq83GUnvsG!%4v`ZB=?00hTMysQ>O_GF*jmoq8MqVI0c8HsT@15?q!H8OD7;euFyG;i3S{APc2?sP~yW5~zW0$!ta z`TLq~)Y`iOANgGY9sU-X9y7*bFmDkv$>{#A*5Bd}_s6gNhQtA60Rp{pLq}3-;HQQ1 z<$wi>u;K&=Rujf|0~Rm`V1e4GR)QHL`Nx0-%mLU+$uJPi70Iu4B)XT>z!3L$3AUNT50r@?zvl1>c;cZ=wP%5l*Of8+ zFfsay|45sT?6|r?c6>67pih@?M38@u>BQ}zc`F?C5=Y%5!)VsAh+Z|F&m4+Rw!LOm zZB|>Zt-zLNeZIB1u%J0l%`0eW-IKc|Usd_ip_@(9U$wt}GitE-@@IE6@K@{K>z)kip84YNMr&V1>z>8(lgUS?v%RIST-|o;aCOU{DGgH(D`HNi-o7{* n?D0$ttvUb4($GNT^`<+HOAVHZ$d>mvypXVS{9MDkA&&n5>O9zi diff --git a/scripts/mac.app/Contents/MacOS/main.c b/scripts/mac.app/Contents/MacOS/main.c deleted file mode 100644 index 5cf4bc0035..0000000000 --- a/scripts/mac.app/Contents/MacOS/main.c +++ /dev/null @@ -1,146 +0,0 @@ -// -// main.c -// igv-launcher -// -// Created by James Robinson on 9/14/24. -// -// Launch IGV on MacOS (https://github.com/igvteam/igv). -// -// Code adapted from https://incenp.org/notes/2023/universal-java-app-on-macos.html -// - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - - -/* Remove the last n components of a pathname. The pathname - * is modified in place. Returns 0 if the requested number - * of components have been removed, -1 otherwise. */ -static int remove_last_component(char *buffer , unsigned n) -{ - char *last_slash = NULL; - while ( n-- > 0 ) { - if ( (last_slash = strrchr(buffer, '/')) ) - *last_slash = '\0'; - } - return last_slash ? 0 : -1; -} - -/* - * Test if a given path exists and if it is a directory. - * It returns 1 if given path is directory and exists - * otherwise returns 0. - */ -static int isDirectoryExists(const char *path) -{ - struct stat stats; - stat(path, &stats); - - // Check for file existence - if (S_ISDIR(stats.st_mode)) - return 1; - - return 0; -} - -static int simpleLauncher(int argc, char **argv) -{ - char app_path[PATH_MAX]; - uint32_t path_size = PATH_MAX; - int ret = 0; - - (void) argc; - (void) argv; - - /* Get the path to the "Contents" directory. */ - if ( _NSGetExecutablePath(app_path, &path_size) == -1 ) - err(EXIT_FAILURE, "Cannot get application directory"); - if ( remove_last_component(app_path, 2) == -1 ) - errx(EXIT_FAILURE, "Cannot get application directory"); - - /* Move to that directory. */ - if ( chdir(app_path) == -1 ) - err(EXIT_FAILURE, "Cannot change current directory"); - - char* cmd; - - /* See if there is a bundled JDK */ - if(isDirectoryExists("jdk-21")) { - char jdkPath[2048] = {'\0'}; - snprintf(jdkPath, sizeof(jdkPath), "%s/jdk-21", app_path); - - setenv("JAVA_HOME", jdkPath, true); - - char javaPath[2048] = {'\0'}; - snprintf(javaPath, sizeof(javaPath), "%s/bin/java", jdkPath); - cmd = javaPath; - - printf("Using bundled JDK"); - - //PATH=$JAVA_HOME/bin:$PATH - } else { - - cmd = "java"; - printf("Using System JDK\n"); - } - - /* Get user defined extra arguments, if any */ - char extraArgumentsPath[2048] = {'\0'}; - const char *homeDir = getenv("HOME"); - snprintf(extraArgumentsPath, sizeof(extraArgumentsPath), "%s/.igv/java_arguments", homeDir); - - if(access(extraArgumentsPath, F_OK) == 0) { - char extraArguments[2048] = {'\0'}; - snprintf(extraArguments, sizeof(extraArguments), "@%s", extraArgumentsPath); - - /* Run IGV with user extra arguments */ - - - char* args[] = { - "java", - "-showversion", - "--module-path=Java/lib", - "-Xmx8g", - "@Java/igv.args", - "-Xdock:name=IGV", - "-Xdock:icon=Resources/IGV_64.png", - "-Dapple.laf.useScreenMenuBar=true", - extraArguments, - "--module=org.igv/org.broad.igv.ui.Main", - NULL}; - - ret = execvp(cmd, args); - return ret; - - } else { - - /* Run IGV without user extra arguments */ - - char* args[] = { - "java", - "-showversion", - "--module-path=Java/lib", - "-Xmx8g", - "@Java/igv.args", - "-Xdock:name=IGV", - "-Xdock:icon=Resources/IGV_64.png", - "--module=org.igv/org.broad.igv.ui.Main", - NULL}; - - ret = execvp(cmd, args); - return ret; - } -} - -int main(int argc, char **argv) -{ - return simpleLauncher(argc, argv); -} From ce6a9e163e9300e3ed3930e05028e17f52725cb9 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Mon, 2 Dec 2024 19:18:46 -0800 Subject: [PATCH 084/130] hack for deleting "snappy" from the mac distribution --- build.gradle | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/build.gradle b/build.gradle index 16d6f892d5..8b3c0684b4 100644 --- a/build.gradle +++ b/build.gradle @@ -316,6 +316,7 @@ tasks.register('createMacAppDist', Copy) { from("scripts/mac.app") { exclude "Contents/Info.plist.template" exclude "Contents/MacOS/IGV.sh" + exclude "Contents/MacOS/main.c" } into "IGV_${version}.app" } @@ -339,8 +340,14 @@ tasks.register('createMacAppDist', Copy) { } into "${buildDir}/IGV-MacApp-dist" + doLast { project.exec { commandLine('chmod', '775', "${buildDir}/IGV-MacApp-dist/IGV_${version}.app") } + + // A hack -- mac apps cannot be notarized with the "snappy" module + project.delete( + files("${buildDir}/IGV-MacApp-dist/IGV_${version}.app/Contents/Java/lib/snappy-java-1.1.10.5-module.jar") + ) } } From fade54b8edcab1c2e552cc46112fc03c661e3fe8 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Mon, 2 Dec 2024 19:44:25 -0800 Subject: [PATCH 085/130] Add gradle tasks to build both intel & apple silicon packages --- build.gradle | 14 ++++++++++++++ gradle.properties | 1 + 2 files changed, 15 insertions(+) diff --git a/build.gradle b/build.gradle index 8b3c0684b4..5b5e7e4715 100644 --- a/build.gradle +++ b/build.gradle @@ -375,6 +375,20 @@ tasks.register('createMacAppWithJavaDistZip', Zip) { } } +tasks.register('createMacAppIntelWithJavaDistZip', Zip) { + dependsOn createMacAppDist + archiveFileName = "IGV_MacAppIntel_${version}_WithJava.zip" + with copySpec { from jdkBundleMacIntel into "IGV_${version}.app/Contents/jdk-21" } + from("${buildDir}/IGV-MacApp-dist") + + doLast { + if (jdkBundleMacIntel == "") { + throw new GradleException("Required property not set: jdkBundleMacIntel"); + } + //project.exec { commandLine('chmod', '775', createMacAppWithJavaDistZip.archiveFileName) } + } +} + tasks.register('createWinDist', Copy) { dependsOn createDist with copySpec { diff --git a/gradle.properties b/gradle.properties index 60ad87722a..4a9561b80d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,6 +15,7 @@ tsaurl=not_set # Location of JDKs to include in distribution bundles; ignored for developer builds jdkBundleLinux= jdkBundleMac= +jdkBundleMacIntel= jdkBundleWindows= # Path to NSIS packaging tool for Windows EXE installer; ignored for developer builds makensisCommand= From 75fe8eb9afa6a04fc5fa31ae91a90f80c249ca7f Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Mon, 2 Dec 2024 22:08:16 -0800 Subject: [PATCH 086/130] Redo ENCODE menu items to eliminate need for a section label. The label is not seen in the production Mac build --- src/main/java/org/broad/igv/ui/IGVMenuBar.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/broad/igv/ui/IGVMenuBar.java b/src/main/java/org/broad/igv/ui/IGVMenuBar.java index 6e36bc4496..3c833bece1 100644 --- a/src/main/java/org/broad/igv/ui/IGVMenuBar.java +++ b/src/main/java/org/broad/igv/ui/IGVMenuBar.java @@ -310,21 +310,17 @@ JMenu createFileMenu() { JSeparator separator = new JSeparator(); menuItems.add(separator); - JLabel encodeLabel = new JLabel(" ENCODE"); - encodeLabel.setFont(encodeLabel.getFont().deriveFont(Font.BOLD)); - menuItems.add(encodeLabel); - // Post 2012 ENCODE menu JMenuItem chipItem = new JMenuItem(); - chipItem.setAction(new BrowseEncodeAction("Signals ChIP ...", 0, BrowseEncodeAction.Type.SIGNALS_CHIP, igv)); + chipItem.setAction(new BrowseEncodeAction("ENCODE: ChIP Signals ...", 0, BrowseEncodeAction.Type.SIGNALS_CHIP, igv)); encodeMenuItems.add(chipItem); JMenuItem otherSignalsItem = new JMenuItem(); - otherSignalsItem.setAction(new BrowseEncodeAction("Signals Other ...", 0, BrowseEncodeAction.Type.SIGNALS_OTHER, igv)); + otherSignalsItem.setAction(new BrowseEncodeAction("ENCODE: Other Signals ...", 0, BrowseEncodeAction.Type.SIGNALS_OTHER, igv)); encodeMenuItems.add(otherSignalsItem); JMenuItem otherItem = new JMenuItem(); - otherItem.setAction(new BrowseEncodeAction("Other ...", 0, BrowseEncodeAction.Type.OTHER, igv)); + otherItem.setAction(new BrowseEncodeAction("ENCODE: Other ...", 0, BrowseEncodeAction.Type.OTHER, igv)); encodeMenuItems.add(otherItem); for(JComponent item : encodeMenuItems) { @@ -334,7 +330,7 @@ JMenu createFileMenu() { // UCSC hosted ENCODE menu. encodeUCSCMenuItem = MenuAndToolbarUtils.createMenuItem( - new BrowseEncodeAction("UCSC Repository (2012) ...", KeyEvent.VK_E, BrowseEncodeAction.Type.UCSC, igv)); + new BrowseEncodeAction("ENCODE: UCSC Repository (2012) ...", KeyEvent.VK_E, BrowseEncodeAction.Type.UCSC, igv)); encodeUCSCMenuItem.setVisible(EncodeTrackChooser.genomeSupportedUCSC(genomeId)); menuItems.add(encodeUCSCMenuItem); From a67652af5428cd35e1a77cad04f4530e1911f37b Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Tue, 3 Dec 2024 22:17:45 -0800 Subject: [PATCH 087/130] refine genome "is downloadable" tests --- .../feature/genome/GenomeDownloadUtils.java | 33 ++++++++++++++----- .../igv/feature/genome/load/GenomeConfig.java | 20 +++++++++++ .../feature/genome/load/JsonGenomeLoader.java | 22 ------------- .../HostedGenomeSelectionDialog.java | 4 +-- .../genome/GenomeDownloadUtilsTest.java | 24 ++++++++++++++ test/data/genomes/json/hg38_twobit.json | 21 ++++++++++++ 6 files changed, 91 insertions(+), 33 deletions(-) create mode 100644 src/test/java/org/broad/igv/feature/genome/GenomeDownloadUtilsTest.java create mode 100644 test/data/genomes/json/hg38_twobit.json diff --git a/src/main/java/org/broad/igv/feature/genome/GenomeDownloadUtils.java b/src/main/java/org/broad/igv/feature/genome/GenomeDownloadUtils.java index a18d8139f2..8c20632aef 100644 --- a/src/main/java/org/broad/igv/feature/genome/GenomeDownloadUtils.java +++ b/src/main/java/org/broad/igv/feature/genome/GenomeDownloadUtils.java @@ -20,8 +20,7 @@ import java.lang.reflect.Field; import java.net.MalformedURLException; import java.net.URL; -import java.util.List; -import java.util.Map; +import java.util.*; /** * Class of static functions for managing genome downloads @@ -30,20 +29,27 @@ public class GenomeDownloadUtils { private static Logger log = LogManager.getLogger(GenomeDownloadUtils.class); - public static boolean isAnnotationsDownloadable(GenomeListItem item) { - return item.getPath().endsWith(".json"); + public static boolean isAnnotationsDownloadable(String path) { + return path.endsWith(".json"); } - public static boolean isSequenceDownloadable(GenomeListItem item) { - if (item.getPath().endsWith(".json")) { + public static boolean isSequenceDownloadable(String path) { + if (path != null && path.endsWith(".json")) { try { - String jsonString = HttpUtils.getInstance().getContentsAsJSON(new URL(item.getPath())); - return jsonString.contains("twoBitURL"); + String jsonString = FileUtils.getContents(path); + GenomeConfig genomeConfig = GenomeConfig.fromJson(jsonString); + String sequenceURL = genomeConfig.getTwoBitURL(); + if (sequenceURL == null) { + sequenceURL = genomeConfig.getFastaURL(); + } + return isRemoteURL(sequenceURL) && !disallowedBuckets.stream().anyMatch(sequenceURL::contains); + } catch (IOException e) { - log.error("Error fetching genome json " + item.getPath()); + log.error("Error fetching genome json " + path); } } return false; + } public static File downloadGenome(GenomeConfig c, boolean downloadSequence, boolean downloadAnnotations) throws IOException { @@ -161,4 +167,13 @@ private static File download(URL url, File directory) throws MalformedURLExcepti return localFile; } + private static boolean isRemoteURL(String url) { + return url.startsWith("http://") || url.startsWith("https://"); + } + + static Set disallowedBuckets = new HashSet<>(Arrays.asList( + "igv.org.genomes", + "igv-genepattern-org", + "igv.broadinstitute.org")); + } diff --git a/src/main/java/org/broad/igv/feature/genome/load/GenomeConfig.java b/src/main/java/org/broad/igv/feature/genome/load/GenomeConfig.java index d698d1ffaa..5fd57257ec 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/GenomeConfig.java +++ b/src/main/java/org/broad/igv/feature/genome/load/GenomeConfig.java @@ -63,6 +63,9 @@ public class GenomeConfig implements Cloneable { private List> chromAliases; public static GenomeConfig fromJson(String json) { + if (json.contains("chromosomeOrder")) { + json = fixChromosomeOrder(json); + } return (new Gson()).fromJson(json, GenomeConfig.class); } @@ -307,4 +310,21 @@ protected GenomeConfig clone() { } } + /** + * Fix deprecated form of chromosome order (comma delimited list of strings) + * + * @param jsonString + * @return + */ + private static String fixChromosomeOrder(String jsonString) { + Map obj = (new Gson()).fromJson(jsonString, Map.class); + Object chromosomeOrder = obj.get("chromosomeOrder"); + if (chromosomeOrder != null) { + if (chromosomeOrder instanceof String) { + obj.put("chromosomeOrder", Arrays.stream(((String) chromosomeOrder).split(",")).map(c -> c.trim()).toArray()); + } + } + return (new Gson()).toJson(obj); + } + } diff --git a/src/main/java/org/broad/igv/feature/genome/load/JsonGenomeLoader.java b/src/main/java/org/broad/igv/feature/genome/load/JsonGenomeLoader.java index c0750a7f99..b2361fa489 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/JsonGenomeLoader.java +++ b/src/main/java/org/broad/igv/feature/genome/load/JsonGenomeLoader.java @@ -66,10 +66,6 @@ public GenomeConfig loadGenomeConfig() throws IOException { String jsonString = ParsingUtils.readContentsFromStream(is); - if (jsonString.contains("chromosomeOrder")) { - jsonString = fixChromosomeOrder(jsonString); - } - GenomeConfig genomeConfig = GenomeConfig.fromJson(jsonString); fixPaths(genomeConfig); @@ -79,24 +75,6 @@ public GenomeConfig loadGenomeConfig() throws IOException { } } - /** - * Fix deprecated form of chromosome order (comma delimited list of strings) - * - * @param jsonString - * @return - */ - private String fixChromosomeOrder(String jsonString) { - Map obj = (new Gson()).fromJson(jsonString, Map.class); - Object chromosomeOrder = obj.get("chromosomeOrder"); - if (chromosomeOrder != null) { - if (chromosomeOrder instanceof String) { - obj.put("chromosomeOrder", Arrays.stream(((String) chromosomeOrder).split(",")).map(c -> c.trim()).toArray()); - } - } - return (new Gson()).toJson(obj); - - } - /** * JSON paths/urls can be relative to the path to the genome json file. This method converts them to absolulte * paths/urls. diff --git a/src/main/java/org/broad/igv/ui/commandbar/HostedGenomeSelectionDialog.java b/src/main/java/org/broad/igv/ui/commandbar/HostedGenomeSelectionDialog.java index 7a9b9ccd8b..1f89e9b3d9 100644 --- a/src/main/java/org/broad/igv/ui/commandbar/HostedGenomeSelectionDialog.java +++ b/src/main/java/org/broad/igv/ui/commandbar/HostedGenomeSelectionDialog.java @@ -207,8 +207,8 @@ private void okButtonActionPerformed(java.awt.event.ActionEvent evt) { private void configureDownloadButtons(GenomeListItem item) { if (item != null) { - final boolean sequenceDownloadable = GenomeDownloadUtils.isSequenceDownloadable(item); - final boolean annotationsDownloadable = GenomeDownloadUtils.isAnnotationsDownloadable(item); + final boolean sequenceDownloadable = GenomeDownloadUtils.isSequenceDownloadable(item.getPath()); + final boolean annotationsDownloadable = GenomeDownloadUtils.isAnnotationsDownloadable(item.getPath()); downloadSequenceCB.setEnabled(sequenceDownloadable); downloadAnnotationsCB.setEnabled(annotationsDownloadable); } diff --git a/src/test/java/org/broad/igv/feature/genome/GenomeDownloadUtilsTest.java b/src/test/java/org/broad/igv/feature/genome/GenomeDownloadUtilsTest.java new file mode 100644 index 0000000000..82f721afda --- /dev/null +++ b/src/test/java/org/broad/igv/feature/genome/GenomeDownloadUtilsTest.java @@ -0,0 +1,24 @@ +package org.broad.igv.feature.genome; + +import org.broad.igv.util.TestUtils; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class GenomeDownloadUtilsTest { + + @Test + public void isAnnotationsDownloadable() { + assertTrue(GenomeDownloadUtils.isAnnotationsDownloadable(TestUtils.DATA_DIR + "genomes/json/hg38_twobit.json")); + + } + + @Test + public void isSequenceDownloadable() { + + assertTrue(GenomeDownloadUtils.isSequenceDownloadable(TestUtils.DATA_DIR + "genomes/json/hg38_twobit.json")); + assertFalse(GenomeDownloadUtils.isSequenceDownloadable(TestUtils.DATA_DIR + "genomes/json/hg18.unittest.json")); + assertFalse(GenomeDownloadUtils.isSequenceDownloadable(TestUtils.DATA_DIR + "genomes/json/sacCer3.json")); + + } +} \ No newline at end of file diff --git a/test/data/genomes/json/hg38_twobit.json b/test/data/genomes/json/hg38_twobit.json new file mode 100644 index 0000000000..df9ed956f5 --- /dev/null +++ b/test/data/genomes/json/hg38_twobit.json @@ -0,0 +1,21 @@ +{ + "id": "hg38", + "name": "Human (GRCh38/hg38)", + "fastaURL": "https://igv.org/genomes/data/hg38/hg38.fa", + "indexURL": "https://igv.org/genomes/data/hg38/hg38.fa.fai", + "cytobandURL": "https://hgdownload.soe.ucsc.edu/goldenPath/hg38/database/cytoBandIdeo.txt.gz", + "aliasURL": "https://igv.org/genomes/data/hg38/hg38_alias.tab", + "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/hg38/bigZips/hg38.2bit", + "chromSizesURL": "https://hgdownload.soe.ucsc.edu/goldenPath/hg38/bigZips/hg38.chrom.sizes", + "chromosomeOrder": "chr1,chr2,chr3,chr4,chr5,chr6,chr7,chr8,chr9,chr10,chr11,chr12,chr13,chr14,chr15,chr16,chr17,chr18,chr19,chr20,chr21,chr22,chrX,chrY", + "tracks": [ + { + "name": "Refseq Select", + "format": "refgene", + "url": "https://hgdownload.soe.ucsc.edu/goldenPath/hg38/database/ncbiRefSeqSelect.txt.gz", + "indexed": false, + "order": 1000001, + "infoURL": "https://www.ncbi.nlm.nih.gov/gene/?term=$$" + } + ] +} From 900ca32135545f75d89d7a2955697f8b7acd2a9c Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Tue, 3 Dec 2024 23:04:47 -0800 Subject: [PATCH 088/130] Genomes -- use radio buttons for genome download options. Rename encode menu items --- .../java/org/broad/igv/ui/IGVMenuBar.java | 8 +- .../HostedGenomeSelectionDialog.java | 102 +++++++++++++----- 2 files changed, 82 insertions(+), 28 deletions(-) diff --git a/src/main/java/org/broad/igv/ui/IGVMenuBar.java b/src/main/java/org/broad/igv/ui/IGVMenuBar.java index 3c833bece1..bb59c34f06 100644 --- a/src/main/java/org/broad/igv/ui/IGVMenuBar.java +++ b/src/main/java/org/broad/igv/ui/IGVMenuBar.java @@ -312,15 +312,15 @@ JMenu createFileMenu() { // Post 2012 ENCODE menu JMenuItem chipItem = new JMenuItem(); - chipItem.setAction(new BrowseEncodeAction("ENCODE: ChIP Signals ...", 0, BrowseEncodeAction.Type.SIGNALS_CHIP, igv)); + chipItem.setAction(new BrowseEncodeAction("ENCODE ChIP Signals ...", 0, BrowseEncodeAction.Type.SIGNALS_CHIP, igv)); encodeMenuItems.add(chipItem); JMenuItem otherSignalsItem = new JMenuItem(); - otherSignalsItem.setAction(new BrowseEncodeAction("ENCODE: Other Signals ...", 0, BrowseEncodeAction.Type.SIGNALS_OTHER, igv)); + otherSignalsItem.setAction(new BrowseEncodeAction("ENCODE Other Signals ...", 0, BrowseEncodeAction.Type.SIGNALS_OTHER, igv)); encodeMenuItems.add(otherSignalsItem); JMenuItem otherItem = new JMenuItem(); - otherItem.setAction(new BrowseEncodeAction("ENCODE: Other ...", 0, BrowseEncodeAction.Type.OTHER, igv)); + otherItem.setAction(new BrowseEncodeAction("ENCODE Other ...", 0, BrowseEncodeAction.Type.OTHER, igv)); encodeMenuItems.add(otherItem); for(JComponent item : encodeMenuItems) { @@ -330,7 +330,7 @@ JMenu createFileMenu() { // UCSC hosted ENCODE menu. encodeUCSCMenuItem = MenuAndToolbarUtils.createMenuItem( - new BrowseEncodeAction("ENCODE: UCSC Repository (2012) ...", KeyEvent.VK_E, BrowseEncodeAction.Type.UCSC, igv)); + new BrowseEncodeAction("ENCODE 2012 UCSC Repository ...", KeyEvent.VK_E, BrowseEncodeAction.Type.UCSC, igv)); encodeUCSCMenuItem.setVisible(EncodeTrackChooser.genomeSupportedUCSC(genomeId)); menuItems.add(encodeUCSCMenuItem); diff --git a/src/main/java/org/broad/igv/ui/commandbar/HostedGenomeSelectionDialog.java b/src/main/java/org/broad/igv/ui/commandbar/HostedGenomeSelectionDialog.java index 1f89e9b3d9..f0ae3ed1aa 100644 --- a/src/main/java/org/broad/igv/ui/commandbar/HostedGenomeSelectionDialog.java +++ b/src/main/java/org/broad/igv/ui/commandbar/HostedGenomeSelectionDialog.java @@ -66,6 +66,17 @@ public class HostedGenomeSelectionDialog extends org.broad.igv.ui.IGVDialog { private JList genomeList; private JCheckBox downloadSequenceCB; private JCheckBox downloadAnnotationsCB; + + + JPanel downloadSequencePanel; + ButtonGroup downloadSequenceGroup; + private JRadioButton downloadSequenceRB; + private JRadioButton remoteSequenceRB; + + JPanel downloadAnnotationsPanel; + ButtonGroup downloadAnnotationsGroup; + private JRadioButton downloadAnnotationsRB; + private JRadioButton remoteAnnotationsRB; private boolean isCanceled; private List allListItems; private DefaultListModel genomeListModel; @@ -181,11 +192,11 @@ public GenomeListItem getSelectedValue() { } public boolean isDownloadSequence() { - return downloadSequenceCB.isSelected(); + return downloadSequenceRB.isSelected(); } public boolean isDownloadAnnotations() { - return downloadAnnotationsCB.isSelected(); + return downloadAnnotationsRB.isSelected(); } public boolean isCanceled() { @@ -209,8 +220,13 @@ private void configureDownloadButtons(GenomeListItem item) { if (item != null) { final boolean sequenceDownloadable = GenomeDownloadUtils.isSequenceDownloadable(item.getPath()); final boolean annotationsDownloadable = GenomeDownloadUtils.isAnnotationsDownloadable(item.getPath()); - downloadSequenceCB.setEnabled(sequenceDownloadable); - downloadAnnotationsCB.setEnabled(annotationsDownloadable); + // downloadSequenceCB.setEnabled(sequenceDownloadable); + downloadSequenceRB.setVisible(sequenceDownloadable); + remoteSequenceRB.setVisible(sequenceDownloadable); + + // downloadAnnotationsCB.setEnabled(annotationsDownloadable); + downloadAnnotationsRB.setVisible(annotationsDownloadable); + remoteAnnotationsRB.setVisible(annotationsDownloadable); } } @@ -227,7 +243,7 @@ private void initComponents() { JPanel dialogPane = new JPanel(); dialogPane.setBorder(new EmptyBorder(12, 12, 12, 12)); - dialogPane.setPreferredSize(new Dimension(350, 500)); + dialogPane.setPreferredSize(new Dimension(500, 500)); dialogPane.setLayout(new BorderLayout()); //======== contentPanel ======== @@ -297,25 +313,63 @@ public void keyReleased(KeyEvent e) { contentPanel.add(scrollPane1); - //---- downloadSequenceCB ---- - downloadSequenceCB = new JCheckBox("Download sequence"); - downloadSequenceCB.setAlignmentX(1.0F); - downloadSequenceCB.setToolTipText("Download the full sequence data for the genome. Note that these files can be large (human is about 1 gigabyte)."); - downloadSequenceCB.setMaximumSize(new Dimension(1000, 23)); - downloadSequenceCB.setPreferredSize(new Dimension(300, 23)); - downloadSequenceCB.setMinimumSize(new Dimension(300, 23)); - downloadSequenceCB.setEnabled(false); // Disabled until a genome is selected. - contentPanel.add(downloadSequenceCB); - - //---- downloadAnnotationsCB ---- - downloadAnnotationsCB = new JCheckBox("Download annotations"); - downloadAnnotationsCB.setAlignmentX(1.0F); - downloadAnnotationsCB.setToolTipText("Download all annotation files referenced by the genome definition."); - downloadAnnotationsCB.setMaximumSize(new Dimension(1000, 23)); - downloadAnnotationsCB.setPreferredSize(new Dimension(300, 23)); - downloadAnnotationsCB.setMinimumSize(new Dimension(300, 23)); - downloadAnnotationsCB.setEnabled(false); // Disabled until a genome is selected - contentPanel.add(downloadAnnotationsCB); + JPanel p1 = new JPanel(); + p1.setLayout(new GridLayout(2, 0)); + p1.setPreferredSize(new Dimension(500, 50)); + p1.setMinimumSize(new Dimension(500, 50)); + p1.setMaximumSize(new Dimension(1000, 50)); + + downloadSequenceRB = new JRadioButton("Download sequence"); + remoteSequenceRB = new JRadioButton("Remote sequence"); + remoteSequenceRB.setSelected(true); + downloadSequenceRB.setVisible(false); + remoteSequenceRB.setVisible(false); + downloadSequenceGroup = new ButtonGroup(); + downloadSequenceGroup.add(downloadSequenceRB); + downloadSequenceGroup.add(remoteSequenceRB); + downloadSequencePanel = new JPanel(); + FlowLayout layout = new FlowLayout(FlowLayout.LEFT); + downloadSequencePanel.setLayout(layout); + downloadSequencePanel.add(downloadSequenceRB); + downloadSequencePanel.add(remoteSequenceRB); + p1.add(downloadSequencePanel); + + downloadAnnotationsRB = new JRadioButton("Download annotations"); + remoteAnnotationsRB = new JRadioButton("Remote annotations"); + remoteAnnotationsRB.setSelected(true); + downloadAnnotationsRB.setVisible(false); + remoteAnnotationsRB.setVisible(false); + downloadAnnotationsGroup = new ButtonGroup(); + downloadAnnotationsGroup.add(downloadAnnotationsRB); + downloadAnnotationsGroup.add(remoteAnnotationsRB); + downloadAnnotationsPanel = new JPanel(); + FlowLayout layout1 = new FlowLayout(FlowLayout.LEFT); + downloadAnnotationsPanel.setLayout(layout1); + downloadAnnotationsPanel.add(downloadAnnotationsRB); + downloadAnnotationsPanel.add(remoteAnnotationsRB); + p1.add(downloadAnnotationsPanel); + + contentPanel.add(p1); + +// //---- downloadSequenceCB ---- +// downloadSequenceCB = new JCheckBox("Download sequence"); +// downloadSequenceCB.setAlignmentX(1.0F); +// downloadSequenceCB.setToolTipText("Download the full sequence data for the genome. Note that these files can be large (human is about 1 gigabyte)."); +// downloadSequenceCB.setMaximumSize(new Dimension(1000, 23)); +// downloadSequenceCB.setPreferredSize(new Dimension(300, 23)); +// downloadSequenceCB.setMinimumSize(new Dimension(300, 23)); +// downloadSequenceCB.setEnabled(false); // Disabled until a genome is selected. +// contentPanel.add(downloadSequenceCB); +// +// //---- downloadAnnotationsCB ---- +// downloadAnnotationsCB = new JCheckBox("Download annotations"); +// downloadAnnotationsCB.setAlignmentX(1.0F); +// downloadAnnotationsCB.setToolTipText("Download all annotation files referenced by the genome definition."); +// downloadAnnotationsCB.setMaximumSize(new Dimension(1000, 23)); +// downloadAnnotationsCB.setPreferredSize(new Dimension(300, 23)); +// downloadAnnotationsCB.setMinimumSize(new Dimension(300, 23)); +// downloadAnnotationsCB.setEnabled(false); // Disabled until a genome is selected +// contentPanel.add(downloadAnnotationsCB); dialogPane.add(contentPanel, BorderLayout.CENTER); From 10abc5254500d510c7a33f90cac155988015186b Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Wed, 4 Dec 2024 10:15:06 -0800 Subject: [PATCH 089/130] genome download dialog refinements --- .../HostedGenomeSelectionDialog.java | 32 ++++++------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/broad/igv/ui/commandbar/HostedGenomeSelectionDialog.java b/src/main/java/org/broad/igv/ui/commandbar/HostedGenomeSelectionDialog.java index f0ae3ed1aa..4cf472c7de 100644 --- a/src/main/java/org/broad/igv/ui/commandbar/HostedGenomeSelectionDialog.java +++ b/src/main/java/org/broad/igv/ui/commandbar/HostedGenomeSelectionDialog.java @@ -61,6 +61,8 @@ public class HostedGenomeSelectionDialog extends org.broad.igv.ui.IGVDialog { private static Logger log = LogManager.getLogger(HostedGenomeSelectionDialog.class); + private static final int PREF_WIDTH = 600; + private static final int PREF_HEIGHT = 500; private JTextField genomeFilter; private JList genomeList; @@ -243,7 +245,7 @@ private void initComponents() { JPanel dialogPane = new JPanel(); dialogPane.setBorder(new EmptyBorder(12, 12, 12, 12)); - dialogPane.setPreferredSize(new Dimension(500, 500)); + dialogPane.setPreferredSize(new Dimension(PREF_WIDTH, PREF_HEIGHT)); dialogPane.setLayout(new BorderLayout()); //======== contentPanel ======== @@ -265,36 +267,22 @@ private void initComponents() { //======== filterPanel ======== JPanel filterPanel = new JPanel(); filterPanel.setMaximumSize(new Dimension(2147483647, 28)); - filterPanel.setLayout(new GridBagLayout()); - ((GridBagLayout) filterPanel.getLayout()).columnWidths = new int[]{0, 0, 0}; - ((GridBagLayout) filterPanel.getLayout()).rowHeights = new int[]{0, 0}; - ((GridBagLayout) filterPanel.getLayout()).columnWeights = new double[]{1.0, 1.0, 1.0E-4}; - ((GridBagLayout) filterPanel.getLayout()).rowWeights = new double[]{1.0, 1.0E-4}; - - //---- label1 ---- - JLabel filterLabel = new JLabel(); - filterLabel.setText("Filter:"); + filterPanel.setLayout(new BorderLayout()); + + JLabel filterLabel = new JLabel("Filter: "); filterLabel.setLabelFor(genomeFilter); filterLabel.setRequestFocusEnabled(false); - filterPanel.add(filterLabel, new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, - GridBagConstraints.WEST, GridBagConstraints.VERTICAL, - new Insets(0, 0, 0, 0), 0, 0)); + filterPanel.add(filterLabel, BorderLayout.WEST); - //---- genomeFilter ---- genomeFilter = new JTextField(); genomeFilter.setToolTipText("Filter genome list"); - genomeFilter.setPreferredSize(new Dimension(220, 28)); - genomeFilter.setMinimumSize(new Dimension(180, 28)); - genomeFilter.setAlignmentX(0.0F); genomeFilter.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { genomeEntryKeyReleased(e); } }); - filterPanel.add(genomeFilter, new GridBagConstraints(1, 0, 1, 1, 1.0, 0.0, - GridBagConstraints.CENTER, GridBagConstraints.BOTH, - new Insets(0, 0, 0, 0), 0, 0)); + filterPanel.add(genomeFilter, BorderLayout.CENTER); contentPanel.add(filterPanel); @@ -320,7 +308,7 @@ public void keyReleased(KeyEvent e) { p1.setMaximumSize(new Dimension(1000, 50)); downloadSequenceRB = new JRadioButton("Download sequence"); - remoteSequenceRB = new JRadioButton("Remote sequence"); + remoteSequenceRB = new JRadioButton("Use remote sequence"); remoteSequenceRB.setSelected(true); downloadSequenceRB.setVisible(false); remoteSequenceRB.setVisible(false); @@ -335,7 +323,7 @@ public void keyReleased(KeyEvent e) { p1.add(downloadSequencePanel); downloadAnnotationsRB = new JRadioButton("Download annotations"); - remoteAnnotationsRB = new JRadioButton("Remote annotations"); + remoteAnnotationsRB = new JRadioButton("Use remote annotations"); remoteAnnotationsRB.setSelected(true); downloadAnnotationsRB.setVisible(false); remoteAnnotationsRB.setVisible(false); From 788434904dd8468954e2f8c5dc0f5dcdacbe24e4 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Wed, 4 Dec 2024 10:56:40 -0800 Subject: [PATCH 090/130] ditto --- .../commandbar/HostedGenomeSelectionDialog.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/broad/igv/ui/commandbar/HostedGenomeSelectionDialog.java b/src/main/java/org/broad/igv/ui/commandbar/HostedGenomeSelectionDialog.java index 4cf472c7de..d063be2c39 100644 --- a/src/main/java/org/broad/igv/ui/commandbar/HostedGenomeSelectionDialog.java +++ b/src/main/java/org/broad/igv/ui/commandbar/HostedGenomeSelectionDialog.java @@ -223,12 +223,12 @@ private void configureDownloadButtons(GenomeListItem item) { final boolean sequenceDownloadable = GenomeDownloadUtils.isSequenceDownloadable(item.getPath()); final boolean annotationsDownloadable = GenomeDownloadUtils.isAnnotationsDownloadable(item.getPath()); // downloadSequenceCB.setEnabled(sequenceDownloadable); - downloadSequenceRB.setVisible(sequenceDownloadable); - remoteSequenceRB.setVisible(sequenceDownloadable); + downloadSequenceRB.setEnabled(sequenceDownloadable); + remoteSequenceRB.setEnabled(sequenceDownloadable); // downloadAnnotationsCB.setEnabled(annotationsDownloadable); - downloadAnnotationsRB.setVisible(annotationsDownloadable); - remoteAnnotationsRB.setVisible(annotationsDownloadable); + downloadAnnotationsRB.setEnabled(annotationsDownloadable); + remoteAnnotationsRB.setEnabled(annotationsDownloadable); } } @@ -310,8 +310,8 @@ public void keyReleased(KeyEvent e) { downloadSequenceRB = new JRadioButton("Download sequence"); remoteSequenceRB = new JRadioButton("Use remote sequence"); remoteSequenceRB.setSelected(true); - downloadSequenceRB.setVisible(false); - remoteSequenceRB.setVisible(false); + downloadSequenceRB.setEnabled(false); + remoteSequenceRB.setEnabled(false); downloadSequenceGroup = new ButtonGroup(); downloadSequenceGroup.add(downloadSequenceRB); downloadSequenceGroup.add(remoteSequenceRB); @@ -325,8 +325,8 @@ public void keyReleased(KeyEvent e) { downloadAnnotationsRB = new JRadioButton("Download annotations"); remoteAnnotationsRB = new JRadioButton("Use remote annotations"); remoteAnnotationsRB.setSelected(true); - downloadAnnotationsRB.setVisible(false); - remoteAnnotationsRB.setVisible(false); + downloadAnnotationsRB.setEnabled(false); + remoteAnnotationsRB.setEnabled(false); downloadAnnotationsGroup = new ButtonGroup(); downloadAnnotationsGroup.add(downloadAnnotationsRB); downloadAnnotationsGroup.add(remoteAnnotationsRB); From e4ded5705ec93ca1f484a67e5b5f40e1561acaa6 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Wed, 4 Dec 2024 11:13:39 -0800 Subject: [PATCH 091/130] rollback --- .../commandbar/HostedGenomeSelectionDialog.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/broad/igv/ui/commandbar/HostedGenomeSelectionDialog.java b/src/main/java/org/broad/igv/ui/commandbar/HostedGenomeSelectionDialog.java index d063be2c39..4cf472c7de 100644 --- a/src/main/java/org/broad/igv/ui/commandbar/HostedGenomeSelectionDialog.java +++ b/src/main/java/org/broad/igv/ui/commandbar/HostedGenomeSelectionDialog.java @@ -223,12 +223,12 @@ private void configureDownloadButtons(GenomeListItem item) { final boolean sequenceDownloadable = GenomeDownloadUtils.isSequenceDownloadable(item.getPath()); final boolean annotationsDownloadable = GenomeDownloadUtils.isAnnotationsDownloadable(item.getPath()); // downloadSequenceCB.setEnabled(sequenceDownloadable); - downloadSequenceRB.setEnabled(sequenceDownloadable); - remoteSequenceRB.setEnabled(sequenceDownloadable); + downloadSequenceRB.setVisible(sequenceDownloadable); + remoteSequenceRB.setVisible(sequenceDownloadable); // downloadAnnotationsCB.setEnabled(annotationsDownloadable); - downloadAnnotationsRB.setEnabled(annotationsDownloadable); - remoteAnnotationsRB.setEnabled(annotationsDownloadable); + downloadAnnotationsRB.setVisible(annotationsDownloadable); + remoteAnnotationsRB.setVisible(annotationsDownloadable); } } @@ -310,8 +310,8 @@ public void keyReleased(KeyEvent e) { downloadSequenceRB = new JRadioButton("Download sequence"); remoteSequenceRB = new JRadioButton("Use remote sequence"); remoteSequenceRB.setSelected(true); - downloadSequenceRB.setEnabled(false); - remoteSequenceRB.setEnabled(false); + downloadSequenceRB.setVisible(false); + remoteSequenceRB.setVisible(false); downloadSequenceGroup = new ButtonGroup(); downloadSequenceGroup.add(downloadSequenceRB); downloadSequenceGroup.add(remoteSequenceRB); @@ -325,8 +325,8 @@ public void keyReleased(KeyEvent e) { downloadAnnotationsRB = new JRadioButton("Download annotations"); remoteAnnotationsRB = new JRadioButton("Use remote annotations"); remoteAnnotationsRB.setSelected(true); - downloadAnnotationsRB.setEnabled(false); - remoteAnnotationsRB.setEnabled(false); + downloadAnnotationsRB.setVisible(false); + remoteAnnotationsRB.setVisible(false); downloadAnnotationsGroup = new ButtonGroup(); downloadAnnotationsGroup.add(downloadAnnotationsRB); downloadAnnotationsGroup.add(remoteAnnotationsRB); From f4287ff0832db9a99a08a91138c80b282d439121 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Wed, 4 Dec 2024 13:15:53 -0800 Subject: [PATCH 092/130] null check for tooltip text --- .../java/org/broad/igv/track/AbstractTrack.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/broad/igv/track/AbstractTrack.java b/src/main/java/org/broad/igv/track/AbstractTrack.java index 59226aabbb..e844923153 100644 --- a/src/main/java/org/broad/igv/track/AbstractTrack.java +++ b/src/main/java/org/broad/igv/track/AbstractTrack.java @@ -855,12 +855,14 @@ public String getTooltipText(int y) { StringBuffer buffer = new StringBuffer(); buffer.append("" + getName()); - Map metadata = resourceLocator.getMetadata(); - if(metadata != null && metadata.size() > 0) { - for(Map.Entry entry : metadata.entrySet()) { - String value = entry.getValue(); - if(value != null && value.length() > 0) { - buffer.append("
" + entry.getKey() + ": " + entry.getValue()); + if(resourceLocator != null) { + Map metadata = resourceLocator.getMetadata(); + if (metadata != null && metadata.size() > 0) { + for (Map.Entry entry : metadata.entrySet()) { + String value = entry.getValue(); + if (value != null && value.length() > 0) { + buffer.append("
" + entry.getKey() + ": " + entry.getValue()); + } } } } From 76584da65ab5ca8e7d8c48140ba41af88a53c11a Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Sun, 8 Dec 2024 19:36:46 -0800 Subject: [PATCH 093/130] Disable memory-mapped index files. This is not needed for IGV, and can result rare but obscure errors. --- src/main/java/org/broad/igv/sam/reader/SAMReader.java | 6 ++++-- src/main/java/org/broad/igv/sam/reader/SamReaderPool.java | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/broad/igv/sam/reader/SAMReader.java b/src/main/java/org/broad/igv/sam/reader/SAMReader.java index 84ceeebdc2..ab87e569c7 100644 --- a/src/main/java/org/broad/igv/sam/reader/SAMReader.java +++ b/src/main/java/org/broad/igv/sam/reader/SAMReader.java @@ -69,7 +69,7 @@ public SAMReader(String samFile) throws IOException { this(samFile, true); } - public SAMReader(String samFile, boolean requireIndex) throws IOException { + public SAMReader(String samFile, boolean requireIndex) { this.samFile = samFile; if (requireIndex) { featureIndex = SamUtils.getIndexFor(samFile); @@ -78,7 +78,9 @@ public SAMReader(String samFile, boolean requireIndex) throws IOException { } } factory = SamReaderFactory.makeDefault(). - validationStringency(ValidationStringency.SILENT); + validationStringency(ValidationStringency.SILENT). + referenceSource(new IGVReferenceSource()). + enable(SamReaderFactory.Option.DONT_MEMORY_MAP_INDEX); loadHeader(); } diff --git a/src/main/java/org/broad/igv/sam/reader/SamReaderPool.java b/src/main/java/org/broad/igv/sam/reader/SamReaderPool.java index e18395cc3b..1c163badd5 100644 --- a/src/main/java/org/broad/igv/sam/reader/SamReaderPool.java +++ b/src/main/java/org/broad/igv/sam/reader/SamReaderPool.java @@ -84,7 +84,8 @@ private SamReader createReader(int bufferSize) throws IOException { boolean isLocal = locator.isLocal(); final SamReaderFactory factory = SamReaderFactory.makeDefault(). referenceSource(new IGVReferenceSource()). - validationStringency(ValidationStringency.SILENT); + validationStringency(ValidationStringency.SILENT). + enable(SamReaderFactory.Option.DONT_MEMORY_MAP_INDEX); SamInputResource resource; if (isLocal) { From 8a80a57f2e54bc2e9a9e55fb382f7ef32dfe4635 Mon Sep 17 00:00:00 2001 From: Piyush Acharya Date: Tue, 17 Dec 2024 17:37:52 -0800 Subject: [PATCH 094/130] Fix unmatched quotation mark in README Windows build instructions (#1631) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ebb0415838..3004acb74f 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Builds are executed from the IGV project directory. Files will be created in the IGV requires **Java 21** to build and run. Later versions of Java should work but we build and test on **Java 21**. -NOTE: If on a Windows platform use ```./gradlew.bat'``` in the instructions below +NOTE: If on a Windows platform use ```./gradlew.bat``` in the instructions below #### Folder structure and build targets From 3a9ef80b948cd773689415597ca7c436eca8090e Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Fri, 3 Jan 2025 14:17:26 -0800 Subject: [PATCH 095/130] Relax implicit assumption in FeatureCache that features are grouped by chromosome. --- .../org/broad/igv/feature/FeatureUtils.java | 5 +- .../java/org/broad/igv/util/FeatureCache.java | 6 +- .../igv/util/DataInputStreamWrapper.java | 6 -- .../org/broad/igv/util/FeatureCacheTest.java | 36 +++++++++ test/data/bedpe/interleaved_chrs.bedpe | 78 +++++++++++++++++++ 5 files changed, 122 insertions(+), 9 deletions(-) delete mode 100644 src/test/java/org/broad/igv/util/DataInputStreamWrapper.java create mode 100644 src/test/java/org/broad/igv/util/FeatureCacheTest.java create mode 100644 test/data/bedpe/interleaved_chrs.bedpe diff --git a/src/main/java/org/broad/igv/feature/FeatureUtils.java b/src/main/java/org/broad/igv/feature/FeatureUtils.java index 213ebeff54..edbb7019a0 100644 --- a/src/main/java/org/broad/igv/feature/FeatureUtils.java +++ b/src/main/java/org/broad/igv/feature/FeatureUtils.java @@ -31,6 +31,7 @@ package org.broad.igv.feature; import com.google.common.base.Predicate; +import htsjdk.samtools.util.Locatable; import htsjdk.tribble.Feature; import java.util.*; @@ -49,7 +50,7 @@ public static Predicate getOverlapPredicate(final String chr, final int /** * Sort the feature list by ascending start value */ - public static void sortFeatureList(List features) { + public static void sortFeatureList(List features) { Collections.sort(features, FEATURE_START_COMPARATOR); } @@ -375,7 +376,7 @@ public static List getAllFeaturesContaining(double position, return returnList; } - public static final Comparator FEATURE_START_COMPARATOR = (o1, o2) -> o1.getStart() - o2.getStart(); + public static final Comparator FEATURE_START_COMPARATOR = (o1, o2) -> o1.getStart() - o2.getStart(); public static final Comparator FEATURE_END_COMPARATOR = (o1, o2) -> o1.getEnd() - o2.getEnd(); public static final Comparator FEATURE_CENTER_COMPARATOR = (o1, o2) -> o1.getStart() - o2.getStart() + o1.getEnd() - o2.getEnd(); diff --git a/src/main/java/org/broad/igv/util/FeatureCache.java b/src/main/java/org/broad/igv/util/FeatureCache.java index fbff5c7bc8..64fd6600ec 100644 --- a/src/main/java/org/broad/igv/util/FeatureCache.java +++ b/src/main/java/org/broad/igv/util/FeatureCache.java @@ -23,7 +23,7 @@ public FeatureCache(List features, int batchSize) { public List getFeatures(String chr, int start, int end) { List features = new ArrayList<>(); IntervalTree> tree = featureMap.get(chr); - if(tree != null) { + if (tree != null) { List>> intervals = tree.findOverlapping(start, end); for (Interval> interval : intervals) { List intervalFeatures = interval.getValue(); @@ -38,6 +38,10 @@ public List getFeatures(String chr, int start, int end) { } private void init(List features, int batchSize) { + + // The feature list must be grouped by chromosome + Collections.sort(features, Comparator.comparing(Locatable::getContig)); + String lastChr = null; List currentFeatureList = new ArrayList<>(); diff --git a/src/test/java/org/broad/igv/util/DataInputStreamWrapper.java b/src/test/java/org/broad/igv/util/DataInputStreamWrapper.java deleted file mode 100644 index 313d4a7558..0000000000 --- a/src/test/java/org/broad/igv/util/DataInputStreamWrapper.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.broad.igv.util; - -public class DataInputStreamWrapper { - - -} diff --git a/src/test/java/org/broad/igv/util/FeatureCacheTest.java b/src/test/java/org/broad/igv/util/FeatureCacheTest.java new file mode 100644 index 0000000000..c1e2ab2394 --- /dev/null +++ b/src/test/java/org/broad/igv/util/FeatureCacheTest.java @@ -0,0 +1,36 @@ +package org.broad.igv.util; + +import org.broad.igv.bedpe.BedPEFeature; +import org.broad.igv.bedpe.BedPEParser; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.*; + +public class FeatureCacheTest { + + @Test + public void testGetFeatures() throws Exception { + + String p = TestUtils.DATA_DIR + "bedpe/interleaved_chrs.bedpe"; + + List features = BedPEParser.parse(new ResourceLocator(p), null).features; + + //chr5:135,300,000-135,399,999 + String chr = "chr5"; + int start = 135300000 - 1; + int end = 135399999; + + // Manually count overlaps + int count = 0; + for (BedPEFeature f : features) { + if (chr.equals(f.getChr()) && f.getEnd() >= start && f.getStart() <= end) count++; + } + + FeatureCache cache = new FeatureCache<>(features); + List subset = cache.getFeatures(chr, start, end); + assertEquals(count, subset.size()); + } + +} \ No newline at end of file diff --git a/test/data/bedpe/interleaved_chrs.bedpe b/test/data/bedpe/interleaved_chrs.bedpe new file mode 100644 index 0000000000..a9d169f2e3 --- /dev/null +++ b/test/data/bedpe/interleaved_chrs.bedpe @@ -0,0 +1,78 @@ +chr5 135399049 135399149 chr5 135398825 135398922 VH01587:178:2223JJ7NX:1:1102:64658:6900 255 - + +chr5 135386568 135386668 chr5 135386578 135386676 VH01587:178:2223JJ7NX:1:1113:42124:15819 255 + - +chr5 135318574 135318616 chr5 135318574 135318616 VH01587:178:2223JJ7NX:1:1207:65121:56177 255 - + +chr5 135386572 135386671 chr5 135386578 135386676 VH01587:178:2223JJ7NX:1:1105:76010:20827 255 + - +chr5 135386572 135386673 chr5 135386581 135386679 VH01587:178:2223JJ7NX:1:1106:50269:28323 255 + - +chr5 135386571 135386671 chr5 135386582 135386680 VH01587:178:2223JJ7NX:1:1203:34612:45526 255 + - +chr5 135340960 135340997 chr5 135454955 135454986 VH01587:178:2223JJ7NX:1:1204:22694:11994 255 + - +chr5 135386571 135386671 chr5 135386582 135386680 VH01587:178:2223JJ7NX:1:1315:61056:22440 255 + - +chr5 135386572 135386673 chr5 135386581 135386679 VH01587:178:2223JJ7NX:1:1315:78360:27053 255 + - +chr5 135386572 135386673 chr5 135386578 135386676 VH01587:178:2223JJ7NX:1:1212:43564:5219 255 + - +chr5 135386572 135386673 chr5 135386578 135386676 VH01587:178:2223JJ7NX:1:1212:6934:19112 255 + - +chr5 135349855 135349954 chr5 135349888 135349986 VH01587:178:2223JJ7NX:1:1316:63869:22131 255 + - +chr5 135318936 135318976 chr5 135318936 135318976 VH01587:178:2223JJ7NX:1:1414:28679:11634 255 - + +chr5 135386572 135386673 chr5 135386582 135386680 VH01587:178:2223JJ7NX:1:1301:39380:24875 255 + - +chr5 135386572 135386673 chr5 135386581 135386679 VH01587:178:2223JJ7NX:1:1301:24357:45491 255 + - +chr5 135386572 135386671 chr5 135386582 135386680 VH01587:178:2223JJ7NX:1:1406:40735:21256 255 + - +chr5 135376747 135378498 chr5 135376778 135378527 VH01587:178:2223JJ7NX:1:1409:18887:21976 255 + - +chr5 135386572 135386673 chr5 135386578 135386676 VH01587:178:2223JJ7NX:1:1409:72855:27894 255 + - +chr5 135386572 135386673 chr5 135386582 135386680 VH01587:178:2223JJ7NX:1:1504:57112:1823 255 + - +chr5 135378511 135378811 chr5 135378584 135378881 VH01587:178:2223JJ7NX:1:1505:62394:41752 255 + - +chr5 135386571 135386668 chr5 135386582 135386680 VH01587:178:2223JJ7NX:1:1516:48263:36881 255 + - +chr5 135399109 135400136 chr5 135398865 135399075 VH01587:178:2223JJ7NX:1:1506:18646:35663 255 - + +chr5 135311209 135311309 chr5 135311253 135311350 VH01587:178:2223JJ7NX:1:1605:6865:31839 255 + - +chr5 135338649 135338746 chr5 135338725 135338822 VH01587:178:2223JJ7NX:1:1503:20842:32851 255 + - +chr5 135386568 135386668 chr5 135386582 135386680 VH01587:178:2223JJ7NX:1:2106:60113:35458 255 + - +chr5 135386572 135386673 chr5 135386578 135386676 VH01587:178:2223JJ7NX:1:2106:44524:54376 255 + - +chr5 135386553 135386654 chr5 135386576 135386671 VH01587:178:2223JJ7NX:1:2107:33309:6351 255 + - +chr5 135311207 135311305 chr5 135311236 135311334 VH01587:178:2223JJ7NX:1:1604:38745:3487 255 + - +chr5 135386572 135386673 chr5 135386582 135386680 VH01587:178:2223JJ7NX:1:1612:66407:36710 255 + - +chr5 135386569 135386668 chr5 135386581 135386679 VH01587:178:2223JJ7NX:1:1615:10809:21719 255 + - +chr5 135386568 135386668 chr5 135386582 135386680 VH01587:178:2223JJ7NX:1:1606:12627:23606 255 + - +chr5 135386568 135386668 chr5 135386582 135386680 VH01587:178:2223JJ7NX:1:1606:13519:23709 255 + - +chr5 135258456 135258545 chr5 135311267 135311350 VH01587:178:2223JJ7NX:1:2110:19092:13298 255 + - +chr5 135386572 135386673 chr5 135386582 135386680 VH01587:178:2223JJ7NX:1:2205:14565:7398 255 + - +chr5 135378506 135378805 chr5 135378567 135378862 VH01587:178:2223JJ7NX:1:2104:34235:30775 255 + - +chr5 135318625 135318658 chr5 135318574 135318658 VH01587:178:2223JJ7NX:1:2113:54780:8375 255 - + +chr5 135386568 135386668 chr5 135386578 135386676 VH01587:178:2223JJ7NX:1:2113:55174:27739 255 + - +chr5_GL456354v1_random 139739 139837 chr5_GL456354v1_random 178521 178615 VH01587:178:2223JJ7NX:1:2309:25575:4310 255 + - +chr5_GL456354v1_random 12993 13041 chr5_GL456354v1_random 12993 13041 VH01587:178:2223JJ7NX:1:1207:78068:27414 255 - + +chr5_GL456354v1_random 12999 13051 chr5_GL456354v1_random 12999 13051 VH01587:178:2223JJ7NX:1:2115:40666:55251 255 - + +chr5_GL456354v1_random 48531 48577 chr5_GL456354v1_random 48531 48577 VH01587:178:2223JJ7NX:1:2212:64383:30055 255 - + +chr5_GL456354v1_random 12994 13051 chr5_GL456354v1_random 12994 13051 VH01587:178:2223JJ7NX:1:2215:68773:37293 255 - + +chr5_GL456354v1_random 12998 13043 chr5_GL456354v1_random 12998 13043 VH01587:180:22255M7NX:1:2408:35813:23057 255 - + +chr5_GL456354v1_random 116075 116110 chr5_GL456354v1_random 116075 116110 VH01587:180:22255M7NX:2:1212:47800:24035 255 - + +chr5_GL456354v1_random 116075 116110 chr5_GL456354v1_random 116075 116110 VH01587:180:22255M7NX:2:1212:47817:24052 255 - + +chr5_GL456354v1_random 116075 116110 chr5_GL456354v1_random 116075 116110 VH01587:180:22255M7NX:2:1212:48589:24686 255 - + +chr5_GL456354v1_random 13000 13041 chr5_GL456354v1_random 13000 13041 VH01587:180:22255M7NX:2:2507:8254:21565 255 - + +chr5 135258456 135258545 chr5 135311267 135311350 VH01587:178:2223JJ7NX:1:2202:23808:53861 255 + - +chr5 135386568 135386668 chr5 135386581 135386679 VH01587:178:2223JJ7NX:1:2214:64349:4499 255 + - +chr5 135373537 135374957 chr5 135375026 135375124 VH01587:178:2223JJ7NX:1:2215:15045:22182 255 + - +chr5 135386568 135386668 chr5 135386578 135386676 VH01587:178:2223JJ7NX:1:2201:43701:45594 255 + - +chr5 135386568 135386668 chr5 135386581 135386679 VH01587:178:2223JJ7NX:1:2202:26175:2201 255 + - +chr5 135386573 135386671 chr5 135386581 135386679 VH01587:178:2223JJ7NX:1:2211:61262:46006 255 + - +chr5 135328512 135328611 chr5 135328578 135328671 VH01587:178:2223JJ7NX:1:2207:55431:17397 255 + - +chr5 135386574 135386675 chr5 135386582 135386680 VH01587:178:2223JJ7NX:1:2312:31217:16059 255 + - +chr5 135386568 135386668 chr5 135386582 135386680 VH01587:178:2223JJ7NX:1:2209:53545:22199 255 + - +chr5 135345904 135346004 chr5 135345930 135346023 VH01587:178:2223JJ7NX:1:2312:24803:44085 255 + - +chr5 135349855 135349954 chr5 135349888 135349986 VH01587:178:2223JJ7NX:1:2316:72529:20399 255 + - +chr5 135258452 135258545 chr5 135311264 135311350 VH01587:178:2223JJ7NX:1:2512:19178:9404 255 + - +chr5 135311209 135311309 chr5 135311253 135311350 VH01587:178:2223JJ7NX:1:2512:6436:21565 255 + - +chr5 135318518 135318554 chr5 135318958 135318984 VH01587:178:2223JJ7NX:1:2505:34441:17363 255 + - +chr5 135327845 135327945 chr5 135327884 135327982 VH01587:178:2223JJ7NX:1:2614:47148:9181 255 + - +chr5 135348373 135348473 chr5 135348400 135348498 VH01587:178:2223JJ7NX:1:2614:38694:49059 255 + - +chr5 135386568 135386668 chr5 135386578 135386676 VH01587:178:2223JJ7NX:1:2616:37510:33365 255 + - +chr5 135318920 135318958 chr5 135318920 135318958 VH01587:178:2223JJ7NX:2:1101:16554:20227 255 - + +chr5 135386572 135386673 chr5 135386581 135386679 VH01587:178:2223JJ7NX:1:2514:45056:3864 255 + - +chr5 135386572 135386673 chr5 135386582 135386680 VH01587:178:2223JJ7NX:1:2607:29948:3195 255 + - +chr5 135375009 135375106 chr5 135376760 135378508 VH01587:178:2223JJ7NX:2:1102:59821:12389 255 + - +chr5 135400144 135400245 chr5 135399136 135400161 VH01587:178:2223JJ7NX:1:2516:50304:43450 255 - + +chr5 135386572 135386673 chr5 135386582 135386680 VH01587:178:2223JJ7NX:2:1211:41849:51906 255 + - +chr5 135324650 135324751 chr5 135324782 135324880 VH01587:178:2223JJ7NX:2:1212:40014:7500 255 + - +chr5 135376732 135378484 chr5 135376779 135378528 VH01587:178:2223JJ7NX:2:1109:15199:38391 255 + - +chr5 135318779 135318815 chr5 135318779 135318815 VH01587:178:2223JJ7NX:2:1205:28353:4447 255 - + +chr5 135386572 135386673 chr5 135386578 135386676 VH01587:178:2223JJ7NX:2:1115:19984:19987 255 + - +chr5 135386587 135386626 chr5 135386587 135386626 VH01587:178:2223JJ7NX:2:1113:54865:4825 255 - + +chr5 135366591 135366688 chr5 135366629 135366726 VH01587:178:2223JJ7NX:2:1114:57489:12080 255 + - +chr5 135318595 135318639 chr5 135318595 135318639 VH01587:178:2223JJ7NX:2:1213:45502:19541 255 - + + From bc00717bf648a2b75db4afe64509d2a01e7d2edf Mon Sep 17 00:00:00 2001 From: Jim Robinson <933148+jrobinso@users.noreply.github.com> Date: Sat, 4 Jan 2025 20:02:00 -0800 Subject: [PATCH 096/130] Interaction track improvements (#1635) * Reduce alpha from 0.05 to 0.02 for proportional arcs consistent with igv.js * Add options to filter arcs with one or both ends out of view --- .../org/broad/igv/bedpe/BedPERenderer.java | 2 +- .../org/broad/igv/bedpe/InteractionTrack.java | 33 +++++++++++++++++-- .../broad/igv/bedpe/NestedArcRenderer.java | 9 ++++- .../org/broad/igv/bedpe/PEBlockRenderer.java | 2 +- .../igv/bedpe/ProportionalArcRenderer.java | 18 ++++++---- 5 files changed, 53 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/broad/igv/bedpe/BedPERenderer.java b/src/main/java/org/broad/igv/bedpe/BedPERenderer.java index 1ecaf42eae..d51b09a268 100644 --- a/src/main/java/org/broad/igv/bedpe/BedPERenderer.java +++ b/src/main/java/org/broad/igv/bedpe/BedPERenderer.java @@ -6,5 +6,5 @@ import java.util.List; public interface BedPERenderer { - void render(List features, RenderContext context, Rectangle trackRectangle); + void render(List features, RenderContext context, Rectangle trackRectangle, InteractionTrack.ArcOption arcOption); } diff --git a/src/main/java/org/broad/igv/bedpe/InteractionTrack.java b/src/main/java/org/broad/igv/bedpe/InteractionTrack.java index 890721cb6e..6e04214119 100644 --- a/src/main/java/org/broad/igv/bedpe/InteractionTrack.java +++ b/src/main/java/org/broad/igv/bedpe/InteractionTrack.java @@ -54,9 +54,12 @@ enum Direction {UP, DOWN} enum GraphType {BLOCK, NESTED_ARC, PROPORTIONAL_ARC} + enum ArcOption {ALL, ONE_END, BOTH_ENDS} + private Genome genome; InteractionTrack.Direction direction = UP; //DOWN; GraphType graphType; // GraphType.block; // + private ArcOption arcOption = ArcOption.ALL; int thickness = 1; boolean autoscale = true; double maxScore = -1; @@ -183,10 +186,10 @@ public void render(RenderContext context, Rectangle trackRectangle) { drawScale(context, trackRectangle); } - renderers.get(graphType).render(features, context, trackRectangle); + renderers.get(graphType).render(features, context, trackRectangle, this.arcOption); } if (showBlocks) { - renderers.get(GraphType.BLOCK).render(features, context, trackRectangle); + renderers.get(GraphType.BLOCK).render(features, context, trackRectangle, this.arcOption); } } finally { @@ -291,6 +294,26 @@ public IGVPopupMenu getPopupMenu(TrackClickEvent te) { menu.add(mm); } + menu.addSeparator(); + menu.add(new JLabel("Arcs")); + ButtonGroup group2 = new ButtonGroup(); + Map modes2 = new LinkedHashMap<>(4); + modes2.put("All", ArcOption.ALL); + modes2.put("One End In View", ArcOption.ONE_END); + modes2.put("Both Ends In View", ArcOption.BOTH_ENDS); + //modes.put("Blocks", GraphType.BLOCK); + + for (final Map.Entry entry : modes2.entrySet()) { + JRadioButtonMenuItem mm = new JRadioButtonMenuItem(entry.getKey()); + mm.setSelected(InteractionTrack.this.arcOption == entry.getValue()); + mm.addActionListener(evt -> { + InteractionTrack.this.arcOption = (entry.getValue()); + repaint(); + }); + group2.add(mm); + menu.add(mm); + } + menu.addSeparator(); JCheckBoxMenuItem showBlocksCB = new JCheckBoxMenuItem("Show Blocks"); showBlocksCB.setSelected(showBlocks); @@ -420,6 +443,7 @@ public void marshalXML(Document document, Element element) { element.setAttribute("direction", String.valueOf(direction)); element.setAttribute("thickness", String.valueOf(thickness)); element.setAttribute("graphType", String.valueOf(graphType)); + element.setAttribute("arcOption", String.valueOf(arcOption)); element.setAttribute("showBlocks", String.valueOf(showBlocks)); element.setAttribute("autoscale", String.valueOf(autoscale)); if (!autoscale) { @@ -433,6 +457,10 @@ public void unmarshalXML(Element element, Integer version) { super.unmarshalXML(element, version); + if (element.hasAttribute("arcOption")) { + String typeString = element.getAttribute("arcOption").toUpperCase(); + this.arcOption = ArcOption.valueOf(typeString); + } if (element.hasAttribute("direction")) this.direction = Direction.valueOf(element.getAttribute("direction")); if (element.hasAttribute("thickness")) @@ -555,4 +583,5 @@ private List downsampleWGFeatures(List features) { return sampledFeatures; } + } diff --git a/src/main/java/org/broad/igv/bedpe/NestedArcRenderer.java b/src/main/java/org/broad/igv/bedpe/NestedArcRenderer.java index 4d79214f9f..86085c2b02 100644 --- a/src/main/java/org/broad/igv/bedpe/NestedArcRenderer.java +++ b/src/main/java/org/broad/igv/bedpe/NestedArcRenderer.java @@ -25,7 +25,7 @@ public NestedArcRenderer(InteractionTrack track) { this.track = track; } - public void render(List features, RenderContext context, Rectangle trackRectangle) { + public void render(List features, RenderContext context, Rectangle trackRectangle, InteractionTrack.ArcOption arcOption) { Graphics2D g = null; @@ -76,6 +76,13 @@ public void render(List features, RenderContext context, Rectangle trackR double pixelStart = (feature.getMidStart() - origin) / locScale; double pixelEnd = (feature.getMidEnd() - origin) / locScale; + // Optionally filter arcs with one or both ends out of view + if(arcOption == InteractionTrack.ArcOption.ONE_END) { + if(pixelStart < trackRectangle.x && pixelEnd > trackRectangle.x + trackRectangle.width) continue; + } else if(arcOption == InteractionTrack.ArcOption.BOTH_ENDS) { + if(pixelStart < trackRectangle.x || pixelEnd > trackRectangle.x + trackRectangle.width) continue; + } + int w = (int) (pixelEnd - pixelStart); if (w < 3) { w = 3; diff --git a/src/main/java/org/broad/igv/bedpe/PEBlockRenderer.java b/src/main/java/org/broad/igv/bedpe/PEBlockRenderer.java index 4f41005278..a9a7223d64 100644 --- a/src/main/java/org/broad/igv/bedpe/PEBlockRenderer.java +++ b/src/main/java/org/broad/igv/bedpe/PEBlockRenderer.java @@ -15,7 +15,7 @@ public PEBlockRenderer(InteractionTrack track) { } @Override - public void render(List features, RenderContext context, Rectangle trackRectangle) { + public void render(List features, RenderContext context, Rectangle trackRectangle, InteractionTrack.ArcOption arcOption) { Graphics2D g = null; diff --git a/src/main/java/org/broad/igv/bedpe/ProportionalArcRenderer.java b/src/main/java/org/broad/igv/bedpe/ProportionalArcRenderer.java index abbf31072e..4b86727dc4 100644 --- a/src/main/java/org/broad/igv/bedpe/ProportionalArcRenderer.java +++ b/src/main/java/org/broad/igv/bedpe/ProportionalArcRenderer.java @@ -13,6 +13,7 @@ public class ProportionalArcRenderer implements BedPERenderer { + public static final float PROP_ALPHA = 0.02f; private Map alphaColors = new HashMap<>(); InteractionTrack track; @@ -21,7 +22,7 @@ public ProportionalArcRenderer(InteractionTrack track) { this.track = track; } - public void render(List features, RenderContext context, Rectangle trackRectangle) { + public void render(List features, RenderContext context, Rectangle trackRectangle, InteractionTrack.ArcOption arcOption) { Graphics2D g = null; @@ -46,8 +47,6 @@ public void render(List features, RenderContext context, Rectangle trackR int gap = track.gap; int h = trackRectangle.height - gap; - - if (track.maxScore > 0 && bedPE.getScore() > 0) { double logMax = Math.log10(track.maxScore + 1); h = (int) ((Math.log10(bedPE.getScore() + 1) / logMax) * h); @@ -56,6 +55,7 @@ public void render(List features, RenderContext context, Rectangle trackR if (bedPE.isSameChr()) { BedPEFeature feature = bedPE.get(); + Color fcolor = feature.color == null ? trackColor : feature.color; if (fcolor != null) { g.setColor(fcolor); @@ -63,14 +63,20 @@ public void render(List features, RenderContext context, Rectangle trackR double pixelStart = (feature.getMidStart() - origin) / locScale; double pixelEnd = (feature.getMidEnd() - origin) / locScale; + + // Optionally filter arcs with one or both ends out of view + if(arcOption == InteractionTrack.ArcOption.ONE_END) { + if(pixelStart < trackRectangle.x && pixelEnd > trackRectangle.x + trackRectangle.width) continue; + } else if(arcOption == InteractionTrack.ArcOption.BOTH_ENDS) { + if(pixelStart < trackRectangle.x || pixelEnd > trackRectangle.x + trackRectangle.width) continue; + } + int w = (int) (pixelEnd - pixelStart); if (w < 3) { w = 3; pixelStart--; } - - double y = direction == UP ? gap + trackRectangle.y + trackRectangle.height - h : gap + trackRectangle.y - h; int angleSt = direction == UP ? 0 : 180; Arc2D.Double arcPath = new Arc2D.Double( @@ -84,7 +90,7 @@ public void render(List features, RenderContext context, Rectangle trackR ); g.draw(arcPath); - Color shadedColor = getAlphaColor(fcolor, 0.05f); + Color shadedColor = getAlphaColor(fcolor, PROP_ALPHA); g.setColor(shadedColor); g.fill(arcPath); From 95350f6bef7c3bbb36bf7b8a68f7d39c7b0934a4 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Mon, 13 Jan 2025 22:48:18 -0800 Subject: [PATCH 097/130] Determine bed file sub-type from track line when specified --- .../org/broad/igv/track/FileFormatUtils.java | 8 +++++ .../java/org/broad/igv/track/TrackLoader.java | 34 +++++++++++++------ .../org/broad/igv/track/TrackProperties.java | 12 ++++++- .../java/org/broad/igv/util/ParsingUtils.java | 4 ++- test/data/peak/broadPeak.bed | 5 +++ test/data/peak/gappedPeak.bed | 2 ++ test/data/peak/narrowPeak.bed | 5 +++ test/data/wig/bedgraph.bed | 14 ++++++++ 8 files changed, 72 insertions(+), 12 deletions(-) create mode 100644 test/data/peak/broadPeak.bed create mode 100644 test/data/peak/gappedPeak.bed create mode 100644 test/data/peak/narrowPeak.bed create mode 100644 test/data/wig/bedgraph.bed diff --git a/src/main/java/org/broad/igv/track/FileFormatUtils.java b/src/main/java/org/broad/igv/track/FileFormatUtils.java index b92f9f4a41..2f021ed8c7 100644 --- a/src/main/java/org/broad/igv/track/FileFormatUtils.java +++ b/src/main/java/org/broad/igv/track/FileFormatUtils.java @@ -9,6 +9,8 @@ import java.io.*; import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; import java.util.zip.GZIPInputStream; public class FileFormatUtils { @@ -18,6 +20,8 @@ public class FileFormatUtils { static final long BIGWIG_MAGIC = 2291137574l; // BigWig Magic static final long BIGBED_MAGIC = 2273964779l; // BigBed Magic + private static Set knownBedFormats = Set.of("interact", "broadpeak", "narrowpeak", "gappedpeak", "regionpeak", "bedgraph"); + public static boolean isBAM(String path) throws IOException { SeekableStream seekableStream = IGVSeekableStreamFactory.getInstance().getStreamFor(path); if (isGzip(seekableStream)) { @@ -104,6 +108,8 @@ public static String determineFormat(String path) throws IOException { ParsingUtils.parseTrackLine(nextLine, properties); if(properties.getFormat() != null) { return properties.getFormat(); + } else if(properties.getType() != null && knownBedFormats.contains(properties.getType())) { + return properties.getType(); } } if(nextLine.startsWith("fixedStep") || nextLine.startsWith("variableStep")) { @@ -211,5 +217,7 @@ private static int getType(int num) { return -1; } + + } diff --git a/src/main/java/org/broad/igv/track/TrackLoader.java b/src/main/java/org/broad/igv/track/TrackLoader.java index 66e02b15ec..6d33c01a1f 100644 --- a/src/main/java/org/broad/igv/track/TrackLoader.java +++ b/src/main/java/org/broad/igv/track/TrackLoader.java @@ -130,6 +130,20 @@ public List load(ResourceLocator locator, Genome genome) throws DataLoadE String format = locator.getFormat(); + // Bed format has some specific subtypes, which are determined by examining the file contents + if ("bed".equals(format)) { + try { + String tmp = FileFormatUtils.determineFormat(locator.getPath()); + if(tmp != null) { + format = tmp; + locator.setFormat(format); + } + } catch (IOException ex) { + log.error("Error determining file format ", ex); + // Do nothing, format remains generic "bed" + } + } + if (format.equals("tbi")) { MessageUtils.showMessage("Error:File type '.tbi' is not recognized. If this is a 'tabix' index
" + " load the associated gzipped file, which should have an extension of '.gz'"); @@ -228,11 +242,11 @@ public List load(ResourceLocator locator, Genome genome) throws DataLoadE } else { // Try overriding format - String determinedFormat = FileFormatUtils.determineFormat(locator.getPath()); - if(determinedFormat != null && !determinedFormat.equals("sampleinfo") && !determinedFormat.equals(format)) { - locator.setFormat(determinedFormat); - return load(locator, genome); - } + String determinedFormat = FileFormatUtils.determineFormat(locator.getPath()); + if (determinedFormat != null && !determinedFormat.equals("sampleinfo") && !determinedFormat.equals(format)) { + locator.setFormat(determinedFormat); + return load(locator, genome); + } // If the file is too large, give up @@ -481,11 +495,11 @@ private void loadTribbleFile(ResourceLocator locator, List newTracks, Gen t.setHeight(15); } } - String path = locator.getPath().toLowerCase(); - if (path.contains(".narrowpeak") || - locator.getPath().contains(".broadpeak") || - locator.getPath().contains(".gappedpeak") || - locator.getPath().contains(".regionpeak")) { + + if (format.equals(".narrowpeak") || + locator.getPath().equals(".broadpeak") || + locator.getPath().equals(".gappedpeak") || + locator.getPath().equals(".regionpeak")) { t.setUseScore(true); } newTracks.add(t); diff --git a/src/main/java/org/broad/igv/track/TrackProperties.java b/src/main/java/org/broad/igv/track/TrackProperties.java index 96658053f7..e8cfd70d8e 100644 --- a/src/main/java/org/broad/igv/track/TrackProperties.java +++ b/src/main/java/org/broad/igv/track/TrackProperties.java @@ -45,7 +45,7 @@ public class TrackProperties { private static Logger log = LogManager.getLogger(TrackProperties.class); - private String trackLine; + public enum BaseCoord { ZERO, ONE, UNSPECIFIED @@ -59,6 +59,9 @@ public enum BaseCoord { */ private BaseCoord baseCoord = BaseCoord.UNSPECIFIED; + private String trackLine; + private String type; + /** * The track name. Will be displayed to the left of the track. */ @@ -158,6 +161,13 @@ public TrackProperties() { } + public void setType(String type) { + this.type = type; + } + + public String getType() { + return type; + } public String getFormat() { return format; } diff --git a/src/main/java/org/broad/igv/util/ParsingUtils.java b/src/main/java/org/broad/igv/util/ParsingUtils.java index 68d72fe00d..6ffd495add 100644 --- a/src/main/java/org/broad/igv/util/ParsingUtils.java +++ b/src/main/java/org/broad/igv/util/ParsingUtils.java @@ -387,7 +387,9 @@ public static boolean parseTrackLine(String nextLine, TrackProperties trackPrope String value = kv.get(1).replaceAll("\"", ""); if(key.equals("format")) { - trackProperties.setFormat(value); + trackProperties.setFormat(value.toLowerCase()); + } else if(key.equals("type")) { + trackProperties.setType(value.toLowerCase()); } else if (key.equals("coords")) { if (value.equals("0")) { diff --git a/test/data/peak/broadPeak.bed b/test/data/peak/broadPeak.bed new file mode 100644 index 0000000000..07feccfa75 --- /dev/null +++ b/test/data/peak/broadPeak.bed @@ -0,0 +1,5 @@ +track type=broadPeak visibility=3 db=hg19 name="bPk" description="ENCODE broadPeak Example" +browser position chr1:798200-800700 +chr1 798256 798454 . 116 . 4.89716 3.70716 -1 +chr1 799435 799507 . 103 . 2.46426 1.54117 -1 +chr1 800141 800596 . 107 . 3.22803 2.12614 -1 diff --git a/test/data/peak/gappedPeak.bed b/test/data/peak/gappedPeak.bed new file mode 100644 index 0000000000..ebcbcf236b --- /dev/null +++ b/test/data/peak/gappedPeak.bed @@ -0,0 +1,2 @@ +track name=gappedPeakExample type=gappedPeak +chr1 171000 171600 Anon_peak_1 55 . 0 0 0 2 400,100 0,500 4.04761 7.53255 5.52807 diff --git a/test/data/peak/narrowPeak.bed b/test/data/peak/narrowPeak.bed new file mode 100644 index 0000000000..d8b3bfdd12 --- /dev/null +++ b/test/data/peak/narrowPeak.bed @@ -0,0 +1,5 @@ +track type=narrowPeak visibility=3 db=hg19 name="nPk" description="ENCODE narrowPeak Example" +browser position chr1:9356000-9365000 +chr1 9356548 9356648 . 0 . 182 5.0945 -1 50 +chr1 9358722 9358822 . 0 . 91 4.6052 -1 40 +chr1 9361082 9361182 . 0 . 182 9.2103 -1 75 diff --git a/test/data/wig/bedgraph.bed b/test/data/wig/bedgraph.bed new file mode 100644 index 0000000000..71f09d7424 --- /dev/null +++ b/test/data/wig/bedgraph.bed @@ -0,0 +1,14 @@ +# File for testing inference of bedgraph format from track line +# locus chr19:49302001-49304701 +# refGene encodeRegions +# zero-based, half-open coords +track type=bedGraph name="BedGraph Format" description="BedGraph format" visibility=full color=200,100,0 altColor=0,100,200 priority=20 +chr19 49302000 49302300 -1.0 +chr19 49302300 49302600 -0.75 +chr19 49302600 49302900 -0.50 +chr19 49302900 49303200 -0.25 +chr19 49303200 49303500 0.0 +chr19 49303500 49303800 0.25 +chr19 49303800 49304100 0.50 +chr19 49304100 49304400 0.75 +chr19 49304400 49304700 1.00 From 3d4b47eb040d97b7ee270df1885a6fdd3776894c Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Mon, 13 Jan 2025 23:17:18 -0800 Subject: [PATCH 098/130] fix logic for bed file type determination --- src/main/java/org/broad/igv/track/TrackLoader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/broad/igv/track/TrackLoader.java b/src/main/java/org/broad/igv/track/TrackLoader.java index 6d33c01a1f..ad43440064 100644 --- a/src/main/java/org/broad/igv/track/TrackLoader.java +++ b/src/main/java/org/broad/igv/track/TrackLoader.java @@ -134,7 +134,7 @@ public List load(ResourceLocator locator, Genome genome) throws DataLoadE if ("bed".equals(format)) { try { String tmp = FileFormatUtils.determineFormat(locator.getPath()); - if(tmp != null) { + if(tmp != null && !tmp.equals("sampleinfo")) { format = tmp; locator.setFormat(format); } From 6429dac7d441ca73d207a131e40964dab3b48f2b Mon Sep 17 00:00:00 2001 From: Jim Robinson <933148+jrobinso@users.noreply.github.com> Date: Thu, 16 Jan 2025 20:24:02 -0800 Subject: [PATCH 099/130] S3 (#1639) S3 updates * Support custom, non-AWS, endpoint * Simplification of oAuth code * Remove custom S3Presigner code, this is now included in the SDK * Add AWS endpoint url preference * Allow selection of file indexes from S3 load dialog --- .../java/org/broad/igv/DirectoryManager.java | 6 +- .../java/org/broad/igv/aws/S3LoadDialog.java | 64 ++- .../broad/igv/lists/GeneListManagerUI.java | 2 +- .../org/broad/igv/oauth/OAuthProvider.java | 41 +- .../java/org/broad/igv/prefs/Constants.java | 4 +- .../org/broad/igv/prefs/IGVPreferences.java | 4 +- .../broad/igv/prefs/PreferencesEditor.java | 4 +- .../java/org/broad/igv/track/TrackLoader.java | 5 - .../java/org/broad/igv/ui/IGVMenuBar.java | 3 +- .../ui/action/ExportRegionsMenuAction.java | 2 +- .../igv/ui/action/LoadFromURLMenuAction.java | 25 +- .../broad/igv/ui/util/FileDialogUtils.java | 3 +- .../java/org/broad/igv/util/AmazonUtils.java | 464 +++++++++++------- .../java/org/broad/igv/util/HttpUtils.java | 3 +- .../java/org/broad/igv/util/S3Presigner.java | 195 -------- .../broad/igv/util/blat/BlatQueryWindow.java | 2 +- .../org/broad/igv/prefs/preferences.tab | 4 +- 17 files changed, 385 insertions(+), 446 deletions(-) delete mode 100644 src/main/java/org/broad/igv/util/S3Presigner.java diff --git a/src/main/java/org/broad/igv/DirectoryManager.java b/src/main/java/org/broad/igv/DirectoryManager.java index 994c274c4b..af84bbb30d 100644 --- a/src/main/java/org/broad/igv/DirectoryManager.java +++ b/src/main/java/org/broad/igv/DirectoryManager.java @@ -61,7 +61,7 @@ public class DirectoryManager { final public static String IGV_DIR_USERPREF = "igvDir"; - private static File getUserHome() { + public static File getUserHome() { if (USER_HOME == null) { String userHomeString = System.getProperty("user.home"); USER_HOME = new File(userHomeString); @@ -73,7 +73,7 @@ private static File getUserHome() { * The user directory. On Mac and Linux this should be the user home directory. On Windows platforms this * is the "My Documents" directory. */ - public static synchronized File getUserDirectory() { + public static synchronized File getUserDefaultDirectory() { if (USER_DIRECTORY == null) { USER_DIRECTORY = FileSystemView.getFileSystemView().getDefaultDirectory(); //Mostly for testing, in some environments USER_DIRECTORY can be null @@ -96,7 +96,7 @@ public static File getIgvDirectory() { // Hack for known Java / Windows bug. Attempt to remvoe (possible) read-only bit from user directory if (System.getProperty("os.name").startsWith("Windows")) { try { - Runtime.getRuntime().exec("attrib -r \"" + getUserDirectory().getAbsolutePath() + "\""); + Runtime.getRuntime().exec("attrib -r \"" + getUserDefaultDirectory().getAbsolutePath() + "\""); } catch (Exception e) { // We tried } diff --git a/src/main/java/org/broad/igv/aws/S3LoadDialog.java b/src/main/java/org/broad/igv/aws/S3LoadDialog.java index 5f4a6b8700..957f216799 100644 --- a/src/main/java/org/broad/igv/aws/S3LoadDialog.java +++ b/src/main/java/org/broad/igv/aws/S3LoadDialog.java @@ -44,8 +44,7 @@ import java.awt.event.ActionEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; -import java.util.ArrayList; -import java.util.Collections; +import java.util.*; import java.util.List; import software.amazon.awssdk.services.s3.model.S3Exception; @@ -57,6 +56,8 @@ public class S3LoadDialog extends org.broad.igv.ui.IGVDialog { private static Logger log = LogManager.getLogger(S3LoadDialog.class); + final static Set indexExtensions = Set.of("bai", "csi", "crai", "idx", "tbi"); + private final DefaultTreeModel treeModel; String selectedId; @@ -102,7 +103,6 @@ private void loadSelections(TreePath[] paths) { LongRunningTask.submit(() -> { ArrayList> preLocatorPaths = new ArrayList<>(); - ArrayList finalLocators = new ArrayList<>(); for (TreePath path : paths) { if (isFilePath(path)) { @@ -118,15 +118,12 @@ private void loadSelections(TreePath[] paths) { } } - for (Triple preLocator : preLocatorPaths) { - ResourceLocator locator = getResourceLocatorFromBucketKey(preLocator); - finalLocators.add(locator); - } + List locators = getResourceLocatorsFromBucketKeys(preLocatorPaths); - if (finalLocators.size() == 1 && "xml".equals(ResourceLocator.deriveFormat(finalLocators.get(0).getPath()))) { - IGV.getInstance().loadSession(finalLocators.get(0).getPath(), null); + if (locators.size() == 1 && "xml".equals(ResourceLocator.deriveFormat(locators.get(0).getPath()))) { + IGV.getInstance().loadSession(locators.get(0).getPath(), null); } else { - IGV.getInstance().loadTracks(finalLocators); + IGV.getInstance().loadTracks(locators); } }); } @@ -135,11 +132,48 @@ private boolean isFilePath(TreePath path) { return ((S3TreeNode) path.getLastPathComponent()).isLeaf(); } - private ResourceLocator getResourceLocatorFromBucketKey(Triple preLocator) { - String bucketName = preLocator.getLeft(); - String s3objPath = preLocator.getMiddle(); - String s3Path = "s3://" + bucketName + "/" + s3objPath; - return new ResourceLocator(s3Path); + private List getResourceLocatorsFromBucketKeys(List> bucketKeys) { + + Map indexMap = new HashMap<>(); + List locators = new ArrayList<>(); + for (Triple bucketKey : bucketKeys) { + + String s3Path = "s3://" + bucketKey.getLeft() + "/" + bucketKey.getMiddle(); + + int idx = s3Path.lastIndexOf('.'); + String ext = idx > 0 && idx < s3Path.length() - 1 ? s3Path.substring(idx + 1) : ""; + + if (indexExtensions.contains(ext)) { + String key = s3Path.substring(0, idx); + indexMap.put(key, s3Path); + } else { + locators.add(new ResourceLocator(s3Path)); + } + + if (indexMap.size() > 0) { + for (ResourceLocator locator : locators) { + String key = locator.getPath(); + if (indexMap.containsKey(key)) { + locator.setIndexPath(indexMap.get(key)); + } else if (key.endsWith(".bam")) { + + // Special case for "Picard" which uses a non standard index naming convention + key = key.substring(0, key.length() - 4); + if (indexMap.containsKey(key)) { + locator.setIndexPath(indexMap.get(key)); + } + } else if (key.endsWith(".cram")) { + + // Special case for "Picard" which uses a non standard index naming convention + key = key.substring(0, key.length() - 5); + if (indexMap.containsKey(key)) { + locator.setIndexPath(indexMap.get(key)); + } + } + } + } + } + return locators; } private Triple getBucketKeyTierFromTreePath(TreePath path) { diff --git a/src/main/java/org/broad/igv/lists/GeneListManagerUI.java b/src/main/java/org/broad/igv/lists/GeneListManagerUI.java index da1c21e7ef..501a891077 100644 --- a/src/main/java/org/broad/igv/lists/GeneListManagerUI.java +++ b/src/main/java/org/broad/igv/lists/GeneListManagerUI.java @@ -338,7 +338,7 @@ private void importButtonActionPerformed(ActionEvent e) { */ private void exportButtonActionPerformed(ActionEvent e) { if (selectedGroup != null) { - File userDir = DirectoryManager.getUserDirectory(); + File userDir = DirectoryManager.getUserDefaultDirectory(); File initFile = new File(selectedGroup + ".gmt"); File glFile = FileDialogUtils.chooseFile("Save gene lists", userDir, initFile, FileDialogUtils.SAVE); if (glFile != null) { diff --git a/src/main/java/org/broad/igv/oauth/OAuthProvider.java b/src/main/java/org/broad/igv/oauth/OAuthProvider.java index 9e0734ee5a..3bdda7f003 100644 --- a/src/main/java/org/broad/igv/oauth/OAuthProvider.java +++ b/src/main/java/org/broad/igv/oauth/OAuthProvider.java @@ -230,16 +230,6 @@ public void fetchAccessToken(String authorizationCode) throws IOException { fetchUserProfile(payload); } - if (authProvider != null && "Amazon".equals(authProvider)) { - // Get AWS credentials after getting relevant tokens - Credentials aws_credentials; - aws_credentials = AmazonUtils.GetCognitoAWSCredentials(); - - // Update S3 client with newly acquired token - AmazonUtils.updateS3Client(aws_credentials); - } - - // Notify UI that we are authz'd/authn'd if (isLoggedIn()) { IGVEventBus.getInstance().post(new AuthStateEvent(true, this.authProvider, this.getCurrentUserName())); @@ -247,7 +237,6 @@ public void fetchAccessToken(String authorizationCode) throws IOException { } catch (Exception e) { log.error(e); - e.printStackTrace(); } } @@ -256,7 +245,7 @@ public void setAccessToken(String accessToken) { } /** - * Fetch a new access token from a refresh token. + * Fetch a new access token from a refresh token. Unlike authorization, this is a synchronous operation * * @throws IOException */ @@ -293,18 +282,15 @@ private void refreshAccessToken() throws IOException { expirationTime = System.currentTimeMillis() + response.getAsJsonPrimitive("expires_in").getAsInt() * 1000; } else { // Refresh token has failed, reauthorize from scratch - reauthorize(); + logout(); + try { + openAuthorizationPage(); + } catch (URISyntaxException e) { + e.printStackTrace(); + } } } - private void reauthorize() throws IOException { - logout(); - try { - openAuthorizationPage(); - } catch (URISyntaxException e) { - e.printStackTrace(); - } - } /** * Extract user information from the claim information @@ -374,6 +360,15 @@ public void logout() { IGVEventBus.getInstance().post(new AuthStateEvent(false, this.authProvider, null)); } + public JsonObject getAuthorizationResponse() { + + if (response == null) { + // Go back to auth flow, not auth'd yet + checkLogin(); + response = getResponse(); + } + return response; + } /** * If not logged in, attempt to login @@ -390,10 +385,10 @@ public synchronized void checkLogin() { } } - // wait until authentication successful or 1 minute - + // wait until authentication successful or 2 minutes - // dwm08 int i = 0; - while (!isLoggedIn() && i < 600) { + while (!isLoggedIn() && i < 1200) { ++i; try { Thread.sleep(100); diff --git a/src/main/java/org/broad/igv/prefs/Constants.java b/src/main/java/org/broad/igv/prefs/Constants.java index 28f12ead80..22463a29f0 100644 --- a/src/main/java/org/broad/igv/prefs/Constants.java +++ b/src/main/java/org/broad/igv/prefs/Constants.java @@ -296,10 +296,12 @@ private Constants() { public static final String DB_NAME = "DB_NAME"; public static final String DB_PORT = "DB_PORT"; - // OAuth provisioning + // OAuth and AWS public static final String PROVISIONING_URL = "PROVISIONING.URL"; public static final String PROVISIONING_URL_DEFAULT = "PROVISIONING_URL_DEFAULT"; + public static final String AWS_ENDPOINT_URL = "AWS_ENDPOINT_URL"; + // JBrowse circular view integration public static final String CIRC_VIEW_ENABLED = "CIRC_VIEW_ENABLED"; public static final String CIRC_VIEW_PORT = "CIRC_VIEW_PORT"; diff --git a/src/main/java/org/broad/igv/prefs/IGVPreferences.java b/src/main/java/org/broad/igv/prefs/IGVPreferences.java index 497b9e2ca6..9118c1afd5 100644 --- a/src/main/java/org/broad/igv/prefs/IGVPreferences.java +++ b/src/main/java/org/broad/igv/prefs/IGVPreferences.java @@ -526,7 +526,7 @@ public File getDefineGenomeInputDirectory() { File directory = null; - String lastFilePath = get(DEFINE_GENOME_INPUT_DIRECTORY_KEY, DirectoryManager.getUserDirectory().getAbsolutePath()); + String lastFilePath = get(DEFINE_GENOME_INPUT_DIRECTORY_KEY, DirectoryManager.getUserDefaultDirectory().getAbsolutePath()); if (lastFilePath != null) { directory = new File(lastFilePath); @@ -550,7 +550,7 @@ public File getLastGenomeImportDirectory() { File genomeImportDirectory = null; - String lastFilePath = get(LAST_GENOME_IMPORT_DIRECTORY, DirectoryManager.getUserDirectory().getAbsolutePath()); + String lastFilePath = get(LAST_GENOME_IMPORT_DIRECTORY, DirectoryManager.getUserDefaultDirectory().getAbsolutePath()); if (lastFilePath != null) { genomeImportDirectory = new File(lastFilePath); diff --git a/src/main/java/org/broad/igv/prefs/PreferencesEditor.java b/src/main/java/org/broad/igv/prefs/PreferencesEditor.java index fc30673000..4224aafb12 100644 --- a/src/main/java/org/broad/igv/prefs/PreferencesEditor.java +++ b/src/main/java/org/broad/igv/prefs/PreferencesEditor.java @@ -289,7 +289,7 @@ public void focusLost(FocusEvent e) { moveButton.addActionListener(event -> { UIUtilities.invokeOnEventThread(() -> { final File directory = DirectoryManager.getFastaCacheDirectory(); - final File newDirectory = FileDialogUtils.chooseDirectory("Select cache directory", DirectoryManager.getUserDirectory()); + final File newDirectory = FileDialogUtils.chooseDirectory("Select cache directory", DirectoryManager.getUserDefaultDirectory()); if (newDirectory != null && !newDirectory.equals(directory)) { DirectoryManager.moveDirectoryContents(directory, newDirectory); SwingUtilities.invokeLater(() -> currentDirectoryLabel.setText(newDirectory.getAbsolutePath())); @@ -320,7 +320,7 @@ public void focusLost(FocusEvent e) { moveButton.addActionListener(event -> { UIUtilities.invokeOnEventThread(() -> { final File igvDirectory = DirectoryManager.getIgvDirectory(); - final File newDirectory = FileDialogUtils.chooseDirectory("Select IGV directory", DirectoryManager.getUserDirectory()); + final File newDirectory = FileDialogUtils.chooseDirectory("Select IGV directory", DirectoryManager.getUserDefaultDirectory()); if (newDirectory != null && !newDirectory.equals(igvDirectory)) { DirectoryManager.moveIGVDirectory(newDirectory); SwingUtilities.invokeLater(() -> currentDirectoryLabel.setText(newDirectory.getAbsolutePath())); diff --git a/src/main/java/org/broad/igv/track/TrackLoader.java b/src/main/java/org/broad/igv/track/TrackLoader.java index ad43440064..3637a9997e 100644 --- a/src/main/java/org/broad/igv/track/TrackLoader.java +++ b/src/main/java/org/broad/igv/track/TrackLoader.java @@ -120,11 +120,6 @@ public List load(ResourceLocator locator, Genome genome) throws DataLoadE final String path = locator.getPath().trim(); - // Check if the AWS credentials are still valid. If not, re-login and renew pre-signed urls - if (AmazonUtils.isAwsS3Path(path)) { - AmazonUtils.checkLogin(); - } - log.info("Loading resource: " + (locator.isDataURL() ? "" : path)); try { diff --git a/src/main/java/org/broad/igv/ui/IGVMenuBar.java b/src/main/java/org/broad/igv/ui/IGVMenuBar.java index bb59c34f06..6ec97373f7 100644 --- a/src/main/java/org/broad/igv/ui/IGVMenuBar.java +++ b/src/main/java/org/broad/igv/ui/IGVMenuBar.java @@ -67,7 +67,6 @@ import javax.swing.*; import javax.swing.event.MenuEvent; -import javax.swing.event.MenuListener; import javax.swing.plaf.basic.BasicBorders; import java.awt.*; import java.awt.event.ActionEvent; @@ -474,7 +473,7 @@ private JMenu createGenomesMenu() { try { File importDirectory = PreferencesManager.getPreferences().getLastGenomeImportDirectory(); if (importDirectory == null) { - PreferencesManager.getPreferences().setLastGenomeImportDirectory(DirectoryManager.getUserDirectory()); + PreferencesManager.getPreferences().setLastGenomeImportDirectory(DirectoryManager.getUserDefaultDirectory()); } // Display the dialog File file = FileDialogUtils.chooseFile("Load Genome", importDirectory, FileDialog.LOAD); diff --git a/src/main/java/org/broad/igv/ui/action/ExportRegionsMenuAction.java b/src/main/java/org/broad/igv/ui/action/ExportRegionsMenuAction.java index acaa9cb540..a3effe511e 100644 --- a/src/main/java/org/broad/igv/ui/action/ExportRegionsMenuAction.java +++ b/src/main/java/org/broad/igv/ui/action/ExportRegionsMenuAction.java @@ -78,7 +78,7 @@ public final void exportRegionsOfInterest() { File exportRegionDirectory = PreferencesManager.getPreferences().getLastExportedRegionDirectory(); if (exportRegionDirectory == null) { - exportRegionDirectory = DirectoryManager.getUserDirectory(); + exportRegionDirectory = DirectoryManager.getUserDefaultDirectory(); } String title = "Export Regions of Interest ..."; diff --git a/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java b/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java index 12823f77a4..d464afa991 100644 --- a/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java +++ b/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java @@ -109,9 +109,7 @@ private void loadUrls(List inputs, List indexes, boolean isHtsGe } else if (inputs.size() == 1 && SessionReader.isSessionFile(inputs.getFirst())) { // Session URL String url = inputs.getFirst(); - if (url.startsWith("s3://")) { - checkAWSAccessbility(url); - } + try { LongRunningTask.submit(() -> this.igv.loadSession(url, null)); } catch (Exception ex) { @@ -186,29 +184,12 @@ private static boolean isHubURL(String input) { private static void checkURLs(List urls) { for (String url : urls) { - if (url.startsWith("s3://")) { - checkAWSAccessbility(url); - } else if (url.startsWith("ftp://")) { + if (url.startsWith("ftp://")) { MessageUtils.showMessage("FTP protocol is not supported"); } } } - private static void checkAWSAccessbility(String url) { - try { - // If AWS support is active, check if objects are in accessible tiers via Load URL menu... - if (AmazonUtils.isAwsS3Path(url)) { - String bucket = AmazonUtils.getBucketFromS3URL(url); - String key = AmazonUtils.getKeyFromS3URL(url); - AmazonUtils.s3ObjectAccessResult res = isObjectAccessible(bucket, key); - if (!res.isObjectAvailable()) { - MessageUtils.showErrorMessage(res.getErrorReason(), null); - } - } - } catch (NullPointerException npe) { - // User has not yet done Amazon->Login sequence - AmazonUtils.checkLogin(); - } - } + } diff --git a/src/main/java/org/broad/igv/ui/util/FileDialogUtils.java b/src/main/java/org/broad/igv/ui/util/FileDialogUtils.java index d7cb1b24e6..cf840252eb 100644 --- a/src/main/java/org/broad/igv/ui/util/FileDialogUtils.java +++ b/src/main/java/org/broad/igv/ui/util/FileDialogUtils.java @@ -33,7 +33,6 @@ import javax.swing.*; import java.awt.*; import java.io.File; -import java.io.FileFilter; import java.io.FilenameFilter; import java.lang.reflect.Method; @@ -54,7 +53,7 @@ public static File chooseFile(String title, File initialDirectory, int mode) { } public static File chooseFile(String title) { - return chooseFile(title, DirectoryManager.getUserDirectory(), null, FileDialog.LOAD); + return chooseFile(title, DirectoryManager.getUserDefaultDirectory(), null, FileDialog.LOAD); } public static File chooseFile(String title, File initialDirectory, File initialFile, int mode) { diff --git a/src/main/java/org/broad/igv/util/AmazonUtils.java b/src/main/java/org/broad/igv/util/AmazonUtils.java index e832e5a67d..94a374124e 100644 --- a/src/main/java/org/broad/igv/util/AmazonUtils.java +++ b/src/main/java/org/broad/igv/util/AmazonUtils.java @@ -1,17 +1,16 @@ package org.broad.igv.util; import com.google.gson.JsonObject; -import org.broad.igv.Globals; +import org.broad.igv.DirectoryManager; import org.broad.igv.aws.IGVS3Object; import org.broad.igv.oauth.OAuthProvider; import org.broad.igv.oauth.OAuthUtils; import org.broad.igv.logging.LogManager; import org.broad.igv.logging.Logger; +import org.broad.igv.prefs.Constants; +import org.broad.igv.prefs.PreferencesManager; import org.broad.igv.ui.IGVMenuBar; -import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider; -import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; -import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; -import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.auth.credentials.*; import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.core.exception.SdkServiceException; import software.amazon.awssdk.regions.Region; @@ -23,19 +22,24 @@ import software.amazon.awssdk.services.cognitoidentity.model.GetOpenIdTokenRequest; import software.amazon.awssdk.services.cognitoidentity.model.GetOpenIdTokenResponse; import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.S3Configuration; import software.amazon.awssdk.services.s3.model.*; import software.amazon.awssdk.services.s3.paginators.ListObjectsV2Iterable; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; +import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest; import software.amazon.awssdk.services.sts.StsClient; import software.amazon.awssdk.services.sts.model.AssumeRoleWithWebIdentityRequest; import software.amazon.awssdk.services.sts.model.AssumeRoleWithWebIdentityResponse; import software.amazon.awssdk.services.sts.model.Credentials; -import java.io.IOException; -import java.io.UnsupportedEncodingException; +import java.io.*; import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.time.Duration; import java.time.Instant; import java.util.*; import java.util.concurrent.CompletableFuture; @@ -50,24 +54,15 @@ public class AmazonUtils { private static CognitoIdentityClient cognitoIdentityClient; private static Region AWSREGION; private static Boolean awsCredentialsPresent = null; - private static Credentials cognitoAWSCredentials = null; - private static int TOKEN_EXPIRE_GRACE_TIME = 1000 * 60; // 1 minute - - - - /** - * Maps s3:// URLs to presigned URLs - */ private static Map s3ToPresignedMap = new HashMap<>(); - /** - * Maps aws presigned URLs to s3://. This is needed in some cases (e.g. Tribble) to regenerate an expired URL - */ private static Map presignedToS3Map = new HashMap<>(); private static JsonObject CognitoConfig; + private static S3Presigner s3Presigner; + private static String endpointURL = "UNKNOWN"; public static void setCognitoConfig(JsonObject json) { CognitoConfig = json; @@ -97,7 +92,7 @@ public static boolean isAwsProviderPresent() { log.info("AWS configuration found. AWS support enabled."); awsCredentialsPresent = true; } else { - log.info("AWS configuration not found."); + log.info("Cognito configuration found but Amazon auth_provider not defined. Only Amazon provider is supported at this time."); awsCredentialsPresent = false; } } catch (NullPointerException np) { @@ -117,6 +112,11 @@ public static boolean isAwsProviderPresent() { return awsCredentialsPresent; } + /** + * Return the region for AWS credentials + * + * @return + */ private static Region getAWSREGION() { if (AWSREGION == null) { @@ -126,7 +126,12 @@ private static Region getAWSREGION() { // TODO -- find region in default place try { AWSREGION = (new DefaultAwsRegionProviderChain()).getRegion(); + if (AWSREGION == null) { + AWSREGION = Region.US_EAST_1; + log.info("Could not find AWS region setting. Assuming us-east-1"); + } } catch (Exception e) { + log.info("Unable to load region from any of the providers in the chain software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain. Assuming us-east-1"); AWSREGION = Region.US_EAST_1; } } @@ -135,7 +140,7 @@ private static Region getAWSREGION() { } /** - * Returns the AWS credentials + * Retrieve AWS credentials, which may trigger a login. * * @return returns the credentials based on the AWS STS access token returned from the AWS Cognito user pool. */ @@ -145,85 +150,73 @@ public static Credentials GetCognitoAWSCredentials() { log.debug("fetch credentials"); OAuthProvider provider = OAuthUtils.getInstance().getAWSProvider(); - JsonObject igv_oauth_conf = GetCognitoConfig(); - JsonObject response = provider.getResponse(); - - // Handle non-user initiated S3 auth (IGV early startup), i.e user-specified GenomesLoader - if (response == null) { - // Go back to auth flow, not auth'd yet - checkLogin(); - response = provider.getResponse(); - } + JsonObject response = provider.getAuthorizationResponse(); - JsonObject payload = JWTParser.getPayload(response.get("id_token").getAsString()); - - log.debug("JWT payload id token: " + payload); - - // Collect necessary information from federated IdP for Authentication purposes - String idTokenStr = response.get("id_token").getAsString(); - String idProvider = payload.get("iss").toString().replace("https://", "") - .replace("\"", ""); - String email = payload.get("email").getAsString(); - String federatedPoolId = igv_oauth_conf.get("aws_cognito_fed_pool_id").getAsString(); - String cognitoRoleARN = igv_oauth_conf.get("aws_cognito_role_arn").getAsString(); - - HashMap logins = new HashMap<>(); - logins.put(idProvider, idTokenStr); - - // Avoid "software.amazon.awssdk.core.exception.SdkClientException: Unable to load credentials from any of the providers in the chain AwsCredentialsProviderChain(" - // The use of the AnonymousCredentialsProvider essentially bypasses the provider chain's requirement to access ~/.aws/credentials. - // https://stackoverflow.com/questions/36604024/sts-saml-and-java-sdk-unable-to-load-aws-credentials-from-any-provider-in-the-c - AnonymousCredentialsProvider anoCredProv = AnonymousCredentialsProvider.create(); - - // https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/cognitoidentity/CognitoIdentityClient.html - // Build the Cognito client - CognitoIdentityClientBuilder cognitoIdentityBuilder = CognitoIdentityClient.builder(); - - cognitoIdentityBuilder.region(getAWSREGION()).credentialsProvider(anoCredProv); - cognitoIdentityClient = cognitoIdentityBuilder.build(); - - - // https://docs.aws.amazon.com/cognito/latest/developerguide/authentication-flow.html - // Basic (Classic) Authflow - // Uses AssumeRoleWithWebIdentity and facilitates CloudTrail org.broad.igv.logging. Uses one more request but provides user traceability. - GetIdRequest.Builder idrequest = GetIdRequest.builder().identityPoolId(federatedPoolId) - .logins(logins); - GetIdResponse idResult = cognitoIdentityClient.getId(idrequest.build()); - - GetOpenIdTokenRequest.Builder openidrequest = GetOpenIdTokenRequest.builder().logins(logins).identityId(idResult.identityId()); - GetOpenIdTokenResponse openId = cognitoIdentityClient.getOpenIdToken(openidrequest.build()); - - - AssumeRoleWithWebIdentityRequest.Builder webidrequest = AssumeRoleWithWebIdentityRequest.builder().webIdentityToken(openId.token()) - .roleSessionName(email) - .roleArn(cognitoRoleARN); - - AssumeRoleWithWebIdentityResponse stsClientResponse = StsClient.builder().credentialsProvider(anoCredProv) - .region(getAWSREGION()) - .build() - .assumeRoleWithWebIdentity(webidrequest.build()); - -// // Enhanced (Simplified) Authflow -// // Major drawback: Does not store federated user information on CloudTrail only authenticated role name appears in logs. -// -// // "To provide end-user credentials, first make an unsigned call to GetId." -// GetIdRequest.Builder idrequest = GetIdRequest.builder().identityPoolId(federatedPoolId) -// .logins(logins); -// GetIdResponse idResult = cognitoIdentityClient.getId(idrequest.build()); -// -// // "Next, make an unsigned call to GetCredentialsForIdentity." -// GetCredentialsForIdentityRequest.Builder authedIds = GetCredentialsForIdentityRequest.builder(); -// authedIds.identityId(idResult.identityId()).logins(logins); -// -// GetCredentialsForIdentityResponse authedRes = cognitoIdentityClient.getCredentialsForIdentity(authedIds.build()); -// -// return authedRes.credentials() - - cognitoAWSCredentials = stsClientResponse.credentials(); + setCredentialsFromOauthResponse(response); } return cognitoAWSCredentials; } + private static void setCredentialsFromOauthResponse(JsonObject response) { + + JsonObject igv_oauth_conf = GetCognitoConfig(); + + JsonObject payload = JWTParser.getPayload(response.get("id_token").getAsString()); + + log.debug("JWT payload id token: " + payload); + + // Collect necessary information from federated IdP for Authentication purposes + String idTokenStr = response.get("id_token").getAsString(); + String idProvider = payload.get("iss").toString().replace("https://", "") + .replace("\"", ""); + String email = payload.get("email").getAsString(); + String federatedPoolId = igv_oauth_conf.get("aws_cognito_fed_pool_id").getAsString(); + String cognitoRoleARN = igv_oauth_conf.get("aws_cognito_role_arn").getAsString(); + + HashMap logins = new HashMap<>(); + logins.put(idProvider, idTokenStr); + + // Avoid "software.amazon.awssdk.core.exception.SdkClientException: Unable to load credentials from any of the providers in the chain AwsCredentialsProviderChain(" + // The use of the AnonymousCredentialsProvider essentially bypasses the provider chain's requirement to access ~/.aws/credentials. + // https://stackoverflow.com/questions/36604024/sts-saml-and-java-sdk-unable-to-load-aws-credentials-from-any-provider-in-the-c + AnonymousCredentialsProvider anoCredProv = AnonymousCredentialsProvider.create(); + + // https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/cognitoidentity/CognitoIdentityClient.html + // Build the Cognito client + CognitoIdentityClientBuilder cognitoIdentityBuilder = CognitoIdentityClient.builder(); + + cognitoIdentityBuilder.region(getAWSREGION()).credentialsProvider(anoCredProv); + cognitoIdentityClient = cognitoIdentityBuilder.build(); + + + // https://docs.aws.amazon.com/cognito/latest/developerguide/authentication-flow.html + // Basic (Classic) Authflow + // Uses AssumeRoleWithWebIdentity and facilitates CloudTrail org.broad.igv.logging. Uses one more request but provides user traceability. + GetIdRequest.Builder idrequest = GetIdRequest.builder() + .identityPoolId(federatedPoolId) + .logins(logins); + GetIdResponse idResult = cognitoIdentityClient.getId(idrequest.build()); + + GetOpenIdTokenRequest.Builder openidrequest = GetOpenIdTokenRequest.builder().logins(logins).identityId(idResult.identityId()); + GetOpenIdTokenResponse openId = cognitoIdentityClient.getOpenIdToken(openidrequest.build()); + + + AssumeRoleWithWebIdentityRequest.Builder webidrequest = AssumeRoleWithWebIdentityRequest.builder() + .webIdentityToken(openId.token()) + .roleSessionName(email) + .roleArn(cognitoRoleARN); + + AssumeRoleWithWebIdentityResponse stsClientResponse = StsClient.builder() + .credentialsProvider(anoCredProv) + .region(getAWSREGION()) + .build() + .assumeRoleWithWebIdentity(webidrequest.build()); + + cognitoAWSCredentials = stsClientResponse.credentials(); + + updateS3Client(cognitoAWSCredentials); + } + /** * @param cognitoAWSCredentials * @return true if credentials are due to expire within next 10 seconds* @@ -238,11 +231,38 @@ private static boolean isExpired(Credentials cognitoAWSCredentials) { * * @param credentials AWS credentials */ - public static void updateS3Client(Credentials credentials) { + private static void updateS3Client(Credentials credentials) { + final Region region = getAWSREGION(); if (credentials == null) { - s3Client = S3Client.builder().region(region).build(); + // .aws/credentials, environment variable, or other AWS supported credential store + String endpointURL = null; + try { + endpointURL = getEndpointURL(); + } catch (IOException e) { + log.error("Error searching for endpoint url", e); + } + + if (endpointURL == null) { + s3Client = S3Client.builder().region(region).build(); + } else { + // Custom endpoint + try { + S3Configuration configuration = S3Configuration.builder() + .pathStyleAccessEnabled(true).build(); + + s3Client = S3Client.builder() + .endpointOverride(new URI(endpointURL)) + .serviceConfiguration(configuration) + .region(getAWSREGION()) // this is not used, but the AWS SDK requires it + .build(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + } else { + // Cognito AwsSessionCredentials creds = AwsSessionCredentials.create( credentials.accessKeyId(), credentials.secretAccessKey(), @@ -254,6 +274,19 @@ public static void updateS3Client(Credentials credentials) { } } + private static S3Client getS3Client() { + if(s3Client == null) { + if (GetCognitoConfig() != null) { + // OAuthUtils.getInstance().getAWSProvider().getAccessToken(); + updateS3Client(GetCognitoAWSCredentials()); + } else { + updateS3Client(null); + } + } + + return s3Client; + } + /** * This method returns the details of the user and bucket lists. @@ -261,22 +294,15 @@ public static void updateS3Client(Credentials credentials) { * @return bucket list */ public static List ListBucketsForUser() { + if (bucketsFinalList.isEmpty()) { - if (GetCognitoConfig() != null) { - OAuthUtils.getInstance().getAWSProvider().getAccessToken(); - updateS3Client(GetCognitoAWSCredentials()); - } else { - updateS3Client(null); - } - List bucketsList = new ArrayList<>(); + S3Client s3Client = getS3Client(); - ListBucketsRequest listBucketsRequest = ListBucketsRequest.builder().build(); - ListBucketsResponse listBucketsResponse = s3Client.listBuckets(listBucketsRequest); - listBucketsResponse.buckets().stream().forEach(x -> bucketsList.add(x.name())); + List bucketsList = s3Client.listBuckets().buckets().stream().map(b -> b.name()).collect(Collectors.toList()); // Filter out buckets that the user does not have permissions for - bucketsFinalList = getReadableBuckets(bucketsList); + bucketsFinalList = bucketsList; //getReadableBuckets(bucketsList); } return bucketsFinalList; @@ -421,13 +447,10 @@ private static List getReadableBuckets(List buckets) { */ public static ArrayList ListBucketObjects(String bucketName, String prefix) { + ArrayList objects = new ArrayList<>(); - if (GetCognitoConfig() != null) { - OAuthUtils.getInstance().getAWSProvider().getAccessToken(); - updateS3Client(GetCognitoAWSCredentials()); - } else { - updateS3Client(null); - } + + S3Client s3Client = getS3Client(); try { // https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html @@ -437,7 +460,9 @@ public static ArrayList ListBucketObjects(String bucketName, String // hierarchy using key name prefixes and delimiters as the Amazon S3 console does. The Amazon // S3 console supports a concept of folders. // """ - ListObjectsV2Request listReq = ListObjectsV2Request.builder().bucket(bucketName) + + ListObjectsV2Request listReq = ListObjectsV2Request.builder() + .bucket(bucketName) .prefix(prefix) .delimiter("/") .build(); @@ -484,41 +509,158 @@ public static String getKeyFromS3URL(String s3URL) { return s3URI.getKey(); } - // Amazon S3 Presign URLs - // Also keeps an internal mapping between ResourceLocator and active/valid signed URLs. + /** + * Create a presigned URL for the s3:// + * + * @param s3Path + * @return + * @throws IOException + */ + private static String createPresignedURL(String s3Path) throws IOException { - // TODO: Ideally the presigned URL should be generated without any of the Cognito being involved first? - // Make sure access token are valid (refreshes token internally) - S3Presigner s3Presigner; + s3Presigner = getPresigner(); - if (GetCognitoConfig() != null) { - OAuthProvider provider = OAuthUtils.getInstance().getAWSProvider(); - provider.getAccessToken(); + AmazonS3URI s3URI = new AmazonS3URI(s3Path); + String bucket = s3URI.getBucket(); + String key = s3URI.getKey(); - Credentials credentials = GetCognitoAWSCredentials(); - AwsSessionCredentials creds = AwsSessionCredentials.create(credentials.accessKeyId(), - credentials.secretAccessKey(), - credentials.sessionToken()); - StaticCredentialsProvider awsCredsProvider = StaticCredentialsProvider.create(creds); + // URI presigned = s3Presigner.presignS3DownloadLink(bucket, key); + GetObjectRequest s3GetRequest = GetObjectRequest.builder().bucket(bucket).key(key).build(); + GetObjectPresignRequest getObjectPresignRequest = + GetObjectPresignRequest.builder() + .signatureDuration(Duration.ofMinutes(60)) + .getObjectRequest(s3GetRequest) + .build(); - s3Presigner = S3Presigner.builder() - .expiration(provider.getExpirationTime()) // Duration.ofSeconds(30) // <= for testing - .awsCredentials(awsCredsProvider) - .region(getAWSREGION()) - .build(); - } else { - s3Presigner = S3Presigner.builder().build(); - } + PresignedGetObjectRequest presignedGetObjectRequest = + s3Presigner.presignGetObject(getObjectPresignRequest); - String bucket = getBucketFromS3URL(s3Path); - String key = getKeyFromS3URL(s3Path); + URL presigned = presignedGetObjectRequest.url(); - URI presigned = s3Presigner.presignS3DownloadLink(bucket, key); log.debug("AWS presigned URL from translateAmazonCloudURL is: " + presigned); return presigned.toString(); } + private static S3Presigner getPresigner() throws IOException { + + if (s3Presigner == null) { + + if (GetCognitoConfig() != null) { + OAuthProvider provider = OAuthUtils.getInstance().getAWSProvider(); + provider.getAccessToken(); + + Credentials credentials = GetCognitoAWSCredentials(); + AwsSessionCredentials creds = AwsSessionCredentials.create(credentials.accessKeyId(), + credentials.secretAccessKey(), + credentials.sessionToken()); + StaticCredentialsProvider awsCredsProvider = StaticCredentialsProvider.create(creds); + + s3Presigner = S3Presigner.builder() + .credentialsProvider(awsCredsProvider) + .region(getAWSREGION()) + .build(); + } else { + final String endpointURL = getEndpointURL(); + + if (endpointURL == null) { + s3Presigner = S3Presigner.builder().build(); + } else { + // Override presigned url style -- some (all?) 3rd party S3 providers do not support virtual host style + S3Configuration configuration = S3Configuration.builder() + .pathStyleAccessEnabled(true).build(); + try { + s3Presigner = S3Presigner.builder() + .serviceConfiguration(configuration) + .endpointOverride(new URI(endpointURL)) + .region(getAWSREGION()) + .build(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + } + } + return s3Presigner; + } + + /** + * Return a custom endpoint URL, if defined. The endpointURL is searched for once per session and not updated. + * Search in the follow locations. + *

+ * (1) oauth config file + * (2) igv preference + * (3) environment variable AWS_ENDPOINT_URL + * (4) aws credentials file + * (5) aws config file + * + * @return Custom AWS endpoint url or null + * @throws IOException + */ + private static String getEndpointURL() throws IOException { + + if ("UNKNOWN".equals(endpointURL)) { + + // IGV preference + endpointURL = PreferencesManager.getPreferences().get(Constants.AWS_ENDPOINT_URL); + if (endpointURL != null) { + return endpointURL; + } + + // environment variable + endpointURL = System.getenv("AWS_ENDPOINT_URL"); + if (endpointURL != null) { + return endpointURL; + } + + // oauth config + if (GetCognitoConfig() != null && GetCognitoConfig().has("endpoint_url")) { + endpointURL = GetCognitoConfig().get("endpoint_url").getAsString(); + if (endpointURL != null) { + return endpointURL; + } + } + + // Search aws directory + File awsDirectory = new File(DirectoryManager.getUserHome(), ".aws"); + if (awsDirectory.exists()) { + File credfile = new File(awsDirectory, "credentials"); + if (credfile.exists()) { + BufferedReader br = new BufferedReader(new FileReader(credfile)); + String nextLine; + while ((nextLine = br.readLine()) != null) { + String[] tokens = nextLine.split("="); + if (tokens.length == 2) { + String key = tokens[0].trim(); + if (key.equals("endpoint_url")) { + endpointURL = tokens[1].trim(); + return endpointURL; + } + } + } + } + credfile = new File(awsDirectory, "config"); + if (credfile.exists()) { + BufferedReader br = new BufferedReader(new FileReader(credfile)); + String nextLine; + while ((nextLine = br.readLine()) != null) { + String[] tokens = nextLine.split("="); + if (tokens.length == 2) { + String key = tokens[0].trim(); + if (key.equals("endpoint_url")) { + endpointURL = tokens[1].trim(); + return endpointURL; + } + } + } + } + } + } + + + return endpointURL; + } + /** * @param s3UrlString * @return @@ -540,29 +682,21 @@ public static Boolean isAwsS3Path(String path) { return (path.startsWith("s3://")); } - public static boolean isPresignedURL(String urlString) { + public static boolean isKnownPresignedURL(String urlString) { return presignedToS3Map.containsKey(urlString); } public static String updatePresignedURL(String urlString) throws IOException { String s3UrlString = presignedToS3Map.get(urlString); if (s3UrlString == null) { - throw new RuntimeException("Unrecognized presigned url: " + urlString); + // We haven't seen this url before. This shouldn't happen, but if it does we have to assume its valid + log.info("Unrecognized presigned url: " + urlString); + return urlString; } else { return translateAmazonCloudURL(s3UrlString); } } - /** - * If using Cognito, check that the use is logged in, and prompt for login if not. - */ - public static void checkLogin() { - if (GetCognitoConfig() != null && - !OAuthUtils.getInstance().getAWSProvider().isLoggedIn()) { - OAuthUtils.getInstance().getAWSProvider().checkLogin(); - } - } - /** * Checks whether a (pre)signed url is still accessible or it has expired, offline. * No extra request/head is required to the presigned object since we have all information @@ -575,34 +709,30 @@ public static void checkLogin() { **/ private static boolean isPresignedURLValid(URL url) { - boolean isValidSignedUrl; + boolean isValidSignedUrl; try { - long presignedTime = signedURLValidity(url); + Map params = StringUtils.splitQuery(url); + String amzDateStr = params.get("X-Amz-Date"); + long amzExpires = Long.parseLong(params.get("X-Amz-Expires")); + + SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); + formatter.setTimeZone(TimeZone.getTimeZone("UTC")); // Z(ulu) -> UTC + Date amzDate = formatter.parse(amzDateStr); + + long presignedTime = amzDate.getTime() + amzExpires * 1000; + log.debug("The date of expiration is " + amzDate + ", expires after " + amzExpires + " seconds for url: " + url); + isValidSignedUrl = presignedTime - System.currentTimeMillis() - TOKEN_EXPIRE_GRACE_TIME > 0; // Duration in milliseconds } catch (ParseException e) { log.error("The AWS signed URL date parameter X-Amz-Date has incorrect formatting"); isValidSignedUrl = false; } catch (UnsupportedEncodingException e) { - e.printStackTrace(); + log.error("Error decoding signed url", e); isValidSignedUrl = false; } return isValidSignedUrl; } - private static long signedURLValidity(URL url) throws ParseException, UnsupportedEncodingException { - Map params = StringUtils.splitQuery(url); - String amzDateStr = params.get("X-Amz-Date"); - long amzExpires = Long.parseLong(params.get("X-Amz-Expires")); - - SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); - formatter.setTimeZone(TimeZone.getTimeZone("UTC")); // Z(ulu) -> UTC - Date amzDate = formatter.parse(amzDateStr); - - long timeOfExpirationMillis = amzDate.getTime() + amzExpires * 1000; - - log.debug("The date of expiration is " + amzDate + ", expires after " + amzExpires + " seconds for url: " + url); - return timeOfExpirationMillis; - } } diff --git a/src/main/java/org/broad/igv/util/HttpUtils.java b/src/main/java/org/broad/igv/util/HttpUtils.java index 086d035ce0..38734e461f 100644 --- a/src/main/java/org/broad/igv/util/HttpUtils.java +++ b/src/main/java/org/broad/igv/util/HttpUtils.java @@ -715,11 +715,10 @@ private HttpURLConnection openConnection( } // If a presigned URL, check its validity and update if needed - if (AmazonUtils.isPresignedURL(url.toExternalForm())) { + if (AmazonUtils.isKnownPresignedURL(url.toExternalForm())) { url = new URL(AmazonUtils.updatePresignedURL(url.toExternalForm())); } - // If an S3 url, obtain a signed https url if (AmazonUtils.isAwsS3Path(url.toExternalForm())) { url = new URL(AmazonUtils.translateAmazonCloudURL(url.toExternalForm())); diff --git a/src/main/java/org/broad/igv/util/S3Presigner.java b/src/main/java/org/broad/igv/util/S3Presigner.java deleted file mode 100644 index cd50fe489d..0000000000 --- a/src/main/java/org/broad/igv/util/S3Presigner.java +++ /dev/null @@ -1,195 +0,0 @@ -package org.broad.igv.util; - -/* -* -* This is a transitional class until the official java-aws-sdk-v2 includes a S3 URL presigners class, see: -* -* https://github.com/aws/aws-sdk-java-v2/issues/849#issuecomment-468892839 -* https://github.com/aws/aws-sdk-java-v2/issues/203 -* -*/ - -import java.io.ByteArrayInputStream; -import java.net.URI; -import java.time.Duration; -import java.time.Instant; - -import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; -import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; -import software.amazon.awssdk.auth.signer.AwsS3V4Signer; -import software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute; -import software.amazon.awssdk.core.ResponseInputStream; -import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; -import software.amazon.awssdk.core.exception.SdkClientException; -import software.amazon.awssdk.core.interceptor.Context.BeforeTransmission; -import software.amazon.awssdk.core.interceptor.ExecutionAttributes; -import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; -import software.amazon.awssdk.core.sync.RequestBody; -import software.amazon.awssdk.http.AbortableInputStream; -import software.amazon.awssdk.http.ExecutableHttpRequest; -import software.amazon.awssdk.http.HttpExecuteRequest; -import software.amazon.awssdk.http.HttpExecuteResponse; -import software.amazon.awssdk.http.SdkHttpClient; -import software.amazon.awssdk.http.SdkHttpFullRequest; -import software.amazon.awssdk.http.SdkHttpResponse; -import software.amazon.awssdk.regions.Region; -import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain; -import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.S3ClientBuilder; -import software.amazon.awssdk.services.s3.model.GetObjectRequest; -import software.amazon.awssdk.services.s3.model.GetObjectResponse; -import software.amazon.awssdk.services.s3.model.PutObjectRequest; -import software.amazon.awssdk.services.s3.model.PutObjectResponse; - -public class S3Presigner { - - private Region region; - private AwsCredentialsProvider awsCredentialsProvider; - private Duration expirationTime; - private Integer timeOffset; - private S3PresignExecutionInterceptor presignInterceptor; - private S3Client s3Client; - - private S3Presigner() { - presignInterceptor = new S3PresignExecutionInterceptor(); - } - - public static Builder builder() { - return new Builder(); - } - - public URI presignS3DownloadLink(String bucketName, String fileName) throws SdkClientException { - try { - - GetObjectRequest s3GetRequest = GetObjectRequest.builder().bucket(bucketName).key(fileName).build(); - ResponseInputStream response = s3Client.getObject(s3GetRequest); - response.close(); - - return presignInterceptor.getSignedURI(); - } catch (Throwable t) { - if (t instanceof SdkClientException) { - throw (SdkClientException) t; - } - throw SdkClientException.builder().cause(t).build(); - } - } - - public URI presignS3UploadLink(String bucketName, String fileName) throws SdkClientException { - try { - - PutObjectRequest s3PutRequest = PutObjectRequest.builder().bucket(bucketName).key(fileName).build(); - PutObjectResponse response = s3Client.putObject(s3PutRequest, RequestBody.empty()); - - return presignInterceptor.getSignedURI(); - } catch (Throwable t) { - if (t instanceof SdkClientException) { - throw (SdkClientException) t; - } - throw SdkClientException.builder().cause(t).build(); - } - } - - public static class Builder { - S3Presigner presigner = new S3Presigner(); - - public S3Presigner build() { - if (presigner.awsCredentialsProvider == null) { - DefaultCredentialsProvider provider = DefaultCredentialsProvider.create(); - presigner.awsCredentialsProvider = provider; - } - - if (presigner.region == null) { - presigner.region = new DefaultAwsRegionProviderChain().getRegion(); - } - - if (presigner.expirationTime == null) { - presigner.expirationTime = Duration.ofDays(4); - } - - if (presigner.timeOffset == null) { - presigner.timeOffset = 2; - } - - S3ClientBuilder s3Builder = S3Client.builder().region(presigner.region).credentialsProvider(presigner.awsCredentialsProvider); - s3Builder.overrideConfiguration(ClientOverrideConfiguration.builder().addExecutionInterceptor(presigner.presignInterceptor).build()); - s3Builder.httpClient(new NullSdkHttpClient()); - presigner.s3Client = s3Builder.build(); - - return presigner; - } - - public Builder awsCredentials(AwsCredentialsProvider awsCredentialsProvider) { - presigner.awsCredentialsProvider = awsCredentialsProvider; - return this; - } - - public Builder region(Region region) { - presigner.region = region; - return this; - } - - public Builder expiration(Duration expirationTime) { - presigner.expirationTime = expirationTime; - return this; - } - - public Builder timeOffset(Integer timeOffset) { - presigner.timeOffset = timeOffset; - return this; - } - - } - - public static class NullSdkHttpClient implements SdkHttpClient { - - @Override - public void close() { - - } - - @Override - public ExecutableHttpRequest prepareRequest(HttpExecuteRequest request) { - return new ExecutableHttpRequest() { - @Override - public HttpExecuteResponse call() { - return HttpExecuteResponse.builder().response(SdkHttpResponse.builder().statusCode(200).build()).responseBody(AbortableInputStream.create(new ByteArrayInputStream(new byte[0]))).build(); - } - - @Override - public void abort() { - } - }; - } - } - - public class S3PresignExecutionInterceptor implements ExecutionInterceptor { - - final private AwsS3V4Signer signer; - private URI signedURI; - - public S3PresignExecutionInterceptor() { - this.signer = AwsS3V4Signer.create(); - } - - @Override - public void beforeTransmission(BeforeTransmission context, ExecutionAttributes executionAttributes) { - // remove all headers because a Browser that downloads the shared URL will not send the exact values. X-Amz-SignedHeaders should only contain the host header. - SdkHttpFullRequest modifiedSdkRequest = (SdkHttpFullRequest) context.httpRequest().toBuilder().clearHeaders().build(); - - - executionAttributes.putAttribute(AwsSignerExecutionAttribute.AWS_CREDENTIALS, awsCredentialsProvider.resolveCredentials()); - executionAttributes.putAttribute(AwsSignerExecutionAttribute.PRESIGNER_EXPIRATION, Instant.ofEpochSecond(System.currentTimeMillis()/1000).plus(expirationTime)); - executionAttributes.putAttribute(AwsSignerExecutionAttribute.SERVICE_SIGNING_NAME, "s3"); - executionAttributes.putAttribute(AwsSignerExecutionAttribute.SIGNING_REGION, region); - executionAttributes.putAttribute(AwsSignerExecutionAttribute.TIME_OFFSET, timeOffset); - SdkHttpFullRequest signedRequest = signer.presign(modifiedSdkRequest, executionAttributes);// sign(getRequest, new ExecutionAttributes()); - signedURI = signedRequest.getUri(); - } - - public URI getSignedURI() { - return signedURI; - } - - } - -} diff --git a/src/main/java/org/broad/igv/util/blat/BlatQueryWindow.java b/src/main/java/org/broad/igv/util/blat/BlatQueryWindow.java index b157a3c9f5..7c21315cf3 100644 --- a/src/main/java/org/broad/igv/util/blat/BlatQueryWindow.java +++ b/src/main/java/org/broad/igv/util/blat/BlatQueryWindow.java @@ -108,7 +108,7 @@ private void closeItemActionPerformed(ActionEvent e) { private void saveItemActionPerformed(ActionEvent e) { - File f = FileDialogUtils.chooseFile("Save BLAT results", DirectoryManager.getUserDirectory(), FileDialogUtils.SAVE); + File f = FileDialogUtils.chooseFile("Save BLAT results", DirectoryManager.getUserDefaultDirectory(), FileDialogUtils.SAVE); if (f != null) { try { model.save(f); diff --git a/src/main/resources/org/broad/igv/prefs/preferences.tab b/src/main/resources/org/broad/igv/prefs/preferences.tab index 47ed001796..470ff9b7ec 100644 --- a/src/main/resources/org/broad/igv/prefs/preferences.tab +++ b/src/main/resources/org/broad/igv/prefs/preferences.tab @@ -273,9 +273,9 @@ MASTER_RESOURCE_FILE_KEY Data registry URL string https://igv.org/genomes/regist --- PROVISIONING.URL OAuth provisioning URL string null --- - -BLAT_URL BLAT URL String https://genome.ucsc.edu/cgi-bin/hgBlat +BLAT_URL BLAT URL string https://genome.ucsc.edu/cgi-bin/hgBlat --- +AWS_ENDPOINT_URL AWS endpoint URL string null ## Tooltip TOOLTIP.INITIAL_DELAY Tooltip inital delay (ms) integer 50 From a37cff69fa01400cb7c9d227eb7740c1a6c6c330 Mon Sep 17 00:00:00 2001 From: Jim Robinson <933148+jrobinso@users.noreply.github.com> Date: Fri, 24 Jan 2025 19:59:00 -0800 Subject: [PATCH 100/130] Enable linked read preferences, previously these were no-ops (#1645) --- src/main/java/org/broad/igv/prefs/Constants.java | 2 ++ src/main/java/org/broad/igv/sam/AlignmentTrack.java | 6 +++--- .../java/org/broad/igv/sam/AlignmentTrackMenu.java | 10 ---------- src/main/java/org/broad/igv/sam/LinkedAlignment.java | 4 +++- src/main/java/org/broad/igv/sam/SAMAlignment.java | 8 ++++++-- src/main/resources/org/broad/igv/prefs/preferences.tab | 6 +++++- 6 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/broad/igv/prefs/Constants.java b/src/main/java/org/broad/igv/prefs/Constants.java index 22463a29f0..2727027748 100644 --- a/src/main/java/org/broad/igv/prefs/Constants.java +++ b/src/main/java/org/broad/igv/prefs/Constants.java @@ -154,6 +154,8 @@ private Constants() { public static final String SAM_COLOR_BY_TAG = "SAM.COLOR_BY_TAG"; public static final String SAM_SORT_BY_TAG = "SAM.SORT_BY_TAG"; public static final String SAM_GROUP_BY_TAG = "SAM.GROUP_BY_TAG"; + public static final String SAM_LINKED_READS = "SAM.LINK_READS"; + public static final String SAM_LINK_TAG = "SAM.LINK_TAG"; public static final String SAM_LINK_BY_TAGS = "SAM.LINK_BY_TAGS"; public static final String SAM_GROUP_BY_POS = "SAM.GROUP_BY_POS"; public static final String SAM_BISULFITE_CONTEXT = "SAM.BISULFITE_CONTEXT"; diff --git a/src/main/java/org/broad/igv/sam/AlignmentTrack.java b/src/main/java/org/broad/igv/sam/AlignmentTrack.java index 2443e2f4dd..e150176efe 100644 --- a/src/main/java/org/broad/igv/sam/AlignmentTrack.java +++ b/src/main/java/org/broad/igv/sam/AlignmentTrack.java @@ -1061,7 +1061,7 @@ void setLinkedReadView(boolean linkedReads, String tag) { } /** - * Detect if we are in linked-read view + * Detect if we are in linked-read view (10X Loupe style view) */ boolean isLinkedReadView() { return renderOptions != null && @@ -1548,7 +1548,7 @@ public boolean isInvertGroupSorting() { } public String getLinkByTag() { - return linkByTag; + return linkByTag == null ? getPreferences().get(SAM_LINK_TAG) : linkByTag; } public GroupOption getGroupByOption() { @@ -1564,7 +1564,7 @@ public GroupOption getGroupByOption() { } public boolean isLinkedReads() { - return linkedReads != null && linkedReads; + return linkedReads == null ? getPreferences().getAsBoolean(SAM_LINKED_READS) : linkedReads; } public boolean isQuickConsensusMode() { diff --git a/src/main/java/org/broad/igv/sam/AlignmentTrackMenu.java b/src/main/java/org/broad/igv/sam/AlignmentTrackMenu.java index 830105d5f9..8ae8db08b6 100644 --- a/src/main/java/org/broad/igv/sam/AlignmentTrackMenu.java +++ b/src/main/java/org/broad/igv/sam/AlignmentTrackMenu.java @@ -1162,16 +1162,6 @@ void addLinkedReadItems() { add(linkByTagItem); } - private JCheckBoxMenuItem linkedReadViewItem(String tag) { - final JCheckBoxMenuItem item = new JCheckBoxMenuItem("Linked read view (" + tag + ")"); - item.setSelected(alignmentTrack.isLinkedReadView() && tag != null && tag.equals(renderOptions.getLinkByTag())); - item.addActionListener(aEvt -> { - boolean linkedReads = item.isSelected(); - alignmentTrack.setLinkedReadView(linkedReads, tag); - }); - return item; - } - private JCheckBoxMenuItem linkedReadItem(String tag) { final JCheckBoxMenuItem item = new JCheckBoxMenuItem("Link by " + tag); item.setSelected(!alignmentTrack.isLinkedReadView() && alignmentTrack.isLinkedReads() && tag.equals(renderOptions.getLinkByTag())); diff --git a/src/main/java/org/broad/igv/sam/LinkedAlignment.java b/src/main/java/org/broad/igv/sam/LinkedAlignment.java index 0f86b07a14..8cd05e95b5 100644 --- a/src/main/java/org/broad/igv/sam/LinkedAlignment.java +++ b/src/main/java/org/broad/igv/sam/LinkedAlignment.java @@ -1,5 +1,7 @@ package org.broad.igv.sam; +import htsjdk.samtools.SAMRecord; +import htsjdk.samtools.SAMTag; import org.broad.igv.Globals; import org.broad.igv.feature.Strand; import org.broad.igv.track.WindowFunction; @@ -299,7 +301,7 @@ public boolean isPrimary() { @Override public boolean isSupplementary() { - return false; + return true; } @Override diff --git a/src/main/java/org/broad/igv/sam/SAMAlignment.java b/src/main/java/org/broad/igv/sam/SAMAlignment.java index 81b5b8a9f0..8e9121a484 100644 --- a/src/main/java/org/broad/igv/sam/SAMAlignment.java +++ b/src/main/java/org/broad/igv/sam/SAMAlignment.java @@ -56,8 +56,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import static org.broad.igv.prefs.Constants.SAM_HIDE_SMALL_INDEL; - /** * @author jrobinso */ @@ -225,6 +223,12 @@ public Object getAttribute(String key) { } } + public List getAttributes() { + return record.getAttributes(); + } + + + private Object getAttribute(SAMTag key) { return key == null ? null : record.getAttribute(key); } diff --git a/src/main/resources/org/broad/igv/prefs/preferences.tab b/src/main/resources/org/broad/igv/prefs/preferences.tab index 470ff9b7ec..b4822d5f7a 100644 --- a/src/main/resources/org/broad/igv/prefs/preferences.tab +++ b/src/main/resources/org/broad/igv/prefs/preferences.tab @@ -107,9 +107,13 @@ SAM.ALLELE_USE_QUALITY Quality weight allele fraction boolean TRUE SAM.COLOR_BY Color alignments by select NONE|READ_STRAND|FIRST_OF_PAIR_STRAND|PAIR_ORIENTATION|UNEXPECTED_PAIR|INSERT_SIZE|BASE_MODIFICATION|BASE_MODIFICATION_2COLOR|SAMPLE|READ_GROUP|LIBRARY|MOVIE|ZMW|BISULFITE|NOMESEQ|TAG|MAPPED_SIZE|LINK_STRAND|YC_TAG|READ_ORDER UNEXPECTED_PAIR SAM.COLOR_BY_TAG Color by TAG string +--- SAM.GROUP_OPTION Group alignments by select NONE|STRAND|SAMPLE|READ_GROUP|LIBRARY|FIRST_OF_PAIR_STRAND|TAG|PAIR_ORIENTATION|MATE_CHROMOSOME|SV_ALIGNMENT|SUPPLEMENTARY|BASE_AT_POS|MOVIE|ZMW|HAPLOTYPE|READ_ORDER|LINKED|PHASE|MAPPING_QUALITY|DUPLICATE SAM.GROUP_BY_TAG Group by TAG string -SAM.GROUP_ALL Syncrohize alignment track grouping boolean FALSE +SAM.GROUP_ALL Syncronize grouping across all alignment tracks boolean FALSE +--- +SAM.LINK_READS Link alignments by tag boolean FALSE +SAM.LINK_TAG Linking tag string READNAME SAM.QUALITY_THRESHOLD Mapping quality threshold int 0 Hide alignments with MQ lower than this value SAM.ALIGNMENT_SCORE_THRESHOLD Alignment score threshold (AS tag) int 0 From d4e2f6f2718a18967ba7b8fedfff19962f156f7a Mon Sep 17 00:00:00 2001 From: Jim Robinson <933148+jrobinso@users.noreply.github.com> Date: Sat, 25 Jan 2025 19:22:39 -0800 Subject: [PATCH 101/130] Track hubs (#1646) * Bug - fix parsing of non integer priorities * Bug fix - handle absolute data URLs in track hub file (see issued #1643) * Track hubs: restrict initial track selection list to Gene group if total track count is > 20 --- .../feature/genome/load/HubGenomeLoader.java | 32 ++-- src/main/java/org/broad/igv/ucsc/Hub.java | 139 ++++++++++-------- 2 files changed, 98 insertions(+), 73 deletions(-) diff --git a/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java b/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java index 467d63af30..3a4fd54844 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java +++ b/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java @@ -10,6 +10,7 @@ import java.io.IOException; import java.util.*; +import java.util.stream.Collectors; /** * Loads a "genome" from a UCSC track hub @@ -52,24 +53,33 @@ public Genome loadGenome() throws IOException { // Check previous selections for this hub first // TODO -- Maintain track order? String key = "hub:" + this.hubURL; + final List groupedTrackConfigurations = hub.getGroupedTrackConfigurations(); + if (PreferencesManager.getPreferences().hasExplicitValue(key)) { - List selectedTracks = new ArrayList<>(); Set selectedTrackNames = new HashSet<>(Arrays.asList(PreferencesManager.getPreferences().get(key).split(","))); - List trackConfigGroups = hub.getGroupedTrackConfigurations(); - for (TrackConfigGroup group : trackConfigGroups) { - for (TrackConfig trackConfig : group.tracks) { - if (selectedTrackNames.contains(trackConfig.getName())) { - selectedTracks.add(trackConfig); - } - } - } - selectedTracks.sort((o1, o2) -> o1.getOrder() - o2.getOrder()); + List selectedTracks = groupedTrackConfigurations.stream() + .flatMap(group -> group.tracks.stream()) + .filter(trackConfig -> selectedTrackNames.contains(trackConfig.getName())) + .sorted(Comparator.comparingInt(TrackConfig::getOrder)) + .collect(Collectors.toList()); config.setTracks(selectedTracks); } // If running in interactive mode opend dialog to set tracks. else if (IGV.hasInstance() && !Globals.isBatch() && !Globals.isHeadless() && !Globals.isTesting()) { - HubTrackSelectionDialog dlg = new HubTrackSelectionDialog(hub.getGroupedTrackConfigurations(), IGV.getInstance().getMainFrame()); + + int count = 0; + for(TrackConfigGroup g : groupedTrackConfigurations) { + count += g.tracks.size(); + } + + // If the total # of tracks is >= 20 filter to "Gene" groups, usually a single group + List filteredGroups = count < 20 ? + groupedTrackConfigurations : + groupedTrackConfigurations.stream().filter(g -> g.label.startsWith("Gene")).collect(Collectors.toList()); + + + HubTrackSelectionDialog dlg = new HubTrackSelectionDialog(filteredGroups, IGV.getInstance().getMainFrame()); dlg.setVisible(true); List selectedTracks = dlg.getSelectedConfigs(); diff --git a/src/main/java/org/broad/igv/ucsc/Hub.java b/src/main/java/org/broad/igv/ucsc/Hub.java index 79cddb09ad..86c6db14b6 100644 --- a/src/main/java/org/broad/igv/ucsc/Hub.java +++ b/src/main/java/org/broad/igv/ucsc/Hub.java @@ -3,17 +3,23 @@ import org.broad.igv.Globals; import org.broad.igv.feature.genome.load.GenomeConfig; import org.broad.igv.feature.genome.load.TrackConfig; +import org.broad.igv.logging.LogManager; +import org.broad.igv.logging.Logger; import org.broad.igv.ui.IGV; import org.broad.igv.util.ParsingUtils; import java.io.BufferedReader; import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; import java.util.*; import java.util.function.Function; public class Hub { + private static Logger log = LogManager.getLogger(Hub.class); private final String url; + private final String host; String baseURL; Stanza hub; Stanza genomeStanza; @@ -27,10 +33,32 @@ public class Hub { public static Hub loadHub(String url) throws IOException { + return new Hub(url); + } + + private Hub(String url) throws IOException { + + this.url = url; + int idx = url.lastIndexOf("/"); String baseURL = url.substring(0, idx + 1); + this.baseURL = baseURL; + + if(url.startsWith("https://") || url.startsWith("http://")) { + try { + URL tmp = new URL(url); + this.host = tmp.getProtocol() + "://" + tmp.getHost(); + } catch (MalformedURLException e) { + // This should never happen + log.error("Error parsing base URL host", e); + throw new RuntimeException(e); + } + } + else { + // Local file, no host + this.host = ""; + } - // Load stanzas List stanzas = loadStanzas(url); // Validation checks @@ -47,56 +75,28 @@ public static Hub loadHub(String url) throws IOException { throw new RuntimeException("Unexpected hub file -- expected 'genome' stanza but found " + stanzas.get(1).type); } + this.hub = stanzas.get(0); + // Load groups - List groups = null; - Stanza genomeStanza = stanzas.get(1); + this. genomeStanza = stanzas.get(1); if (genomeStanza.hasProperty("groups")) { - String groupsTxtURL = baseURL + genomeStanza.getProperty("groups"); - groups = loadStanzas(groupsTxtURL); + String groupsTxtURL = getDataURL(genomeStanza.getProperty("groups")); + this.groupStanzas = loadStanzas(groupsTxtURL); } - // load includes. Nested includes are not supported + // load includes. Nested includes (includes within includes) are not supported List includes = stanzas.stream().filter(s -> "include".equals(s.type)).toList(); for (Stanza s : includes) { - List includeStanzas = loadStanzas(baseURL + s.getProperty("include")); + List includeStanzas = loadStanzas(getDataURL(s.getProperty("include"))); for (Stanza inc : includeStanzas) { - s.setProperty("visibility", "hide"); - stanzas.add(s); + inc.setProperty("visibility", "hide"); + stanzas.add(inc); } } - return new Hub(url, stanzas, groups); - } - - private Hub(String url, List stanzas, List groupStanzas) { - - this.url = url; - - int idx = url.lastIndexOf("/"); - String baseURL = url.substring(0, idx + 1); - this.baseURL = baseURL; - if (stanzas.size() < 2) { - throw new RuntimeException("Expected at least 2 stanzas, hub and genome"); - } // The first stanza must be type = hub - if ("hub".equals(stanzas.get(0).type)) { - this.hub = stanzas.get(0); - } else { - throw new RuntimeException("Unexpected hub.txt file -- does the first line start with 'hub'?"); - } - if (!"on".equals(this.hub.getProperty("useOneFile"))) { - throw new RuntimeException("Only 'useOneFile' hubs are currently supported"); - } - - // The second stanza should be a genome - if ("genome".equals(stanzas.get(1).type)) { - this.genomeStanza = stanzas.get(1); - } else { - throw new RuntimeException("Unexpected hub file -- expected 'genome' stanza but found " + stanzas.get(1).type); - } - // Remaining stanzas should be tracks this.trackStanzas = new ArrayList<>(); for (int i = 2; i < stanzas.size(); i++) { @@ -106,16 +106,32 @@ private Hub(String url, List stanzas, List groupStanzas) { } if (groupStanzas != null) { - this.groupStanzas = groupStanzas; this.groupPriorityMap = new HashMap<>(); for (Stanza g : groupStanzas) { if (g.hasProperty("priority")) { - this.groupPriorityMap.put(g.getProperty("name"), Integer.parseInt(g.getProperty("priority")) * 10); + this.groupPriorityMap.put(g.getProperty("name"), getPriority(g)); } } } } + /** + * Return the priority for the group. The priority format is uncertain, but extends to at least 2 levels (e.g. 3.4). + * Ignore levels > 2 + * + * @param g the group stanza + * @return A priority as an integer + */ + private static int getPriority(Stanza g) { + String priorityString = g.getProperty("priority"); + String[] tokens = priorityString.split("\\."); + int p = Integer.parseInt(tokens[0]) * 10; + if (tokens.length > 1) { + p += Integer.parseInt(tokens[1]); + } + return p; + } + private static List loadStanzas(String url) throws IOException { List nodes = new ArrayList<>(); Stanza currentNode = null; @@ -185,13 +201,13 @@ public GenomeConfig getGenomeConfig(boolean includeTracks) { } else if (this.genomeStanza.hasProperty("description")) { config.setName(this.genomeStanza.getProperty("description")); } - if(config.getName() == null) { + if (config.getName() == null) { config.setName(config.getId()); } else { config.setName(config.getName() + " (" + config.getId() + ")"); } - config.setTwoBitURL(this.baseURL + this.genomeStanza.getProperty("twoBitPath")); + config.setTwoBitURL(getDataURL(this.genomeStanza.getProperty("twoBitPath"))); config.setNameSet("ucsc"); config.setWholeGenomeView(false); @@ -202,17 +218,17 @@ public GenomeConfig getGenomeConfig(boolean includeTracks) { config.setDescription(config.getId()); if (this.genomeStanza.hasProperty("blat")) { - config.setBlat(this.baseURL + this.genomeStanza.getProperty("blat")); + config.setBlat(getDataURL(this.genomeStanza.getProperty("blat"))); } if (this.genomeStanza.hasProperty("chromAliasBb")) { - config.setChromAliasBbURL(this.baseURL + this.genomeStanza.getProperty("chromAliasBb")); + config.setChromAliasBbURL(getDataURL(this.genomeStanza.getProperty("chromAliasBb"))); } if (this.genomeStanza.hasProperty("twoBitBptURL")) { - config.setTwoBitBptURL(this.baseURL + this.genomeStanza.getProperty("twoBitBptURL")); + config.setTwoBitBptURL(getDataURL(this.genomeStanza.getProperty("twoBitBptURL"))); } if (this.genomeStanza.hasProperty("twoBitBptUrl")) { - config.setTwoBitBptURL(this.baseURL + this.genomeStanza.getProperty("twoBitBptUrl")); + config.setTwoBitBptURL(getDataURL(this.genomeStanza.getProperty("twoBitBptUrl"))); } // chromSizes can take a very long time to load, and is not useful with the default WGV = off @@ -231,7 +247,7 @@ public GenomeConfig getGenomeConfig(boolean includeTracks) { } if (this.genomeStanza.hasProperty("htmlPath")) { - config.setInfoURL(this.baseURL + this.genomeStanza.getProperty("htmlPath")); + config.setInfoURL(getDataURL(this.genomeStanza.getProperty("htmlPath"))); } // Search for cytoband @@ -246,17 +262,15 @@ shortLabel Chromosome Band (Ideogram) */ for (Stanza t : this.trackStanzas) { if ("cytoBandIdeo".equals(t.name) && t.hasProperty("bigDataUrl")) { - config.setCytobandBbURL(this.baseURL + t.getProperty("bigDataUrl")); + config.setCytobandBbURL(getDataURL(t.getProperty("bigDataUrl"))); break; } } // Tracks. To prevent loading tracks set `includeTrackGroups`to false or "none" if (includeTracks) { - Function filter = (t) -> { - return !Hub.filterTracks.contains(t.name) && - (!"hide".equals(t.getProperty("visibility"))); - }; + Function filter = (t) -> !Hub.filterTracks.contains(t.name) && + (!"hide".equals(t.getProperty("visibility"))); config.setTracks(this.getTracksConfigs(filter)); } @@ -265,6 +279,10 @@ shortLabel Chromosome Band (Ideogram) return config; } + private String getDataURL(String relativeURL) { + return relativeURL.startsWith("/") ? this.host + relativeURL : this.baseURL + relativeURL; + } + public List getGroupedTrackConfigurations() { // Organize track configs by group @@ -272,10 +290,7 @@ public List getGroupedTrackConfigurations() { java.util.function.Function filter = (stanza -> !stanza.name.equals("cytoBandIdeo")); for (TrackConfig c : this.getTracksConfigs(filter)) { String groupName = c.getGroup() != null ? c.getGroup() : "other"; - if (!trackConfigMap.containsKey(groupName)) { - trackConfigMap.put(groupName, new ArrayList<>()); - } - trackConfigMap.get(groupName).add(c); + trackConfigMap.computeIfAbsent(groupName, k -> new ArrayList<>()).add(c); } // Extract map of group names @@ -311,7 +326,7 @@ List getTracksConfigs(java.util.function.Function TrackConfig getTrackConfig(Stanza t) { String format = t.format(); - String url = this.baseURL + t.getProperty("bigDataUrl"); + String url = getDataURL(t.getProperty("bigDataUrl")); TrackConfig config = new TrackConfig(url); config.setPanelName(IGV.DATA_PANEL_NAME); @@ -322,7 +337,7 @@ TrackConfig getTrackConfig(Stanza t) { // TODO -- work on recognizing big* formats // config.format = t.format(); - config.setUrl(this.baseURL + t.getProperty("bigDataUrl")); + config.setUrl(getDataURL(t.getProperty("bigDataUrl"))); // Expanded display mode does not work well in IGV desktop for some tracks //config.displayMode = t.displayMode(); @@ -332,13 +347,13 @@ TrackConfig getTrackConfig(Stanza t) { } if (t.hasProperty("longLabel") && t.hasProperty("html")) { - config.setDescription("" + t.getProperty("longLabel") + ""); + config.setDescription("" + t.getProperty("longLabel") + "")); } else if (t.hasProperty("longLabel")) { config.setDescription(t.getProperty("longLabel")); } - if(t.hasProperty("html")) { - config.setHtml(this.baseURL + t.getProperty("html")); + if (t.hasProperty("html")) { + config.setHtml(getDataURL(t.getProperty("html"))); } config.setVisible(!("hide".equals(t.getProperty("visibility")))); @@ -382,7 +397,7 @@ TrackConfig getTrackConfig(Stanza t) { config.setSearchIndex(t.getProperty("searchIndex")); } if (t.hasProperty("searchTrix")) { - config.setTrixURL(this.baseURL + t.getProperty("searchTrix")); + config.setTrixURL(getDataURL(t.getProperty("searchTrix"))); } if (t.hasProperty("group")) { From c2691bd81bdf277f375d91eb2d11621824c28436 Mon Sep 17 00:00:00 2001 From: Jim Robinson <933148+jrobinso@users.noreply.github.com> Date: Sun, 26 Jan 2025 19:23:48 -0800 Subject: [PATCH 102/130] Hub track menu improvements -- collapsible categories (#1648) --- src/main/java/org/broad/igv/ucsc/Hub.java | 3 +- .../igv/ucsc/HubTrackSelectionDialog.java | 45 +++++------ .../broad/igv/ui/panel/CollapsiblePanel.java | 70 ++++++++++++++++++ .../org/broad/igv/ui/util/IconFactory.java | 8 +- src/main/resources/images/minus_sm.gif | Bin 0 -> 110 bytes src/main/resources/images/plus_sm.gif | Bin 0 -> 116 bytes 6 files changed, 103 insertions(+), 23 deletions(-) create mode 100644 src/main/java/org/broad/igv/ui/panel/CollapsiblePanel.java create mode 100644 src/main/resources/images/minus_sm.gif create mode 100644 src/main/resources/images/plus_sm.gif diff --git a/src/main/java/org/broad/igv/ucsc/Hub.java b/src/main/java/org/broad/igv/ucsc/Hub.java index 86c6db14b6..fdbc99024d 100644 --- a/src/main/java/org/broad/igv/ucsc/Hub.java +++ b/src/main/java/org/broad/igv/ucsc/Hub.java @@ -356,7 +356,8 @@ TrackConfig getTrackConfig(Stanza t) { config.setHtml(getDataURL(t.getProperty("html"))); } - config.setVisible(!("hide".equals(t.getProperty("visibility")))); + String visibility = t.getProperty("visibility"); + config.setVisible(visibility != null && !("hide".equals(visibility))); if (t.hasProperty("autoScale")) { config.setAutoscale(t.getProperty("autoScale").toLowerCase().equals("on")); diff --git a/src/main/java/org/broad/igv/ucsc/HubTrackSelectionDialog.java b/src/main/java/org/broad/igv/ucsc/HubTrackSelectionDialog.java index 11ce541de8..2fe6b1b5c9 100644 --- a/src/main/java/org/broad/igv/ucsc/HubTrackSelectionDialog.java +++ b/src/main/java/org/broad/igv/ucsc/HubTrackSelectionDialog.java @@ -2,10 +2,10 @@ import org.broad.igv.Globals; import org.broad.igv.feature.genome.load.TrackConfig; +import org.broad.igv.ui.panel.CollapsiblePanel; import org.broad.igv.ui.util.HyperlinkFactory; import javax.swing.*; -import javax.swing.border.Border; import java.awt.*; import java.io.IOException; import java.lang.reflect.InvocationTargetException; @@ -13,6 +13,7 @@ import java.util.List; import java.util.stream.Collectors; + /** * Dialog to enable selection of tracks defined by track hubs. Modifies the "visible" property of * supplied track configurations in place. @@ -32,23 +33,21 @@ public HubTrackSelectionDialog(List groupedTrackConfigurations void init(List trackConfigurations) { - configMap = new HashMap<>(); - setTitle("Select tracks to load"); -// JLabel headerMessage = new JLabel("Select tracks to load"); -// headerMessage.setFont(FontManager.getFont(Font.BOLD, 14)); -// headerMessage.setHorizontalAlignment(SwingConstants.CENTER); -// headerMessage.setPreferredSize(new Dimension(300, 50)); -// add(headerMessage, BorderLayout.NORTH); + + configMap = new HashMap<>(); JPanel mainPanel = new JPanel(); mainPanel.setLayout(new BorderLayout()); - JScrollPane scrollPane = new JScrollPane(mainPanel); - add(scrollPane, BorderLayout.CENTER); + // JScrollPane scrollPane = new JScrollPane(mainPanel); + // add(scrollPane, BorderLayout.CENTER); + + add(mainPanel); JPanel categoryContainer = new JPanel(); categoryContainer.setLayout(new BoxLayout(categoryContainer, BoxLayout.PAGE_AXIS)); - mainPanel.add(categoryContainer); + JScrollPane scrollPane = new JScrollPane(categoryContainer); + mainPanel.add(scrollPane, BorderLayout.CENTER); // Panel for select all/none JPanel checkAllPanel = new JPanel(); @@ -64,10 +63,11 @@ void init(List trackConfigurations) { //mainPanel.add(checkAllPanel, BorderLayout.NORTH); + // Loop through track groups List cpl = new ArrayList<>(); for (TrackConfigGroup configGroup : trackConfigurations) { categoryContainer.add(Box.createVerticalStrut(10)); - JPanel categoryPanel = categoryPanel(configGroup); + JPanel categoryPanel = createCategoryPanel(configGroup); categoryContainer.add(categoryPanel); cpl.add(categoryPanel); } @@ -121,25 +121,27 @@ private void okAction() { * @param configGroup * @return */ - private JPanel categoryPanel(TrackConfigGroup configGroup) { + private JPanel createCategoryPanel(TrackConfigGroup configGroup) { - JPanel container = new JPanel(); - container.setLayout(new BorderLayout()); - Border border = BorderFactory.createLineBorder(Color.lightGray);// BorderFactory.createLoweredBevelBorder(); - container.setBorder(BorderFactory.createTitledBorder(border, configGroup.label)); +// JPanel container = new JPanel(); +// container.setLayout(new BorderLayout()); +// Border border = BorderFactory.createLineBorder(Color.lightGray);// BorderFactory.createLoweredBevelBorder(); +// container.setBorder(BorderFactory.createTitledBorder(border, configGroup.label)); JPanel trackContainer = new JPanel(); final WrapLayout wrapLayout = new WrapLayout(); wrapLayout.setAlignment(FlowLayout.LEFT); trackContainer.setLayout(wrapLayout); - container.add(trackContainer); + //container.add(trackContainer); + boolean isSelected = false; for (TrackConfig trackConfig : configGroup.tracks) { JPanel p = new JPanel(); final JCheckBox checkBox = new JCheckBox(); configMap.put(checkBox, trackConfig); checkBox.setSelected(trackConfig.getVisible()); + isSelected = isSelected || trackConfig.getVisible(); JLabel label = trackConfig.getHtml() == null ? new JLabel(trackConfig.getName()) : @@ -152,7 +154,8 @@ private JPanel categoryPanel(TrackConfigGroup configGroup) { trackContainer.add(p); } - return container; + return new CollapsiblePanel(configGroup.label, trackContainer, isSelected); + // return container; } /** @@ -377,12 +380,12 @@ private void addRow(Dimension dim, int rowWidth, int rowHeight) { */ public static void main(String[] args) throws InterruptedException, InvocationTargetException, IOException { - String hubFile = "test/data/hubs/hub.txt"; + String hubFile = "https://hgdownload.soe.ucsc.edu/gbdb/hs1/hubs/public/hub.txt"; Hub hub = Hub.loadHub(hubFile); List groupedTrackConfigurations = hub.getGroupedTrackConfigurations(); final HubTrackSelectionDialog dlf = new HubTrackSelectionDialog(groupedTrackConfigurations, null); - + dlf.setSize(800, 500); dlf.setVisible(true); for (TrackConfig config : dlf.getSelectedConfigs()) { diff --git a/src/main/java/org/broad/igv/ui/panel/CollapsiblePanel.java b/src/main/java/org/broad/igv/ui/panel/CollapsiblePanel.java new file mode 100644 index 0000000000..249c979fc6 --- /dev/null +++ b/src/main/java/org/broad/igv/ui/panel/CollapsiblePanel.java @@ -0,0 +1,70 @@ +package org.broad.igv.ui.panel; + +import org.broad.igv.ui.FontManager; +import org.broad.igv.ui.util.IconFactory; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; + +public class CollapsiblePanel extends JPanel { + + public static final Color HEADER_BG = new Color(210, 210, 210); + + + private final JButton collapseButton; + private ImageIcon openIcon; + private ImageIcon closeIcon; + + public CollapsiblePanel(String label, JComponent content) { + this(label, content, false); + } + + public CollapsiblePanel(String label, JComponent content, boolean isOpen) { + + setLayout(new BorderLayout()); + + setBorder(BorderFactory.createLineBorder(Color.BLACK)); + + this.openIcon = IconFactory.getInstance().getIcon(IconFactory.IconID.MINUS); + this.closeIcon = IconFactory.getInstance().getIcon(IconFactory.IconID.PLUS); + + content.setVisible(isOpen); + this.add(content, BorderLayout.CENTER); + + JPanel header = new JPanel(); + header.setLayout(new BorderLayout()); + header.setBackground(HEADER_BG); + + this.collapseButton = new JButton(); + collapseButton.setIcon(isOpen ? openIcon : closeIcon); + collapseButton.setBorder(new EmptyBorder(10, 5, 10, 0)); + collapseButton.setHorizontalAlignment(SwingConstants.LEFT); + + collapseButton.addActionListener(e -> { + collapseButton.setIcon(content.isVisible() ? closeIcon : openIcon); + content.setVisible(!content.isVisible()); + }); + header.add(collapseButton, BorderLayout.WEST); + + final JLabel jLabel = new JLabel(label); + jLabel.setFont(FontManager.getFont(14)); + jLabel.setHorizontalAlignment(SwingConstants.CENTER); + header.add(jLabel, BorderLayout.CENTER); + + this.add(header, BorderLayout.NORTH); + + } + + + public static void main(String[] args) { + + JComponent content = new JTextArea("alskdfjalskdjflsdkjfsaldkfjsladkfjsalkfj"); + CollapsiblePanel cp = new CollapsiblePanel("Expand/Collapse", content); + + JFrame f = new JFrame("test"); + f.setSize(500, 500); + f.setContentPane(cp); + f.setVisible(true); + } +} diff --git a/src/main/java/org/broad/igv/ui/util/IconFactory.java b/src/main/java/org/broad/igv/ui/util/IconFactory.java index d4ca07d184..e29d07d211 100644 --- a/src/main/java/org/broad/igv/ui/util/IconFactory.java +++ b/src/main/java/org/broad/igv/ui/util/IconFactory.java @@ -51,7 +51,9 @@ public enum IconID { REFRESH, HOME, FIST, - CLOSE + CLOSE, + PLUS, + MINUS } private Map icons; @@ -104,6 +106,10 @@ private IconFactory() { createImageIcon("/images/tooltip.png", "tooltip")); icons.put(IconID.CLOSE, createImageIcon("/images/crystal/fileclose.png", "close")); + icons.put(IconID.PLUS, + createImageIcon("/images/plus_sm.gif" , "plus")); + icons.put(IconID.MINUS, + createImageIcon("/images/minus_sm.gif", "minus")); } public ImageIcon getIcon(IconID id) { diff --git a/src/main/resources/images/minus_sm.gif b/src/main/resources/images/minus_sm.gif new file mode 100644 index 0000000000000000000000000000000000000000..c96dc72063373592ad063b9b1735f3d954a3429a GIT binary patch literal 110 zcmZ?wbhEHb6k-r!Si}GV)2C0*&d&b-|G%A`-TU|N!D0+LAPS_0f!Ug4*Bxo)Q=aY( zrpH&CzQ4-u_h?7rGf%-B?@rf?yHW&>hbkVu-s``M~!DKo%cf9qiN^DOB^~J(*vG(Jew7@ N>T*N!Z4U+pYXEBsDHi|$ literal 0 HcmV?d00001 From 3bf983dd15ca96e5d2172bb63f9c191cd65595d5 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Mon, 3 Feb 2025 22:28:23 -0800 Subject: [PATCH 103/130] fix diagnostic messages from startup scripts --- scripts/igv.command | 2 +- scripts/igv.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/igv.command b/scripts/igv.command index 14d0aff07e..23fa89e6f5 100755 --- a/scripts/igv.command +++ b/scripts/igv.command @@ -18,7 +18,7 @@ if [ -d "${prefix}/jdk-21" ]; then JAVA_HOME="${prefix}/jdk-21" PATH=$JAVA_HOME/bin:$PATH else - echo "Using system JDK. IGV requires Java 17." + echo "Using system JDK. IGV requires Java 21." fi # Report on Java version diff --git a/scripts/igv.sh b/scripts/igv.sh index 400457f09e..eb290c295d 100755 --- a/scripts/igv.sh +++ b/scripts/igv.sh @@ -16,7 +16,7 @@ if [ -d "${prefix}/jdk-21" ]; then JAVA_HOME="${prefix}/jdk-21" PATH=$JAVA_HOME/bin:$PATH else - echo "Using system JDK. IGV requires Java 17." + echo "Using system JDK. IGV requires Java 21." fi # Report on Java version From 9df597599e6e4835a65fe233d80f1610ba16330c Mon Sep 17 00:00:00 2001 From: Jim Robinson <933148+jrobinso@users.noreply.github.com> Date: Tue, 4 Feb 2025 09:52:50 -0800 Subject: [PATCH 104/130] Additional track hub support: (#1651) * Additional track hub support: * Fix bugs preventing loading of CHM13 / hs1 hub. Fixes #1643 * Enable addition of hubs as a track source to any IGV genome * UI improvements to track selection dialog --- src/main/java/module-info.java | 1 + .../org/broad/igv/feature/genome/Genome.java | 32 +- .../igv/feature/genome/load/GenomeConfig.java | 12 +- .../feature/genome/load/HubGenomeLoader.java | 11 +- .../igv/feature/genome/load/TrackConfig.java | 22 +- .../igv/ucsc/HubTrackSelectionDialog.java | 396 ------------------ .../org/broad/igv/ucsc/TrackConfigGroup.java | 16 - .../org/broad/igv/ucsc/{ => hub}/Hub.java | 209 +++++---- .../broad/igv/ucsc/hub/TrackConfigGroup.java | 27 ++ .../igv/ucsc/hub/TrackHubSelectionDialog.java | 219 ++++++++++ .../org/broad/igv/ucsc/hub/WrapLayout.java | 204 +++++++++ .../java/org/broad/igv/ui/IGVMenuBar.java | 146 +++---- ...Action.java => SelectHubTracksAction.java} | 38 +- .../broad/igv/ui/panel/CollapsiblePanel.java | 39 +- .../org/broad/igv/ucsc/{ => hub}/HubTest.java | 7 +- 15 files changed, 765 insertions(+), 614 deletions(-) delete mode 100644 src/main/java/org/broad/igv/ucsc/HubTrackSelectionDialog.java delete mode 100644 src/main/java/org/broad/igv/ucsc/TrackConfigGroup.java rename src/main/java/org/broad/igv/ucsc/{ => hub}/Hub.java (71%) create mode 100644 src/main/java/org/broad/igv/ucsc/hub/TrackConfigGroup.java create mode 100644 src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java create mode 100644 src/main/java/org/broad/igv/ucsc/hub/WrapLayout.java rename src/main/java/org/broad/igv/ui/action/{SelectGenomeAnnotationTracksAction.java => SelectHubTracksAction.java} (77%) rename src/test/java/org/broad/igv/ucsc/{ => hub}/HubTest.java (94%) diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 20ea2ca08f..075e7b3e75 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -12,6 +12,7 @@ opens org.broad.igv.feature.genome.load to com.google.gson; opens org.broad.igv.feature to com.google.gson; exports org.broad.igv.ucsc; + exports org.broad.igv.ucsc.hub; requires com.google.common; requires commons.math3; diff --git a/src/main/java/org/broad/igv/feature/genome/Genome.java b/src/main/java/org/broad/igv/feature/genome/Genome.java index 2d5772d8bb..9b3b421990 100644 --- a/src/main/java/org/broad/igv/feature/genome/Genome.java +++ b/src/main/java/org/broad/igv/feature/genome/Genome.java @@ -53,7 +53,7 @@ import org.broad.igv.logging.Logger; import org.broad.igv.track.FeatureTrack; import org.broad.igv.track.TribbleFeatureSource; -import org.broad.igv.ucsc.Hub; +import org.broad.igv.ucsc.hub.Hub; import org.broad.igv.ucsc.twobit.TwoBitSequence; import org.broad.igv.util.ResourceLocator; import org.broad.igv.util.liftover.Liftover; @@ -96,7 +96,8 @@ public class Genome { private String homeChromosome; private String defaultPos; private String nameSet; - private Hub hub; + private Hub genomeHub; + private List trackHubs; public Genome(GenomeConfig config) throws IOException { @@ -104,6 +105,7 @@ public Genome(GenomeConfig config) throws IOException { displayName = config.getName(); nameSet = config.getNameSet(); blatDB = config.getBlatDB(); + trackHubs = new ArrayList<>(); if (config.getUcsdID() == null) { ucscID = ucsdIDMap.containsKey(id) ? ucsdIDMap.get(id) : id; } else { @@ -226,6 +228,16 @@ public Genome(GenomeConfig config) throws IOException { // TODO -- no place to go } + if(config.getHubs() != null) { + for(String hubUrl : config.getHubs()) { + try { + trackHubs.add(Hub.loadHub(hubUrl)); + } catch (IOException e) { + log.error("Error loading hub", e); + } + } + } + addTracks(config); @@ -254,6 +266,8 @@ public Genome(String id, List chromosomes) { this.longChromosomeNames = computeLongChromosomeNames(); this.homeChromosome = this.longChromosomeNames.size() > 1 ? Globals.CHR_ALL : chromosomeNames.get(0); this.chromAliasSource = (new ChromAliasDefaults(id, chromosomeNames)); + + this.trackHubs = new ArrayList<>(); } private void addTracks(GenomeConfig config) { @@ -794,12 +808,18 @@ public List computeLongChromosomeNames() { } - public Hub getHub() { - return hub; + public Hub getGenomeHub() { + return genomeHub; + } + + public void setGenomeHub(Hub genomeHub) { + this.genomeHub = genomeHub; + // A genome hub is by definition also a track hub + this.trackHubs.add(genomeHub); } - public void setHub(Hub hub) { - this.hub = hub; + public List getTrackHubs() { + return trackHubs; } public synchronized static Genome nullGenome() { diff --git a/src/main/java/org/broad/igv/feature/genome/load/GenomeConfig.java b/src/main/java/org/broad/igv/feature/genome/load/GenomeConfig.java index 5fd57257ec..4b97b3f8c0 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/GenomeConfig.java +++ b/src/main/java/org/broad/igv/feature/genome/load/GenomeConfig.java @@ -54,7 +54,7 @@ public class GenomeConfig implements Cloneable { private List tracks; - private List annotations; // Backward compatibility, synonym for tracks + private List hubs; // The properties below support the legacy ".genome" file, which directly loads resources from a zip archive. @@ -72,6 +72,14 @@ public static GenomeConfig fromJson(String json) { public GenomeConfig() { } + public List getHubs() { + return hubs; + } + + public void setHubs(List hubs) { + this.hubs = hubs; + } + public String getId() { return id; } @@ -257,7 +265,7 @@ public void setChromSizesURL(String chromSizesURL) { } public List getTrackConfigs() { - return tracks != null ? tracks : annotations; + return tracks; } public void setTracks(List tracks) { diff --git a/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java b/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java index 3a4fd54844..1619581dde 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java +++ b/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java @@ -3,10 +3,10 @@ import org.broad.igv.Globals; import org.broad.igv.feature.genome.Genome; import org.broad.igv.prefs.PreferencesManager; -import org.broad.igv.ucsc.Hub; -import org.broad.igv.ucsc.TrackConfigGroup; +import org.broad.igv.ucsc.hub.Hub; +import org.broad.igv.ucsc.hub.TrackConfigGroup; import org.broad.igv.ui.IGV; -import org.broad.igv.ucsc.HubTrackSelectionDialog; +import org.broad.igv.ucsc.hub.TrackHubSelectionDialog; import java.io.IOException; import java.util.*; @@ -60,7 +60,6 @@ public Genome loadGenome() throws IOException { List selectedTracks = groupedTrackConfigurations.stream() .flatMap(group -> group.tracks.stream()) .filter(trackConfig -> selectedTrackNames.contains(trackConfig.getName())) - .sorted(Comparator.comparingInt(TrackConfig::getOrder)) .collect(Collectors.toList()); config.setTracks(selectedTracks); } @@ -79,7 +78,7 @@ else if (IGV.hasInstance() && !Globals.isBatch() && !Globals.isHeadless() && !Gl groupedTrackConfigurations.stream().filter(g -> g.label.startsWith("Gene")).collect(Collectors.toList()); - HubTrackSelectionDialog dlg = new HubTrackSelectionDialog(filteredGroups, IGV.getInstance().getMainFrame()); + TrackHubSelectionDialog dlg = new TrackHubSelectionDialog(hub, filteredGroups, IGV.getInstance().getMainFrame()); dlg.setVisible(true); List selectedTracks = dlg.getSelectedConfigs(); @@ -91,7 +90,7 @@ else if (IGV.hasInstance() && !Globals.isBatch() && !Globals.isHeadless() && !Gl } Genome genome = new Genome(config); - genome.setHub(hub); + genome.setGenomeHub(hub); return genome; diff --git a/src/main/java/org/broad/igv/feature/genome/load/TrackConfig.java b/src/main/java/org/broad/igv/feature/genome/load/TrackConfig.java index 5cab6af46a..ba548e1fa2 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/TrackConfig.java +++ b/src/main/java/org/broad/igv/feature/genome/load/TrackConfig.java @@ -2,7 +2,6 @@ /** * A static json-like object, emulates javascript equivalent. Created to ease port of session code from javascript. - * */ public class TrackConfig implements Cloneable { @@ -26,20 +25,21 @@ public class TrackConfig implements Cloneable { private Boolean visible; private String infoURL; private String searchIndex; - private String group; - private Integer order; private Integer visibilityWindow; private Boolean indexed; private Boolean hidden; private String html; private String panelName; + private String stanzaParent; // For supporting track hubs + public TrackConfig() { } /** * The only required property of a track configuration is a URL (which can be an actual URL or a static file path) + * * @param url */ public TrackConfig(String url) { @@ -206,14 +206,6 @@ public void setGroup(String group) { this.group = group; } - public Integer getOrder() { - return order; - } - - public void setOrder(Integer order) { - this.order = order; - } - public Integer getVisibilityWindow() { return visibilityWindow; } @@ -254,6 +246,14 @@ public void setPanelName(String panelName) { this.panelName = panelName; } + public String getStanzaParent() { + return stanzaParent; + } + + public void setStanzaParent(String stanzaParent) { + this.stanzaParent = stanzaParent; + } + @Override protected TrackConfig clone() throws CloneNotSupportedException { return (TrackConfig) super.clone(); diff --git a/src/main/java/org/broad/igv/ucsc/HubTrackSelectionDialog.java b/src/main/java/org/broad/igv/ucsc/HubTrackSelectionDialog.java deleted file mode 100644 index 2fe6b1b5c9..0000000000 --- a/src/main/java/org/broad/igv/ucsc/HubTrackSelectionDialog.java +++ /dev/null @@ -1,396 +0,0 @@ -package org.broad.igv.ucsc; - -import org.broad.igv.Globals; -import org.broad.igv.feature.genome.load.TrackConfig; -import org.broad.igv.ui.panel.CollapsiblePanel; -import org.broad.igv.ui.util.HyperlinkFactory; - -import javax.swing.*; -import java.awt.*; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.util.*; -import java.util.List; -import java.util.stream.Collectors; - - -/** - * Dialog to enable selection of tracks defined by track hubs. Modifies the "visible" property of - * supplied track configurations in place. - */ -public class HubTrackSelectionDialog extends JDialog { - - private static final Dimension TRACK_SPACER = new Dimension(10, 5); - private Map configMap; - - public HubTrackSelectionDialog(List groupedTrackConfigurations, Frame owner) { - super(owner); - setModal(true); - init(groupedTrackConfigurations); - setLocationRelativeTo(owner); - } - - - void init(List trackConfigurations) { - - setTitle("Select tracks to load"); - - configMap = new HashMap<>(); - - JPanel mainPanel = new JPanel(); - mainPanel.setLayout(new BorderLayout()); - // JScrollPane scrollPane = new JScrollPane(mainPanel); - // add(scrollPane, BorderLayout.CENTER); - - add(mainPanel); - - JPanel categoryContainer = new JPanel(); - categoryContainer.setLayout(new BoxLayout(categoryContainer, BoxLayout.PAGE_AXIS)); - JScrollPane scrollPane = new JScrollPane(categoryContainer); - mainPanel.add(scrollPane, BorderLayout.CENTER); - - // Panel for select all/none - JPanel checkAllPanel = new JPanel(); - ((FlowLayout) checkAllPanel.getLayout()).setAlignment(FlowLayout.LEFT); - JButton selectAllButton = new JButton("Select All"); - selectAllButton.setFocusPainted(false); - selectAllButton.addActionListener(e -> configMap.keySet().forEach(cb -> cb.setSelected(true))); - checkAllPanel.add(selectAllButton); - JButton selectNoneButton = new JButton("Select None"); - selectNoneButton.addActionListener(e -> configMap.keySet().forEach(cb -> cb.setSelected(false))); - checkAllPanel.add(selectNoneButton); - categoryContainer.add(checkAllPanel); - //mainPanel.add(checkAllPanel, BorderLayout.NORTH); - - - // Loop through track groups - List cpl = new ArrayList<>(); - for (TrackConfigGroup configGroup : trackConfigurations) { - categoryContainer.add(Box.createVerticalStrut(10)); - JPanel categoryPanel = createCategoryPanel(configGroup); - categoryContainer.add(categoryPanel); - cpl.add(categoryPanel); - } - - JPanel buttonPanel = new JPanel(); - ((FlowLayout) buttonPanel.getLayout()).setAlignment(FlowLayout.RIGHT); - - JButton cancelButton = new JButton("Cancel"); - cancelButton.addActionListener(e -> setVisible(false)); - - JButton okButton = new JButton("OK"); - okButton.addActionListener(e -> okAction()); - - if (Globals.IS_MAC) { - buttonPanel.add(cancelButton); - buttonPanel.add(okButton); - } else { - buttonPanel.add(okButton); - buttonPanel.add(cancelButton); - } - - getRootPane().setDefaultButton(okButton); - - mainPanel.add(buttonPanel, BorderLayout.SOUTH); - - // Try to adjust height to "just enough", BoxLayout will potentially leave empty space otherwise. There might - // be a better way to achieve this. - int h = 75; - for (JPanel p : cpl) h += p.getMinimumSize().height; - setSize(new Dimension(800, Math.min(800, h))); - pack(); - revalidate(); - - } - - private void okAction() { - for (Map.Entry entry : configMap.entrySet()) { - final TrackConfig trackConfig = entry.getValue(); - if (entry.getKey().isSelected()) { - trackConfig.setVisible(true); - } else { - trackConfig.setVisible(false); - } - } - setVisible(false); - } - - /** - * Create a JPanel for a particular track category, including a label and checkboxes for contained tracks. - * - * @param configGroup - * @return - */ - private JPanel createCategoryPanel(TrackConfigGroup configGroup) { - -// JPanel container = new JPanel(); -// container.setLayout(new BorderLayout()); -// Border border = BorderFactory.createLineBorder(Color.lightGray);// BorderFactory.createLoweredBevelBorder(); -// container.setBorder(BorderFactory.createTitledBorder(border, configGroup.label)); - - JPanel trackContainer = new JPanel(); - final WrapLayout wrapLayout = new WrapLayout(); - wrapLayout.setAlignment(FlowLayout.LEFT); - trackContainer.setLayout(wrapLayout); - - //container.add(trackContainer); - boolean isSelected = false; - for (TrackConfig trackConfig : configGroup.tracks) { - - JPanel p = new JPanel(); - final JCheckBox checkBox = new JCheckBox(); - configMap.put(checkBox, trackConfig); - checkBox.setSelected(trackConfig.getVisible()); - isSelected = isSelected || trackConfig.getVisible(); - - JLabel label = trackConfig.getHtml() == null ? - new JLabel(trackConfig.getName()) : - HyperlinkFactory.createLink(trackConfig.getName(), trackConfig.getHtml()); - label.setLabelFor(checkBox); - - p.add(checkBox); - p.add(label); - p.add(Box.createRigidArea(TRACK_SPACER)); - trackContainer.add(p); - } - - return new CollapsiblePanel(configGroup.label, trackContainer, isSelected); - // return container; - } - - /** - * Convenience method to extract and return selected track configurations. - * - * @return - */ - public List getSelectedConfigs() { - List selected = configMap.values().stream().filter(trackConfig -> trackConfig.getVisible()).collect(Collectors.toList()); - selected.sort((o1, o2) -> o1.getOrder() - o2.getOrder()); - return selected; - } - - /** - * A FlowLayout extension that wraps components as needed and updates the preferre size accordingly. - * FlowLayout itself does not update the preferred size, so the component width grows without bounds. - *

- * Code courtesy Rob Camick. See https://tips4java.wordpress.com/2008/11/06/wrap-layout/ - *

- * MIT License - *

- * Copyright (c) 2023 Rob Camick - *

- * 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. - */ - static class WrapLayout extends FlowLayout { - private Dimension preferredLayoutSize; - - /** - * Constructs a new WrapLayout with a left - * alignment and a default 5-unit horizontal and vertical gap. - */ - public WrapLayout() { - super(); - } - - /** - * Constructs a new FlowLayout with the specified - * alignment and a default 5-unit horizontal and vertical gap. - * The value of the alignment argument must be one of - * WrapLayout, WrapLayout, - * or WrapLayout. - * - * @param align the alignment value - */ - public WrapLayout(int align) { - super(align); - } - - /** - * Creates a new flow layout manager with the indicated alignment - * and the indicated horizontal and vertical gaps. - *

- * The value of the alignment argument must be one of - * WrapLayout, WrapLayout, - * or WrapLayout. - * - * @param align the alignment value - * @param hgap the horizontal gap between components - * @param vgap the vertical gap between components - */ - public WrapLayout(int align, int hgap, int vgap) { - super(align, hgap, vgap); - } - - /** - * Returns the preferred dimensions for this layout given the - * visible components in the specified target container. - * - * @param target the component which needs to be laid out - * @return the preferred dimensions to lay out the - * subcomponents of the specified container - */ - @Override - public Dimension preferredLayoutSize(Container target) { - return layoutSize(target, true); - } - - /** - * Returns the minimum dimensions needed to layout the visible - * components contained in the specified target container. - * - * @param target the component which needs to be laid out - * @return the minimum dimensions to lay out the - * subcomponents of the specified container - */ - @Override - public Dimension minimumLayoutSize(Container target) { - Dimension minimum = layoutSize(target, false); - minimum.width -= (getHgap() + 1); - return minimum; - } - - /** - * Returns the minimum or preferred dimension needed to layout the target - * container. - * - * @param target target to get layout size for - * @param preferred should preferred size be calculated - * @return the dimension to layout the target container - */ - private Dimension layoutSize(Container target, boolean preferred) { - synchronized (target.getTreeLock()) { - // Each row must fit with the width allocated to the containter. - // When the container width = 0, the preferred width of the container - // has not yet been calculated so lets ask for the maximum. - - int targetWidth = target.getSize().width; - Container container = target; - - while (container.getSize().width == 0 && container.getParent() != null) { - container = container.getParent(); - } - - targetWidth = container.getSize().width; - - if (targetWidth == 0) - targetWidth = Integer.MAX_VALUE; - - int hgap = getHgap(); - int vgap = getVgap(); - Insets insets = target.getInsets(); - int horizontalInsetsAndGap = insets.left + insets.right + (hgap * 2); - int maxWidth = targetWidth - horizontalInsetsAndGap; - - // Fit components into the allowed width - - Dimension dim = new Dimension(0, 0); - int rowWidth = 0; - int rowHeight = 0; - - int nmembers = target.getComponentCount(); - - for (int i = 0; i < nmembers; i++) { - Component m = target.getComponent(i); - - if (m.isVisible()) { - Dimension d = preferred ? m.getPreferredSize() : m.getMinimumSize(); - - // Can't add the component to current row. Start a new row. - - if (rowWidth + d.width > maxWidth) { - addRow(dim, rowWidth, rowHeight); - rowWidth = 0; - rowHeight = 0; - } - - // Add a horizontal gap for all components after the first - - if (rowWidth != 0) { - rowWidth += hgap; - } - - rowWidth += d.width; - rowHeight = Math.max(rowHeight, d.height); - } - } - - addRow(dim, rowWidth, rowHeight); - - dim.width += horizontalInsetsAndGap; - dim.height += insets.top + insets.bottom + vgap * 2; - - // When using a scroll pane or the DecoratedLookAndFeel we need to - // make sure the preferred size is less than the size of the - // target containter so shrinking the container size works - // correctly. Removing the horizontal gap is an easy way to do this. - - Container scrollPane = SwingUtilities.getAncestorOfClass(JScrollPane.class, target); - - if (scrollPane != null && target.isValid()) { - dim.width -= (hgap + 1); - } - - return dim; - } - } - - /* - * A new row has been completed. Use the dimensions of this row - * to update the preferred size for the container. - * - * @param dim update the width and height when appropriate - * @param rowWidth the width of the row to add - * @param rowHeight the height of the row to add - */ - private void addRow(Dimension dim, int rowWidth, int rowHeight) { - dim.width = Math.max(dim.width, rowWidth); - - if (dim.height > 0) { - dim.height += getVgap(); - } - - dim.height += rowHeight; - } - } - - - /** - * main for testing and development - * - * @param args - * @throws InterruptedException - * @throws InvocationTargetException - * @throws IOException - */ - public static void main(String[] args) throws InterruptedException, InvocationTargetException, IOException { - - String hubFile = "https://hgdownload.soe.ucsc.edu/gbdb/hs1/hubs/public/hub.txt"; - Hub hub = Hub.loadHub(hubFile); - List groupedTrackConfigurations = hub.getGroupedTrackConfigurations(); - - final HubTrackSelectionDialog dlf = new HubTrackSelectionDialog(groupedTrackConfigurations, null); - dlf.setSize(800, 500); - dlf.setVisible(true); - - for (TrackConfig config : dlf.getSelectedConfigs()) { - System.out.println(config.getName()); - } - } - -} diff --git a/src/main/java/org/broad/igv/ucsc/TrackConfigGroup.java b/src/main/java/org/broad/igv/ucsc/TrackConfigGroup.java deleted file mode 100644 index ef266db7a0..0000000000 --- a/src/main/java/org/broad/igv/ucsc/TrackConfigGroup.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.broad.igv.ucsc; - -import org.broad.igv.feature.genome.load.TrackConfig; - -import java.util.List; - -public class TrackConfigGroup { - - public String label; - public List tracks; - - public TrackConfigGroup(String label, List tracks) { - this.label = label; - this.tracks = tracks; - } -} diff --git a/src/main/java/org/broad/igv/ucsc/Hub.java b/src/main/java/org/broad/igv/ucsc/hub/Hub.java similarity index 71% rename from src/main/java/org/broad/igv/ucsc/Hub.java rename to src/main/java/org/broad/igv/ucsc/hub/Hub.java index fdbc99024d..a86736d18a 100644 --- a/src/main/java/org/broad/igv/ucsc/Hub.java +++ b/src/main/java/org/broad/igv/ucsc/hub/Hub.java @@ -1,4 +1,4 @@ -package org.broad.igv.ucsc; +package org.broad.igv.ucsc.hub; import org.broad.igv.Globals; import org.broad.igv.feature.genome.load.GenomeConfig; @@ -14,6 +14,7 @@ import java.net.URL; import java.util.*; import java.util.function.Function; +import java.util.stream.Collectors; public class Hub { @@ -21,7 +22,7 @@ public class Hub { private final String url; private final String host; String baseURL; - Stanza hub; + Stanza hubStanza; Stanza genomeStanza; List trackStanzas; List groupStanzas; @@ -30,13 +31,17 @@ public class Hub { static Set supportedTypes = new HashSet(Arrays.asList("bigBed", "bigWig", "bigGenePred", "vcfTabix")); static Set filterTracks = new HashSet(Arrays.asList("cytoBandIdeo", "assembly", "gap", "gapOverlap", "allGaps", "cpgIslandExtUnmasked", "windowMasker")); + static Map vizModeMap = Map.of( + "pack", "EXPANDED", + "full", "EXPANDED", + "squish", "SQUISHED", + "dense", "COLLAPSED"); public static Hub loadHub(String url) throws IOException { - return new Hub(url); } - private Hub(String url) throws IOException { + Hub(String url) throws IOException { this.url = url; @@ -44,7 +49,7 @@ private Hub(String url) throws IOException { String baseURL = url.substring(0, idx + 1); this.baseURL = baseURL; - if(url.startsWith("https://") || url.startsWith("http://")) { + if (url.startsWith("https://") || url.startsWith("http://")) { try { URL tmp = new URL(url); this.host = tmp.getProtocol() + "://" + tmp.getHost(); @@ -53,8 +58,7 @@ private Hub(String url) throws IOException { log.error("Error parsing base URL host", e); throw new RuntimeException(e); } - } - else { + } else { // Local file, no host this.host = ""; } @@ -75,10 +79,10 @@ private Hub(String url) throws IOException { throw new RuntimeException("Unexpected hub file -- expected 'genome' stanza but found " + stanzas.get(1).type); } - this.hub = stanzas.get(0); + this.hubStanza = stanzas.get(0); // Load groups - this. genomeStanza = stanzas.get(1); + this.genomeStanza = stanzas.get(1); if (genomeStanza.hasProperty("groups")) { String groupsTxtURL = getDataURL(genomeStanza.getProperty("groups")); this.groupStanzas = loadStanzas(groupsTxtURL); @@ -109,30 +113,49 @@ private Hub(String url) throws IOException { this.groupPriorityMap = new HashMap<>(); for (Stanza g : groupStanzas) { if (g.hasProperty("priority")) { - this.groupPriorityMap.put(g.getProperty("name"), getPriority(g)); + this.groupPriorityMap.put(g.getProperty("name"), getPriority(g.getProperty("priority"))); } } } } + + public String getShortLabel() { + return this.hubStanza.hasProperty("shortLabel") ? this.hubStanza.getProperty("shortLabel") : this.url; + } + + public String getLongLabel() { + return this.hubStanza.hasProperty("longLabel") ? this.hubStanza.getProperty("longLabel") : this.url; + } + + public String getDescriptionURL() { + return this.hubStanza.hasProperty("descriptionUrl") ? + this.getDataURL(this.hubStanza.getProperty("descriptionUrl")) : + this.hubStanza.hasProperty("desriptionUrl") ? + this.getDataURL(this.hubStanza.getProperty("desriptionUrl")) : null; + } + + /** * Return the priority for the group. The priority format is uncertain, but extends to at least 2 levels (e.g. 3.4). - * Ignore levels > 2 + * Ignore levels > 3 * - * @param g the group stanza + * @param priorityString Priority as a string (e.g. 3.4) * @return A priority as an integer */ - private static int getPriority(Stanza g) { - String priorityString = g.getProperty("priority"); + private static int getPriority(String priorityString) { String[] tokens = priorityString.split("\\."); - int p = Integer.parseInt(tokens[0]) * 10; + int p = Integer.parseInt(tokens[0]) * 100; if (tokens.length > 1) { - p += Integer.parseInt(tokens[1]); + p += Integer.parseInt(tokens[1]) * 10; + } + if (tokens.length > 2) { + p += Integer.parseInt(tokens[2]); } return p; } - private static List loadStanzas(String url) throws IOException { + static List loadStanzas(String url) throws IOException { List nodes = new ArrayList<>(); Stanza currentNode = null; boolean startNewNode = true; @@ -153,7 +176,7 @@ private static List loadStanzas(String url) throws IOException { if (startNewNode) { // Start a new node -- indent is currently ignored as igv.js does not support sub-tracks, // so track stanzas are flattened - Stanza newNode = new Stanza(++order, key, value); + Stanza newNode = new Stanza(key, value); nodes.add(newNode); currentNode = newNode; startNewNode = false; @@ -163,7 +186,6 @@ private static List loadStanzas(String url) throws IOException { } } return resolveParents(nodes); - } private static int indentLevel(String str) { @@ -271,7 +293,7 @@ shortLabel Chromosome Band (Ideogram) if (includeTracks) { Function filter = (t) -> !Hub.filterTracks.contains(t.name) && (!"hide".equals(t.getProperty("visibility"))); - config.setTracks(this.getTracksConfigs(filter)); + config.setTracks(this.getTrackConfigs(filter)); } // config.trackConfigurations = this.#getGroupedTrackConfigurations() @@ -285,37 +307,77 @@ private String getDataURL(String relativeURL) { public List getGroupedTrackConfigurations() { - // Organize track configs by group - LinkedHashMap> trackConfigMap = new LinkedHashMap<>(); - java.util.function.Function filter = (stanza -> !stanza.name.equals("cytoBandIdeo")); - for (TrackConfig c : this.getTracksConfigs(filter)) { - String groupName = c.getGroup() != null ? c.getGroup() : "other"; - trackConfigMap.computeIfAbsent(groupName, k -> new ArrayList<>()).add(c); - } - - // Extract map of group names - Map groupNamesMap = new HashMap<>(); + // Build map of group objects + Map groupMap = new HashMap<>(); + groupMap.put("other", new TrackConfigGroup("other", "Other", Integer.MAX_VALUE, false)); if (this.groupStanzas != null) { for (Stanza groupStanza : this.groupStanzas) { - groupNamesMap.put(groupStanza.getProperty("name"), groupStanza.getProperty("label")); + String name = groupStanza.getProperty("name"); + boolean defaultOpen = "0".equals(groupStanza.getProperty("defaultIsClosed")); + int priority = groupStanza.hasProperty("priority") ? getPriority(groupStanza.getProperty("priority")) : Integer.MAX_VALUE - 1; + groupMap.put(name, new TrackConfigGroup(name, groupStanza.getProperty("label"), priority, defaultOpen)); } } - // Use linked has map to maintain order - List groupedTrackConfigurations = new ArrayList<>(); - for (Map.Entry> entry : trackConfigMap.entrySet()) { - String group = entry.getKey(); - String label = groupNamesMap.containsKey(group) ? groupNamesMap.get(group) : group; - groupedTrackConfigurations.add(new TrackConfigGroup(label, entry.getValue())); + // Build map of stanzas to resolve parents + Map trackStanzaMap = new HashMap<>(); + for (Stanza s : this.trackStanzas) { + trackStanzaMap.put(s.getProperty("track"), s); + } + // Initialized cache of track containers. Use linked hashmap to maintain insertion order + LinkedHashMap parentCache = new LinkedHashMap<>(); + + final List trackConfigs = this.getTrackConfigs(stanza -> !stanza.name.equals("cytoBandIdeo")); + for (TrackConfig c : trackConfigs) { + String groupName = c.getGroup() != null ? c.getGroup() : "other"; + final TrackConfigGroup trackConfigGroup = groupMap.get(groupName); + int priority = trackConfigGroup.priority; + trackConfigGroup.tracks.add(c); + + String parentName = c.getStanzaParent(); + if (parentName != null && trackStanzaMap.containsKey(parentName)) { + // Create a contingent container, will be used if a sufficient # of tracks belong to this container + TrackConfigGroup container = parentCache.get(parentName); + if (container == null) { + Stanza s = trackStanzaMap.get(parentName); + String label = trackConfigGroup.label + " - " + s.getProperty("shortLabel"); + container = new TrackConfigGroup(parentName, label, priority + 1, false); + parentCache.put(parentName, container); + } + container.tracks.add(c); + } + } + + // Flatten the track groups into a list. Remove empty groups. + List groupedTrackConfigurations = groupMap.values().stream() + .filter(g -> !g.isEmpty()).collect(Collectors.toList()); + + // Promote contingent embedded track groups (e.g. composite tracks) to top level group if # of tracks > threshold + // Member tracks must also be removed from existing top level categories + List tmp = new ArrayList<>(); + Set toRemove = new HashSet<>(); + for (TrackConfigGroup parent : parentCache.values()) { + if (parent.tracks.size() > 5) { + tmp.add(parent); + toRemove.addAll(parent.tracks); + } } + if (toRemove.size() > 0) { + for (TrackConfigGroup g : groupedTrackConfigurations) { + g.tracks = g.tracks.stream().filter(t -> !toRemove.contains(t)).collect(Collectors.toList()); + } + } + groupedTrackConfigurations.addAll(tmp); + + Collections.sort(groupedTrackConfigurations, Comparator.comparingInt(o -> o.priority)); return groupedTrackConfigurations; } /** * Return an array of igv track config objects that satisfy the filter */ - List getTracksConfigs(java.util.function.Function filter) { + List getTrackConfigs(java.util.function.Function filter) { return this.trackStanzas.stream().filter(t -> { return supportedTypes.contains(t.format()) && t.hasProperty("bigDataUrl") && (filter == null || filter.apply(t)); }) @@ -323,6 +385,7 @@ List getTracksConfigs(java.util.function.Function .toList(); } + TrackConfig getTrackConfig(Stanza t) { String format = t.format(); @@ -356,8 +419,17 @@ TrackConfig getTrackConfig(Stanza t) { config.setHtml(getDataURL(t.getProperty("html"))); } - String visibility = t.getProperty("visibility"); - config.setVisible(visibility != null && !("hide".equals(visibility))); + String vizProperty = t.getProperty("visibility"); + + if (vizProperty != null && vizModeMap.containsKey(vizProperty)) { + config.setDisplayMode(vizModeMap.get(vizProperty)); + } + + boolean visibility = t.hasProperty("compositeTrack") ? + "on".equals(t.getProperty("compositeTrack")) : + !("hide".equals(vizProperty)); + + config.setVisible(visibility); if (t.hasProperty("autoScale")) { config.setAutoscale(t.getProperty("autoScale").toLowerCase().equals("on")); @@ -403,11 +475,10 @@ TrackConfig getTrackConfig(Stanza t) { if (t.hasProperty("group")) { config.setGroup(t.getProperty("group")); - if (this.groupPriorityMap != null && this.groupPriorityMap.containsKey(config.getGroup())) { - int nextPriority = this.groupPriorityMap.get(config.getGroup()) + 1; - config.setOrder(nextPriority); - this.groupPriorityMap.put(config.getGroup(), nextPriority); - } + } + + if (t.parent != null) { + config.setStanzaParent(t.parent.name); } return config; @@ -420,27 +491,37 @@ public String getUrl() { static class Stanza { + private static Set parentOverrideProperties = new HashSet<>(Arrays.asList("visibility", "priority", "group")); private final String type; private final String name; - private final int order; - + private Stanza parent; private Map properties; - Stanza parent; - Stanza(int order, String type, String name) { - this.order = order; + Stanza(String type, String name) { + this.type = type; this.name = name; this.properties = new HashMap<>(); } + public String getType() { + return type; + } + + public String getName() { + return name; + } + void setProperty(String key, String value) { this.properties.put(key, value); } String getProperty(String key) { - if (this.properties.containsKey(key)) { + + if (parentOverrideProperties.contains(key) && this.parent != null && this.parent.hasProperty(key)) { + return this.parent.getProperty(key); + } else if (this.properties.containsKey(key)) { return this.properties.get(key); } else if (this.parent != null) { return this.parent.getProperty(key); @@ -468,26 +549,12 @@ String format() { return null; // unknown type } - /** - * IGV display mode - */ - String displayMode() { - String viz = this.getProperty("visibility"); - if (viz == null) { - return "COLLAPSED"; - } else { - viz = viz.toLowerCase(); - switch (viz) { - case "dense": - return "COLLAPSED"; - case "pack": - return "EXPANDED"; - case "squish": - return "SQUISHED"; - default: - return "COLLAPSED"; - } - } + public Stanza getParent() { + return parent; + } + + public void setParent(Stanza parent) { + this.parent = parent; } } diff --git a/src/main/java/org/broad/igv/ucsc/hub/TrackConfigGroup.java b/src/main/java/org/broad/igv/ucsc/hub/TrackConfigGroup.java new file mode 100644 index 0000000000..72fe9202b8 --- /dev/null +++ b/src/main/java/org/broad/igv/ucsc/hub/TrackConfigGroup.java @@ -0,0 +1,27 @@ +package org.broad.igv.ucsc.hub; + +import org.broad.igv.feature.genome.load.TrackConfig; + +import java.util.ArrayList; +import java.util.List; + +public class TrackConfigGroup { + + public int priority; + public String name; + public String label; + public boolean defaultOpen; + public List tracks; + + public TrackConfigGroup(String name, String label, int priority, boolean defaultOpen) { + this.name = name; + this.priority = priority; + this.label = label; + this.defaultOpen = defaultOpen; + this.tracks = new ArrayList<>(); + } + + public boolean isEmpty() { + return tracks.isEmpty(); + } +} diff --git a/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java b/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java new file mode 100644 index 0000000000..813a048a14 --- /dev/null +++ b/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java @@ -0,0 +1,219 @@ +package org.broad.igv.ucsc.hub; + +import org.broad.igv.Globals; +import org.broad.igv.feature.genome.load.TrackConfig; +import org.broad.igv.ui.panel.CollapsiblePanel; +import org.broad.igv.ui.util.HyperlinkFactory; + +import javax.swing.*; +import java.awt.*; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.*; +import java.util.List; +import java.util.stream.Collectors; + + +/** + * Dialog to enable selection of tracks defined by track hubs. Modifies the "visible" property of + * supplied track configurations in place. + */ +public class TrackHubSelectionDialog extends JDialog { + + Hub hub; + private Map configMap; + private ArrayList categoryPanels; + + public TrackHubSelectionDialog(Hub hub, List groupedTrackConfigurations, Frame owner) { + super(owner); + setModal(true); + this.hub = hub; + init(groupedTrackConfigurations); + setLocationRelativeTo(owner); + } + + + void init(List trackConfigurations) { + + setTitle(this.hub.getLongLabel()); + + setSize(new Dimension(1000, 800)); + + configMap = new HashMap<>(); + categoryPanels = new ArrayList<>(); + + JPanel mainPanel = new JPanel(); + mainPanel.setLayout(new BorderLayout()); + add(mainPanel); + + JPanel topPanel = new JPanel(); + topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.Y_AXIS)); + topPanel.add(getLabeledHyperlink("Hub URL: ", hub.getUrl())); + String descriptionURL = hub.getDescriptionURL(); + if(descriptionURL != null) { + topPanel.add(getLabeledHyperlink("Description: ", descriptionURL)); + } + + // Panel for select all/none + JPanel topButtonPanel = new JPanel(); + ((FlowLayout) topButtonPanel.getLayout()).setAlignment(FlowLayout.LEFT); + JButton expandAllButton = new JButton("Expand All"); + expandAllButton.addActionListener(e -> { + categoryPanels.forEach(cp -> cp.expand()); + this.revalidate(); + }); + topButtonPanel.add(expandAllButton); + + JButton collapseAllButton = new JButton("Collapse All"); + collapseAllButton.addActionListener(e -> { + categoryPanels.forEach(cp -> cp.collapse()); + this.revalidate(); + }); + topButtonPanel.add(collapseAllButton); + + topPanel.add(topButtonPanel); + mainPanel.add(topPanel, BorderLayout.NORTH); + + // Panel for category boxes + JPanel categoryContainer = new JPanel(); + categoryContainer.setLayout(new BoxLayout(categoryContainer, BoxLayout.PAGE_AXIS)); + JScrollPane scrollPane = new JScrollPane(categoryContainer); + scrollPane.setBorder(BorderFactory.createLineBorder(Color.gray)); + mainPanel.add(scrollPane, BorderLayout.CENTER); + + // Loop through track groups + for (TrackConfigGroup configGroup : trackConfigurations) { + categoryContainer.add(Box.createVerticalStrut(10)); + CollapsiblePanel categoryPanel = createCategoryPanel(configGroup); + categoryContainer.add(categoryPanel); + categoryPanels.add(categoryPanel); + } + + JPanel buttonPanel = new JPanel(); + ((FlowLayout) buttonPanel.getLayout()).setAlignment(FlowLayout.RIGHT); + + JButton cancelButton = new JButton("Cancel"); + cancelButton.addActionListener(e -> setVisible(false)); + + JButton okButton = new JButton("OK"); + okButton.addActionListener(e -> okAction()); + + if (Globals.IS_MAC) { + buttonPanel.add(cancelButton); + buttonPanel.add(okButton); + } else { + buttonPanel.add(okButton); + buttonPanel.add(cancelButton); + } + + getRootPane().setDefaultButton(okButton); + + mainPanel.add(buttonPanel, BorderLayout.SOUTH); + + } + + private JPanel getLabeledHyperlink(String label, String url) { + JPanel hubURLPanel = new JPanel(); + ((FlowLayout) hubURLPanel.getLayout()).setAlignment(FlowLayout.LEFT); + hubURLPanel.setBorder(BorderFactory.createEmptyBorder(2, 6, 0, 0)); + hubURLPanel.add(new JLabel(label)); + hubURLPanel.add(HyperlinkFactory.createLink(url, url)); + return hubURLPanel; + } + + private void okAction() { + for (Map.Entry entry : configMap.entrySet()) { + final TrackConfig trackConfig = entry.getValue(); + if (entry.getKey().isSelected()) { + trackConfig.setVisible(true); + } else { + trackConfig.setVisible(false); + } + } + setVisible(false); + } + + /** + * Create a JPanel for a particular track category, including a label and checkboxes for contained tracks. + * + * @param configGroup + * @return + */ + private CollapsiblePanel createCategoryPanel(TrackConfigGroup configGroup) { + + JPanel trackContainer = new JPanel(); + final WrapLayout wrapLayout = new WrapLayout(); + wrapLayout.setAlignment(FlowLayout.LEFT); + trackContainer.setLayout(wrapLayout); + + boolean isSelected = false; + for (TrackConfig trackConfig : configGroup.tracks) { + + final JCheckBox checkBox = new JCheckBox(); + configMap.put(checkBox, trackConfig); + checkBox.setSelected(trackConfig.getVisible()); + isSelected = isSelected || trackConfig.getVisible(); + +// JLabel label = trackConfig.getHtml() == null ? +// new JLabel(trackConfig.getName()) : +// HyperlinkFactory.createLink(trackConfig.getName(), trackConfig.getHtml()); + JLabel label = new JLabel(trackConfig.getName()); + + + SelectionBox p = new SelectionBox(checkBox, label); + trackContainer.add(p); + } + + return new CollapsiblePanel(configGroup.label, trackContainer, isSelected || configGroup.defaultOpen); + } + + /** + * Convenience method to extract and return selected track configurations. + * + * @return + */ + public List getSelectedConfigs() { + List selected = configMap.values().stream().filter(trackConfig -> trackConfig.getVisible()).collect(Collectors.toList()); + return selected; + } + + static class SelectionBox extends JPanel { + + public SelectionBox(JCheckBox checkBox, JLabel label) { + this.setLayout(new BorderLayout()); + label.setLabelFor(checkBox); + add(checkBox, BorderLayout.WEST); + add(label, BorderLayout.CENTER); + + } + + @Override + public Dimension getPreferredSize() { + return new Dimension(200, 20); + } + } + + + /** + * main for testing and development + * + * @param args + * @throws InterruptedException + * @throws InvocationTargetException + * @throws IOException + */ + public static void main(String[] args) throws InterruptedException, InvocationTargetException, IOException { + + String hubFile = "https://hgdownload.soe.ucsc.edu/gbdb/hs1/hubs/public/hub.txt"; + Hub hub = Hub.loadHub(hubFile); + List groupedTrackConfigurations = hub.getGroupedTrackConfigurations(); + + final TrackHubSelectionDialog dlf = new TrackHubSelectionDialog(hub, groupedTrackConfigurations, null); + dlf.setVisible(true); + + for (TrackConfig config : dlf.getSelectedConfigs()) { + System.out.println(config.getName()); + } + } + +} diff --git a/src/main/java/org/broad/igv/ucsc/hub/WrapLayout.java b/src/main/java/org/broad/igv/ucsc/hub/WrapLayout.java new file mode 100644 index 0000000000..2e5a194aec --- /dev/null +++ b/src/main/java/org/broad/igv/ucsc/hub/WrapLayout.java @@ -0,0 +1,204 @@ +package org.broad.igv.ucsc.hub; + +import javax.swing.*; +import java.awt.*; + +/** + * A FlowLayout extension that wraps components as needed and updates the preferre size accordingly. + * FlowLayout itself does not update the preferred size, so the component width grows without bounds. + *

+ * Code courtesy Rob Camick. See https://tips4java.wordpress.com/2008/11/06/wrap-layout/ + *

+ * MIT License + *

+ * Copyright (c) 2023 Rob Camick + *

+ * 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. + */ +class WrapLayout extends FlowLayout { + private Dimension preferredLayoutSize; + + /** + * Constructs a new WrapLayout with a left + * alignment and a default 5-unit horizontal and vertical gap. + */ + public WrapLayout() { + super(); + } + + /** + * Constructs a new FlowLayout with the specified + * alignment and a default 5-unit horizontal and vertical gap. + * The value of the alignment argument must be one of + * WrapLayout, WrapLayout, + * or WrapLayout. + * + * @param align the alignment value + */ + public WrapLayout(int align) { + super(align); + } + + /** + * Creates a new flow layout manager with the indicated alignment + * and the indicated horizontal and vertical gaps. + *

+ * The value of the alignment argument must be one of + * WrapLayout, WrapLayout, + * or WrapLayout. + * + * @param align the alignment value + * @param hgap the horizontal gap between components + * @param vgap the vertical gap between components + */ + public WrapLayout(int align, int hgap, int vgap) { + super(align, hgap, vgap); + } + + /** + * Returns the preferred dimensions for this layout given the + * visible components in the specified target container. + * + * @param target the component which needs to be laid out + * @return the preferred dimensions to lay out the + * subcomponents of the specified container + */ + @Override + public Dimension preferredLayoutSize(Container target) { + return layoutSize(target, true); + } + + /** + * Returns the minimum dimensions needed to layout the visible + * components contained in the specified target container. + * + * @param target the component which needs to be laid out + * @return the minimum dimensions to lay out the + * subcomponents of the specified container + */ + @Override + public Dimension minimumLayoutSize(Container target) { + Dimension minimum = layoutSize(target, false); + minimum.width -= (getHgap() + 1); + return minimum; + } + + /** + * Returns the minimum or preferred dimension needed to layout the target + * container. + * + * @param target target to get layout size for + * @param preferred should preferred size be calculated + * @return the dimension to layout the target container + */ + private Dimension layoutSize(Container target, boolean preferred) { + synchronized (target.getTreeLock()) { + // Each row must fit with the width allocated to the containter. + // When the container width = 0, the preferred width of the container + // has not yet been calculated so lets ask for the maximum. + + int targetWidth = target.getSize().width; + Container container = target; + + while (container.getSize().width == 0 && container.getParent() != null) { + container = container.getParent(); + } + + targetWidth = container.getSize().width; + + if (targetWidth == 0) + targetWidth = Integer.MAX_VALUE; + + int hgap = getHgap(); + int vgap = getVgap(); + Insets insets = target.getInsets(); + int horizontalInsetsAndGap = insets.left + insets.right + (hgap * 2); + int maxWidth = targetWidth - horizontalInsetsAndGap; + + // Fit components into the allowed width + + Dimension dim = new Dimension(0, 0); + int rowWidth = 0; + int rowHeight = 0; + + int nmembers = target.getComponentCount(); + + for (int i = 0; i < nmembers; i++) { + Component m = target.getComponent(i); + + if (m.isVisible()) { + Dimension d = preferred ? m.getPreferredSize() : m.getMinimumSize(); + + // Can't add the component to current row. Start a new row. + + if (rowWidth + d.width > maxWidth) { + addRow(dim, rowWidth, rowHeight); + rowWidth = 0; + rowHeight = 0; + } + + // Add a horizontal gap for all components after the first + + if (rowWidth != 0) { + rowWidth += hgap; + } + + rowWidth += d.width; + rowHeight = Math.max(rowHeight, d.height); + } + } + + addRow(dim, rowWidth, rowHeight); + + dim.width += horizontalInsetsAndGap; + dim.height += insets.top + insets.bottom + vgap * 2; + + // When using a scroll pane or the DecoratedLookAndFeel we need to + // make sure the preferred size is less than the size of the + // target containter so shrinking the container size works + // correctly. Removing the horizontal gap is an easy way to do this. + + Container scrollPane = SwingUtilities.getAncestorOfClass(JScrollPane.class, target); + + if (scrollPane != null && target.isValid()) { + dim.width -= (hgap + 1); + } + + return dim; + } + } + + /* + * A new row has been completed. Use the dimensions of this row + * to update the preferred size for the container. + * + * @param dim update the width and height when appropriate + * @param rowWidth the width of the row to add + * @param rowHeight the height of the row to add + */ + private void addRow(Dimension dim, int rowWidth, int rowHeight) { + dim.width = Math.max(dim.width, rowWidth); + + if (dim.height > 0) { + dim.height += getVgap(); + } + + dim.height += rowHeight; + } +} diff --git a/src/main/java/org/broad/igv/ui/IGVMenuBar.java b/src/main/java/org/broad/igv/ui/IGVMenuBar.java index 6ec97373f7..e997bb0ccc 100644 --- a/src/main/java/org/broad/igv/ui/IGVMenuBar.java +++ b/src/main/java/org/broad/igv/ui/IGVMenuBar.java @@ -40,6 +40,7 @@ import org.broad.igv.feature.genome.ChromSizesUtils; import org.broad.igv.track.AttributeManager; import org.broad.igv.track.Track; +import org.broad.igv.ucsc.hub.Hub; import org.broad.igv.ui.commandbar.HostedGenomeSelectionDialog; import org.broad.igv.util.GoogleUtils; import org.broad.igv.oauth.OAuthProvider; @@ -108,16 +109,11 @@ public class IGVMenuBar extends JMenuBar implements IGVEventObserver { * we can't access genome server list */ private JMenuItem loadGenomeFromServerMenuItem; - private JMenuItem loadTracksFromServerMenuItem; private JMenuItem selectGenomeAnnotationsItem; - private JMenuItem encodeUCSCMenuItem; - private List encodeMenuItems = new ArrayList<>(); - private JMenuItem reloadSessionItem; private JMenuItem recentFilesMenu; - static IGVMenuBar createInstance(IGV igv) { if (instance != null) { if (igv == instance.igv) { @@ -170,9 +166,10 @@ public void showAboutDialog() { private List createMenus() { - List menus = new ArrayList(); + List menus = new ArrayList<>(); - menus.add(createFileMenu()); + JMenu fileMenu = createFileMenu(GenomeManager.getInstance().getCurrentGenome()); + menus.add(fileMenu); menus.add(createGenomesMenu()); menus.add(createViewMenu()); menus.add(createTracksMenu()); @@ -273,13 +270,17 @@ public void enableExtrasMenu() { } - JMenu createFileMenu() { + JMenu createFileMenu(Genome genome) { - Genome genome = GenomeManager.getInstance().getCurrentGenome(); - String genomeId = genome == null ? null : genome.getId(); + if (genome == null) { + return new JMenu("File"); + } - List menuItems = new ArrayList(); - MenuAction menuAction = null; + String genomeId = genome.getId(); + + List menuItems = new ArrayList<>(); + + MenuAction menuAction; menuItems.add(new JSeparator()); @@ -292,46 +293,57 @@ JMenu createFileMenu() { menuAction.setToolTipText(UIConstants.LOAD_TRACKS_TOOLTIP); menuItems.add(MenuAndToolbarUtils.createMenuItem(menuAction)); - menuAction = new LoadFromServerAction("Load From Server...", KeyEvent.VK_S, igv); - menuAction.setToolTipText(UIConstants.LOAD_SERVER_DATA_TOOLTIP); - loadTracksFromServerMenuItem = MenuAndToolbarUtils.createMenuItem(menuAction); - menuItems.add(loadTracksFromServerMenuItem); + if (genomeId != null && LoadFromServerAction.getNodeURLs(genomeId) != null) { + menuAction = new LoadFromServerAction("Load From IGV Server...", KeyEvent.VK_S, igv); + menuAction.setToolTipText(UIConstants.LOAD_SERVER_DATA_TOOLTIP); + JMenuItem loadTracksFromServerMenuItem = MenuAndToolbarUtils.createMenuItem(menuAction); + menuItems.add(loadTracksFromServerMenuItem); + } + recentFilesMenu = new RecentUrlsMenu(); menuItems.add(recentFilesMenu); - if (PreferencesManager.getPreferences().getAsBoolean(DB_ENABLED)) { - menuAction = new LoadFromDatabaseAction("Load from Database...", 0, igv); - menuItems.add(MenuAndToolbarUtils.createMenuItem(menuAction)); + // Track hubs + if (genome.getTrackHubs().size() > 0) { + menuItems.add(new JSeparator()); + for (Hub trackHub : genome.getTrackHubs()) { + String label = "Hub: " + trackHub.getShortLabel(); + menuAction = new SelectHubTracksAction(label, igv, trackHub); + JMenuItem selectGenomeAnnotationsItem = MenuAndToolbarUtils.createMenuItem(menuAction); + selectGenomeAnnotationsItem.setToolTipText(trackHub.getLongLabel()); + menuItems.add(selectGenomeAnnotationsItem); + } } // ENCODE items. These will be hidden / shown depending on genome chosen - JSeparator separator = new JSeparator(); - menuItems.add(separator); + if (EncodeTrackChooser.genomeSupportedUCSC(genomeId) || EncodeTrackChooser.genomeSupported(genomeId)) { - // Post 2012 ENCODE menu - JMenuItem chipItem = new JMenuItem(); - chipItem.setAction(new BrowseEncodeAction("ENCODE ChIP Signals ...", 0, BrowseEncodeAction.Type.SIGNALS_CHIP, igv)); - encodeMenuItems.add(chipItem); + JSeparator separator = new JSeparator(); + menuItems.add(separator); - JMenuItem otherSignalsItem = new JMenuItem(); - otherSignalsItem.setAction(new BrowseEncodeAction("ENCODE Other Signals ...", 0, BrowseEncodeAction.Type.SIGNALS_OTHER, igv)); - encodeMenuItems.add(otherSignalsItem); + // Post 2012 ENCODE menu + if (EncodeTrackChooser.genomeSupported(genomeId)) { + JMenuItem chipItem = new JMenuItem(); + chipItem.setAction(new BrowseEncodeAction("ENCODE ChIP Signals ...", 0, BrowseEncodeAction.Type.SIGNALS_CHIP, igv)); + menuItems.add(chipItem); - JMenuItem otherItem = new JMenuItem(); - otherItem.setAction(new BrowseEncodeAction("ENCODE Other ...", 0, BrowseEncodeAction.Type.OTHER, igv)); - encodeMenuItems.add(otherItem); + JMenuItem otherSignalsItem = new JMenuItem(); + otherSignalsItem.setAction(new BrowseEncodeAction("ENCODE Other Signals ...", 0, BrowseEncodeAction.Type.SIGNALS_OTHER, igv)); + menuItems.add(otherSignalsItem); - for(JComponent item : encodeMenuItems) { - menuItems.add(item); - item.setVisible(EncodeTrackChooser.genomeSupported(genomeId)); - } + JMenuItem otherItem = new JMenuItem(); + otherItem.setAction(new BrowseEncodeAction("ENCODE Other ...", 0, BrowseEncodeAction.Type.OTHER, igv)); + menuItems.add(otherItem); + } - // UCSC hosted ENCODE menu. - encodeUCSCMenuItem = MenuAndToolbarUtils.createMenuItem( - new BrowseEncodeAction("ENCODE 2012 UCSC Repository ...", KeyEvent.VK_E, BrowseEncodeAction.Type.UCSC, igv)); - encodeUCSCMenuItem.setVisible(EncodeTrackChooser.genomeSupportedUCSC(genomeId)); - menuItems.add(encodeUCSCMenuItem); + // UCSC hosted ENCODE menu. + if (EncodeTrackChooser.genomeSupportedUCSC(genomeId)) { + JMenuItem encodeUCSCMenuItem = MenuAndToolbarUtils.createMenuItem( + new BrowseEncodeAction("ENCODE 2012 UCSC Repository ...", KeyEvent.VK_E, BrowseEncodeAction.Type.UCSC, igv)); + menuItems.add(encodeUCSCMenuItem); + } + } menuItems.add(new JSeparator()); menuAction = new ReloadTracksMenuAction("Reload Tracks", -1, igv); @@ -408,7 +420,8 @@ public void actionPerformed(ActionEvent e) { recentSessionsSep.setVisible(false); menuItems.add(recentSessionsSep); //menuItems.addAll(addRecentSessionMenuItems()); - menuItems.add(new JSeparator());; + menuItems.add(new JSeparator()); + ; MenuAction fileMenuAction = new MenuAction("File", null, KeyEvent.VK_F); JMenu fileMenu = MenuAndToolbarUtils.createMenu(menuItems, fileMenuAction); @@ -423,40 +436,6 @@ public void actionPerformed(ActionEvent e) { return fileMenu; } - private void addEncodeItems(List menuItems, String genomeId) { - - JSeparator separator = new JSeparator(); - menuItems.add(separator); - - JLabel encodeLabel = new JLabel(" ENCODE"); - encodeLabel.setFont(encodeLabel.getFont().deriveFont(Font.BOLD)); - menuItems.add(encodeLabel); - - // Post 2012 ENCODE menu - JMenuItem chipItem = new JMenuItem(); - chipItem.setAction(new BrowseEncodeAction("CHiP - Signals", 0, BrowseEncodeAction.Type.SIGNALS_CHIP, igv)); - encodeMenuItems.add(chipItem); - - JMenuItem otherSignalsItem = new JMenuItem(); - otherSignalsItem.setAction(new BrowseEncodeAction("Other - Signals", 0, BrowseEncodeAction.Type.SIGNALS_OTHER, igv)); - encodeMenuItems.add(otherSignalsItem); - - JMenuItem otherItem = new JMenuItem(); - otherItem.setAction(new BrowseEncodeAction("Other (peaks, calls, ...)", 0, BrowseEncodeAction.Type.OTHER, igv)); - encodeMenuItems.add(otherItem); - - for (JComponent item : encodeMenuItems) { - menuItems.add(item); - item.setVisible(EncodeTrackChooser.genomeSupported(genomeId)); - } - - // UCSC hosted ENCODE menu. - encodeUCSCMenuItem = MenuAndToolbarUtils.createMenuItem( - new BrowseEncodeAction("ENCODE (2012)...", KeyEvent.VK_E, BrowseEncodeAction.Type.UCSC, igv)); - encodeUCSCMenuItem.setVisible(EncodeTrackChooser.genomeSupportedUCSC(genomeId)); - menuItems.add(encodeUCSCMenuItem); - } - private JMenu createGenomesMenu() { JMenu menu = new JMenu("Genomes"); @@ -501,10 +480,10 @@ private JMenu createGenomesMenu() { MenuAction genArkAction = new UCSCGenArkAction("Load Genome from UCSC GenArk...", 0, igv); menu.add(MenuAndToolbarUtils.createMenuItem(genArkAction)); - MenuAction menuAction = new SelectGenomeAnnotationTracksAction("Select GenArk Tracks...", igv); + MenuAction menuAction = new SelectHubTracksAction("Select GenArk Genome Tracks...", igv, null); selectGenomeAnnotationsItem = MenuAndToolbarUtils.createMenuItem(menuAction); Genome newGenome = GenomeManager.getInstance().getCurrentGenome(); - selectGenomeAnnotationsItem.setEnabled(newGenome != null && newGenome.getHub() != null); + selectGenomeAnnotationsItem.setEnabled(newGenome != null && newGenome.getGenomeHub() != null); menu.add(selectGenomeAnnotationsItem); menu.add(new JSeparator()); @@ -521,7 +500,7 @@ public void actionPerformed(ActionEvent event) { menu.addMenuListener((MenuSelectedListener) e -> { Genome genome1 = GenomeManager.getInstance().getCurrentGenome(); - selectGenomeAnnotationsItem.setEnabled(genome1 != null && genome1.getHub() != null); + selectGenomeAnnotationsItem.setEnabled(genome1 != null && genome1.getGenomeHub() != null); }); return menu; @@ -1186,13 +1165,10 @@ public void receiveEvent(final IGVEvent event) { if (event instanceof GenomeChangeEvent) { UIUtilities.invokeOnEventThread(() -> { + IGVMenuBar.this.remove(0); final Genome genome = ((GenomeChangeEvent) event).genome(); - final String genomeId = genome.getId(); - encodeUCSCMenuItem.setVisible(EncodeTrackChooser.genomeSupportedUCSC(genomeId)); - for (JComponent item : encodeMenuItems) { - item.setVisible(EncodeTrackChooser.genomeSupported(genomeId)); - } - + JMenu fileMenu = createFileMenu(genome); + IGVMenuBar.this.add(fileMenu, 0); }); } } @@ -1201,7 +1177,7 @@ public void enableReloadSession() { this.reloadSessionItem.setEnabled(true); } - public void showRecentFilesMenu(){ + public void showRecentFilesMenu() { this.recentFilesMenu.setVisible(true); } diff --git a/src/main/java/org/broad/igv/ui/action/SelectGenomeAnnotationTracksAction.java b/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java similarity index 77% rename from src/main/java/org/broad/igv/ui/action/SelectGenomeAnnotationTracksAction.java rename to src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java index d6671aa3bd..2cba4a0729 100644 --- a/src/main/java/org/broad/igv/ui/action/SelectGenomeAnnotationTracksAction.java +++ b/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java @@ -29,8 +29,6 @@ */ package org.broad.igv.ui.action; -//~--- non-JDK imports -------------------------------------------------------- - import org.broad.igv.feature.genome.Genome; import org.broad.igv.feature.genome.GenomeManager; import org.broad.igv.feature.genome.load.TrackConfig; @@ -38,9 +36,9 @@ import org.broad.igv.logging.Logger; import org.broad.igv.prefs.PreferencesManager; import org.broad.igv.track.Track; -import org.broad.igv.ucsc.Hub; -import org.broad.igv.ucsc.TrackConfigGroup; -import org.broad.igv.ucsc.HubTrackSelectionDialog; +import org.broad.igv.ucsc.hub.Hub; +import org.broad.igv.ucsc.hub.TrackConfigGroup; +import org.broad.igv.ucsc.hub.TrackHubSelectionDialog; import org.broad.igv.ui.IGV; import org.broad.igv.ui.util.MessageUtils; import org.broad.igv.util.ResourceLocator; @@ -49,30 +47,40 @@ import java.util.*; /** + * Select tracks from a track hub. This action is used in 2 modes, (1) load tracks from a specific hub, and (2) select + * default annotation tracks for the currently loaded genome. Tracks are loaded in both modes, but in the + * second selected tracks are also added to the genome definition. + * * @author jrobinso */ -public class SelectGenomeAnnotationTracksAction extends MenuAction { +public class SelectHubTracksAction extends MenuAction { - static Logger log = LogManager.getLogger(SelectGenomeAnnotationTracksAction.class); + static Logger log = LogManager.getLogger(SelectHubTracksAction.class); + private Hub hub; IGV mainFrame; // Keep track of authorization failures so user isn't constantly harranged static HashSet failedURLs = new HashSet(); + boolean updateGenome; - public SelectGenomeAnnotationTracksAction(String label, IGV mainFrame) { + public SelectHubTracksAction(String label, IGV mainFrame, Hub hub) { super(label, null); this.mainFrame = mainFrame; + this.updateGenome = hub == null; + this.hub = hub; } @Override public void actionPerformed(ActionEvent evt) { Genome genome = GenomeManager.getInstance().getCurrentGenome(); - Hub hub = genome.getHub(); - if (hub == null) { - // This should not happen - MessageUtils.showMessage("No annotation tracks available for current genome."); + if(hub == null) { + hub = genome.getGenomeHub(); + if (hub == null) { + // This should not happen + MessageUtils.showMessage("No tracks available for current genome."); + } } final List loadedTracks = IGV.getInstance().getAllTracks().stream().filter(t -> t.getResourceLocator() != null).toList(); @@ -84,7 +92,7 @@ public void actionPerformed(ActionEvent evt) { } } - HubTrackSelectionDialog dlg = new HubTrackSelectionDialog(groups, IGV.getInstance().getMainFrame()); + TrackHubSelectionDialog dlg = new TrackHubSelectionDialog(hub, groups, IGV.getInstance().getMainFrame()); dlg.setVisible(true); // The dialog action will modify the visible state for each track config @@ -111,7 +119,9 @@ public void actionPerformed(ActionEvent evt) { IGV.getInstance().loadTracks(locators); // Update genome - genome.setAnnotationResources(locators); + if(updateGenome) { + genome.setAnnotationResources(locators); + } // Update preferences String key = "hub:" + hub.getUrl(); diff --git a/src/main/java/org/broad/igv/ui/panel/CollapsiblePanel.java b/src/main/java/org/broad/igv/ui/panel/CollapsiblePanel.java index 249c979fc6..2e86236588 100644 --- a/src/main/java/org/broad/igv/ui/panel/CollapsiblePanel.java +++ b/src/main/java/org/broad/igv/ui/panel/CollapsiblePanel.java @@ -9,10 +9,12 @@ public class CollapsiblePanel extends JPanel { - public static final Color HEADER_BG = new Color(210, 210, 210); + public static final Color HEADER_BG = new Color(180,204,226); private final JButton collapseButton; + private final JComponent content; + private final JPanel header; private ImageIcon openIcon; private ImageIcon closeIcon; @@ -23,8 +25,9 @@ public CollapsiblePanel(String label, JComponent content) { public CollapsiblePanel(String label, JComponent content, boolean isOpen) { setLayout(new BorderLayout()); + this.content = content; - setBorder(BorderFactory.createLineBorder(Color.BLACK)); + //setBorder(BorderFactory.createLineBorder(Color.BLACK)); this.openIcon = IconFactory.getInstance().getIcon(IconFactory.IconID.MINUS); this.closeIcon = IconFactory.getInstance().getIcon(IconFactory.IconID.PLUS); @@ -32,7 +35,7 @@ public CollapsiblePanel(String label, JComponent content, boolean isOpen) { content.setVisible(isOpen); this.add(content, BorderLayout.CENTER); - JPanel header = new JPanel(); + header = new JPanel(); header.setLayout(new BorderLayout()); header.setBackground(HEADER_BG); @@ -44,6 +47,7 @@ public CollapsiblePanel(String label, JComponent content, boolean isOpen) { collapseButton.addActionListener(e -> { collapseButton.setIcon(content.isVisible() ? closeIcon : openIcon); content.setVisible(!content.isVisible()); + this.getParent().revalidate(); }); header.add(collapseButton, BorderLayout.WEST); @@ -56,6 +60,35 @@ public CollapsiblePanel(String label, JComponent content, boolean isOpen) { } + public void collapse() { + collapseButton.setIcon(closeIcon); + content.setVisible(false); + } + + public void expand() { + collapseButton.setIcon(openIcon); + content.setVisible(true); + } + + @Override + public Dimension getPreferredSize() { + Dimension d = super.getPreferredSize(); + if(!content.isVisible()) { + return new Dimension(d.width, header.getHeight()); + } else { + return d; + } + } + + @Override + public Dimension getMaximumSize() { + Dimension d = super.getMaximumSize(); + if(!content.isVisible()) { + return new Dimension(d.width, header.getHeight()); + } else { + return d; + } + } public static void main(String[] args) { diff --git a/src/test/java/org/broad/igv/ucsc/HubTest.java b/src/test/java/org/broad/igv/ucsc/hub/HubTest.java similarity index 94% rename from src/test/java/org/broad/igv/ucsc/HubTest.java rename to src/test/java/org/broad/igv/ucsc/hub/HubTest.java index c2960b8ef8..be9c5d3870 100644 --- a/src/test/java/org/broad/igv/ucsc/HubTest.java +++ b/src/test/java/org/broad/igv/ucsc/hub/HubTest.java @@ -1,4 +1,4 @@ -package org.broad.igv.ucsc; +package org.broad.igv.ucsc.hub; import org.broad.igv.feature.genome.load.GenomeConfig; import org.broad.igv.util.TestUtils; @@ -16,7 +16,7 @@ public void testGetGenomeConfig() throws IOException { String hubFile = TestUtils.DATA_DIR + "hubs/hub.txt"; Hub hub = Hub.loadHub(hubFile); - assertNotNull(hub.hub); + assertNotNull(hub.hubStanza); assertNotNull(hub.genomeStanza); assertEquals(22, hub.trackStanzas.size()); @@ -37,5 +37,4 @@ public void testGetGroupedTrackConfigurations() throws IOException { List groupedTrackConfigurations = hub.getGroupedTrackConfigurations(); assertEquals(5, groupedTrackConfigurations.size()); } - -} \ No newline at end of file + } \ No newline at end of file From 991f01e7c300c1e475fc0810a53b3bf44385075c Mon Sep 17 00:00:00 2001 From: Jim Robinson <933148+jrobinso@users.noreply.github.com> Date: Tue, 4 Feb 2025 09:52:50 -0800 Subject: [PATCH 105/130] Additional track hub support: (#1651) * Additional track hub support: * Fix bugs preventing loading of CHM13 / hs1 hub. Fixes #1643 * Enable addition of hubs as a track source to any IGV genome * UI improvements to track selection dialog --- src/main/java/module-info.java | 1 + .../org/broad/igv/feature/genome/Genome.java | 33 +- .../igv/feature/genome/GenomeManager.java | 1 - .../igv/feature/genome/load/GenomeConfig.java | 12 +- .../feature/genome/load/HubGenomeLoader.java | 18 +- .../igv/feature/genome/load/TrackConfig.java | 31 +- .../org/broad/igv/track/FeatureTrack.java | 4 +- src/main/java/org/broad/igv/ucsc/Hub.java | 498 ------------------ .../igv/ucsc/HubTrackSelectionDialog.java | 396 -------------- .../org/broad/igv/ucsc/TrackConfigGroup.java | 16 - .../panel => ucsc/hub}/CollapsiblePanel.java | 42 +- src/main/java/org/broad/igv/ucsc/hub/Hub.java | 189 +++++++ .../org/broad/igv/ucsc/hub/HubParser.java | 202 +++++++ .../java/org/broad/igv/ucsc/hub/Stanza.java | 82 +++ .../broad/igv/ucsc/hub/TrackConfigGroup.java | 27 + .../org/broad/igv/ucsc/hub/TrackDbHub.java | 235 +++++++++ .../igv/ucsc/hub/TrackHubSelectionDialog.java | 270 ++++++++++ .../org/broad/igv/ucsc/hub/WrapLayout.java | 204 +++++++ .../java/org/broad/igv/ui/IGVMenuBar.java | 149 +++--- .../igv/ui/action/LoadFromURLMenuAction.java | 3 +- .../SelectGenomeAnnotationTracksAction.java | 123 ----- .../igv/ui/action/SelectHubTracksAction.java | 137 +++++ .../org/broad/igv/ui/util/IconFactory.java | 5 +- ...aderTest.java => GenomeHubLoaderTest.java} | 2 +- .../org/broad/igv/ucsc/{ => hub}/HubTest.java | 22 +- 25 files changed, 1538 insertions(+), 1164 deletions(-) delete mode 100644 src/main/java/org/broad/igv/ucsc/Hub.java delete mode 100644 src/main/java/org/broad/igv/ucsc/HubTrackSelectionDialog.java delete mode 100644 src/main/java/org/broad/igv/ucsc/TrackConfigGroup.java rename src/main/java/org/broad/igv/{ui/panel => ucsc/hub}/CollapsiblePanel.java (64%) create mode 100644 src/main/java/org/broad/igv/ucsc/hub/Hub.java create mode 100644 src/main/java/org/broad/igv/ucsc/hub/HubParser.java create mode 100644 src/main/java/org/broad/igv/ucsc/hub/Stanza.java create mode 100644 src/main/java/org/broad/igv/ucsc/hub/TrackConfigGroup.java create mode 100644 src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java create mode 100644 src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java create mode 100644 src/main/java/org/broad/igv/ucsc/hub/WrapLayout.java delete mode 100644 src/main/java/org/broad/igv/ui/action/SelectGenomeAnnotationTracksAction.java create mode 100644 src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java rename src/test/java/org/broad/igv/feature/genome/load/{HubGenomeLoaderTest.java => GenomeHubLoaderTest.java} (92%) rename src/test/java/org/broad/igv/ucsc/{ => hub}/HubTest.java (57%) diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 20ea2ca08f..075e7b3e75 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -12,6 +12,7 @@ opens org.broad.igv.feature.genome.load to com.google.gson; opens org.broad.igv.feature to com.google.gson; exports org.broad.igv.ucsc; + exports org.broad.igv.ucsc.hub; requires com.google.common; requires commons.math3; diff --git a/src/main/java/org/broad/igv/feature/genome/Genome.java b/src/main/java/org/broad/igv/feature/genome/Genome.java index 2d5772d8bb..1da7e3d34d 100644 --- a/src/main/java/org/broad/igv/feature/genome/Genome.java +++ b/src/main/java/org/broad/igv/feature/genome/Genome.java @@ -53,7 +53,8 @@ import org.broad.igv.logging.Logger; import org.broad.igv.track.FeatureTrack; import org.broad.igv.track.TribbleFeatureSource; -import org.broad.igv.ucsc.Hub; +import org.broad.igv.ucsc.hub.Hub; +import org.broad.igv.ucsc.hub.HubParser; import org.broad.igv.ucsc.twobit.TwoBitSequence; import org.broad.igv.util.ResourceLocator; import org.broad.igv.util.liftover.Liftover; @@ -96,7 +97,8 @@ public class Genome { private String homeChromosome; private String defaultPos; private String nameSet; - private Hub hub; + private Hub genomeHub; + private List trackHubs; public Genome(GenomeConfig config) throws IOException { @@ -104,6 +106,7 @@ public Genome(GenomeConfig config) throws IOException { displayName = config.getName(); nameSet = config.getNameSet(); blatDB = config.getBlatDB(); + trackHubs = new ArrayList<>(); if (config.getUcsdID() == null) { ucscID = ucsdIDMap.containsKey(id) ? ucsdIDMap.get(id) : id; } else { @@ -226,6 +229,16 @@ public Genome(GenomeConfig config) throws IOException { // TODO -- no place to go } + if(config.getHubs() != null) { + for(String hubUrl : config.getHubs()) { + try { + trackHubs.add(HubParser.loadHub(hubUrl, getId())); + } catch (IOException e) { + log.error("Error loading hub", e); + } + } + } + addTracks(config); @@ -254,6 +267,8 @@ public Genome(String id, List chromosomes) { this.longChromosomeNames = computeLongChromosomeNames(); this.homeChromosome = this.longChromosomeNames.size() > 1 ? Globals.CHR_ALL : chromosomeNames.get(0); this.chromAliasSource = (new ChromAliasDefaults(id, chromosomeNames)); + + this.trackHubs = new ArrayList<>(); } private void addTracks(GenomeConfig config) { @@ -794,12 +809,18 @@ public List computeLongChromosomeNames() { } - public Hub getHub() { - return hub; + public Hub getGenomeHub() { + return genomeHub; + } + + public void setGenomeHub(Hub genomeHub) { + this.genomeHub = genomeHub; + // A genome hub is by definition also a track hub + this.trackHubs.add(genomeHub); } - public void setHub(Hub hub) { - this.hub = hub; + public List getTrackHubs() { + return trackHubs; } public synchronized static Genome nullGenome() { diff --git a/src/main/java/org/broad/igv/feature/genome/GenomeManager.java b/src/main/java/org/broad/igv/feature/genome/GenomeManager.java index abc2ea6f67..420cabc9ea 100644 --- a/src/main/java/org/broad/igv/feature/genome/GenomeManager.java +++ b/src/main/java/org/broad/igv/feature/genome/GenomeManager.java @@ -142,7 +142,6 @@ public boolean loadGenomeById(String genomeId) throws IOException { return loadGenome(genomePath) != null; // monitor[0]); } - /** * The main load method -- loads a genome from a file or url path. Note this is a long running operation and * should not be done on the Swing event thread as it will block the UI. diff --git a/src/main/java/org/broad/igv/feature/genome/load/GenomeConfig.java b/src/main/java/org/broad/igv/feature/genome/load/GenomeConfig.java index 5fd57257ec..4b97b3f8c0 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/GenomeConfig.java +++ b/src/main/java/org/broad/igv/feature/genome/load/GenomeConfig.java @@ -54,7 +54,7 @@ public class GenomeConfig implements Cloneable { private List tracks; - private List annotations; // Backward compatibility, synonym for tracks + private List hubs; // The properties below support the legacy ".genome" file, which directly loads resources from a zip archive. @@ -72,6 +72,14 @@ public static GenomeConfig fromJson(String json) { public GenomeConfig() { } + public List getHubs() { + return hubs; + } + + public void setHubs(List hubs) { + this.hubs = hubs; + } + public String getId() { return id; } @@ -257,7 +265,7 @@ public void setChromSizesURL(String chromSizesURL) { } public List getTrackConfigs() { - return tracks != null ? tracks : annotations; + return tracks; } public void setTracks(List tracks) { diff --git a/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java b/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java index 3a4fd54844..3f3f82e1f7 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java +++ b/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java @@ -3,10 +3,11 @@ import org.broad.igv.Globals; import org.broad.igv.feature.genome.Genome; import org.broad.igv.prefs.PreferencesManager; -import org.broad.igv.ucsc.Hub; -import org.broad.igv.ucsc.TrackConfigGroup; +import org.broad.igv.ucsc.hub.Hub; +import org.broad.igv.ucsc.hub.HubParser; +import org.broad.igv.ucsc.hub.TrackConfigGroup; import org.broad.igv.ui.IGV; -import org.broad.igv.ucsc.HubTrackSelectionDialog; +import org.broad.igv.ucsc.hub.TrackHubSelectionDialog; import java.io.IOException; import java.util.*; @@ -44,9 +45,9 @@ public static String convertToHubURL(String accension) { @Override public Genome loadGenome() throws IOException { - Hub hub = Hub.loadHub(this.hubURL); + Hub hub = HubParser.loadAssemblyHub(this.hubURL); - GenomeConfig config = hub.getGenomeConfig(false); + GenomeConfig config = hub.getGenomeConfig(); // Potentially override default tracks from hub with user selections @@ -60,7 +61,6 @@ public Genome loadGenome() throws IOException { List selectedTracks = groupedTrackConfigurations.stream() .flatMap(group -> group.tracks.stream()) .filter(trackConfig -> selectedTrackNames.contains(trackConfig.getName())) - .sorted(Comparator.comparingInt(TrackConfig::getOrder)) .collect(Collectors.toList()); config.setTracks(selectedTracks); } @@ -69,7 +69,7 @@ public Genome loadGenome() throws IOException { else if (IGV.hasInstance() && !Globals.isBatch() && !Globals.isHeadless() && !Globals.isTesting()) { int count = 0; - for(TrackConfigGroup g : groupedTrackConfigurations) { + for (TrackConfigGroup g : groupedTrackConfigurations) { count += g.tracks.size(); } @@ -79,7 +79,7 @@ else if (IGV.hasInstance() && !Globals.isBatch() && !Globals.isHeadless() && !Gl groupedTrackConfigurations.stream().filter(g -> g.label.startsWith("Gene")).collect(Collectors.toList()); - HubTrackSelectionDialog dlg = new HubTrackSelectionDialog(filteredGroups, IGV.getInstance().getMainFrame()); + TrackHubSelectionDialog dlg = new TrackHubSelectionDialog(hub, filteredGroups, IGV.getInstance().getMainFrame()); dlg.setVisible(true); List selectedTracks = dlg.getSelectedConfigs(); @@ -91,7 +91,7 @@ else if (IGV.hasInstance() && !Globals.isBatch() && !Globals.isHeadless() && !Gl } Genome genome = new Genome(config); - genome.setHub(hub); + genome.setGenomeHub(hub); return genome; diff --git a/src/main/java/org/broad/igv/feature/genome/load/TrackConfig.java b/src/main/java/org/broad/igv/feature/genome/load/TrackConfig.java index 5cab6af46a..e1853cdf63 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/TrackConfig.java +++ b/src/main/java/org/broad/igv/feature/genome/load/TrackConfig.java @@ -2,13 +2,13 @@ /** * A static json-like object, emulates javascript equivalent. Created to ease port of session code from javascript. - * */ public class TrackConfig implements Cloneable { private String id; private String name; + private String longLabel; private String url; private String indexURL; private String trixURL; @@ -26,20 +26,21 @@ public class TrackConfig implements Cloneable { private Boolean visible; private String infoURL; private String searchIndex; - private String group; - private Integer order; private Integer visibilityWindow; private Boolean indexed; private Boolean hidden; private String html; private String panelName; + private String stanzaParent; // For supporting track hubs + public TrackConfig() { } /** * The only required property of a track configuration is a URL (which can be an actual URL or a static file path) + * * @param url */ public TrackConfig(String url) { @@ -62,6 +63,14 @@ public void setName(String name) { this.name = name; } + public String getLongLabel() { + return longLabel; + } + + public void setLongLabel(String longLabel) { + this.longLabel = longLabel; + } + public String getUrl() { return url; } @@ -206,14 +215,6 @@ public void setGroup(String group) { this.group = group; } - public Integer getOrder() { - return order; - } - - public void setOrder(Integer order) { - this.order = order; - } - public Integer getVisibilityWindow() { return visibilityWindow; } @@ -254,6 +255,14 @@ public void setPanelName(String panelName) { this.panelName = panelName; } + public String getStanzaParent() { + return stanzaParent; + } + + public void setStanzaParent(String stanzaParent) { + this.stanzaParent = stanzaParent; + } + @Override protected TrackConfig clone() throws CloneNotSupportedException { return (TrackConfig) super.clone(); diff --git a/src/main/java/org/broad/igv/track/FeatureTrack.java b/src/main/java/org/broad/igv/track/FeatureTrack.java index 056c33f3b1..b402fc6059 100644 --- a/src/main/java/org/broad/igv/track/FeatureTrack.java +++ b/src/main/java/org/broad/igv/track/FeatureTrack.java @@ -389,7 +389,7 @@ public String getValueStringAt(String chr, double position, int mouseX, int mous StringBuffer buf = new StringBuffer(); boolean firstFeature = true; - int maxNumber = IGV.getInstance().isShowDetailsOnClick() ? 100 : 10; + int maxNumber = 10; int n = 1; for (Feature feature : allFeatures) { if (feature != null && feature instanceof IGVFeature) { @@ -409,7 +409,7 @@ public String getValueStringAt(String chr, double position, int mouseX, int mous } firstFeature = false; if (n > maxNumber) { - buf.append("


>+ " + (allFeatures.size() - maxNumber) + " more ...
"); + buf.append("
+ " + (allFeatures.size() - maxNumber) + " more"); break; } } diff --git a/src/main/java/org/broad/igv/ucsc/Hub.java b/src/main/java/org/broad/igv/ucsc/Hub.java deleted file mode 100644 index fdbc99024d..0000000000 --- a/src/main/java/org/broad/igv/ucsc/Hub.java +++ /dev/null @@ -1,498 +0,0 @@ -package org.broad.igv.ucsc; - -import org.broad.igv.Globals; -import org.broad.igv.feature.genome.load.GenomeConfig; -import org.broad.igv.feature.genome.load.TrackConfig; -import org.broad.igv.logging.LogManager; -import org.broad.igv.logging.Logger; -import org.broad.igv.ui.IGV; -import org.broad.igv.util.ParsingUtils; - -import java.io.BufferedReader; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.*; -import java.util.function.Function; - -public class Hub { - - private static Logger log = LogManager.getLogger(Hub.class); - private final String url; - private final String host; - String baseURL; - Stanza hub; - Stanza genomeStanza; - List trackStanzas; - List groupStanzas; - Map groupPriorityMap; - - static Set supportedTypes = new HashSet(Arrays.asList("bigBed", "bigWig", "bigGenePred", "vcfTabix")); - static Set filterTracks = new HashSet(Arrays.asList("cytoBandIdeo", "assembly", "gap", "gapOverlap", "allGaps", - "cpgIslandExtUnmasked", "windowMasker")); - - public static Hub loadHub(String url) throws IOException { - - return new Hub(url); - } - - private Hub(String url) throws IOException { - - this.url = url; - - int idx = url.lastIndexOf("/"); - String baseURL = url.substring(0, idx + 1); - this.baseURL = baseURL; - - if(url.startsWith("https://") || url.startsWith("http://")) { - try { - URL tmp = new URL(url); - this.host = tmp.getProtocol() + "://" + tmp.getHost(); - } catch (MalformedURLException e) { - // This should never happen - log.error("Error parsing base URL host", e); - throw new RuntimeException(e); - } - } - else { - // Local file, no host - this.host = ""; - } - - List stanzas = loadStanzas(url); - - // Validation checks - if (stanzas.size() < 2) { - throw new RuntimeException("Expected at least 2 stanzas, hub and genome"); - } - if (!"hub".equals(stanzas.get(0).type)) { - throw new RuntimeException("Unexpected hub.txt file -- does the first line start with 'hub'?"); - } - if (!"on".equals(stanzas.get(0).getProperty("useOneFile"))) { - throw new RuntimeException("Only 'useOneFile' hubs are currently supported"); - } - if (!"genome".equals(stanzas.get(1).type)) { - throw new RuntimeException("Unexpected hub file -- expected 'genome' stanza but found " + stanzas.get(1).type); - } - - this.hub = stanzas.get(0); - - // Load groups - this. genomeStanza = stanzas.get(1); - if (genomeStanza.hasProperty("groups")) { - String groupsTxtURL = getDataURL(genomeStanza.getProperty("groups")); - this.groupStanzas = loadStanzas(groupsTxtURL); - } - - // load includes. Nested includes (includes within includes) are not supported - List includes = stanzas.stream().filter(s -> "include".equals(s.type)).toList(); - for (Stanza s : includes) { - List includeStanzas = loadStanzas(getDataURL(s.getProperty("include"))); - for (Stanza inc : includeStanzas) { - inc.setProperty("visibility", "hide"); - stanzas.add(inc); - } - } - - - // The first stanza must be type = hub - // The second stanza should be a genome - // Remaining stanzas should be tracks - this.trackStanzas = new ArrayList<>(); - for (int i = 2; i < stanzas.size(); i++) { - if ("track".equals(stanzas.get(i).type)) { - this.trackStanzas.add(stanzas.get(i)); - } - } - - if (groupStanzas != null) { - this.groupPriorityMap = new HashMap<>(); - for (Stanza g : groupStanzas) { - if (g.hasProperty("priority")) { - this.groupPriorityMap.put(g.getProperty("name"), getPriority(g)); - } - } - } - } - - /** - * Return the priority for the group. The priority format is uncertain, but extends to at least 2 levels (e.g. 3.4). - * Ignore levels > 2 - * - * @param g the group stanza - * @return A priority as an integer - */ - private static int getPriority(Stanza g) { - String priorityString = g.getProperty("priority"); - String[] tokens = priorityString.split("\\."); - int p = Integer.parseInt(tokens[0]) * 10; - if (tokens.length > 1) { - p += Integer.parseInt(tokens[1]); - } - return p; - } - - private static List loadStanzas(String url) throws IOException { - List nodes = new ArrayList<>(); - Stanza currentNode = null; - boolean startNewNode = true; - int order = 0; - try (BufferedReader br = ParsingUtils.openBufferedReader(url)) { - String line; - while ((line = br.readLine()) != null) { - - int indent = indentLevel(line); - int i = line.indexOf(" ", indent); - if (i < 0) { - // Break - start a new node - startNewNode = true; - } else { - String key = line.substring(indent, i); - if (key.startsWith("#")) continue; - String value = line.substring(i + 1); - if (startNewNode) { - // Start a new node -- indent is currently ignored as igv.js does not support sub-tracks, - // so track stanzas are flattened - Stanza newNode = new Stanza(++order, key, value); - nodes.add(newNode); - currentNode = newNode; - startNewNode = false; - } - currentNode.setProperty(key, value); - } - } - } - return resolveParents(nodes); - - } - - private static int indentLevel(String str) { - int level; - for (level = 0; level < str.length(); level++) { - char c = str.charAt(level); - if (c != ' ' && c != '\t') break; - } - return level; - } - - private static List resolveParents(List nodes) { - Map nodeMap = new HashMap<>(); - for (Stanza n : nodes) { - nodeMap.put(n.name, n); - } - for (Stanza n : nodes) { - if (n.properties.containsKey("parent")) { - String parentName = firstWord(n.properties.get("parent")); - n.parent = nodeMap.get(parentName); - } - } - return nodes; - } - - public GenomeConfig getGenomeConfig(boolean includeTracks) { - // TODO -- add blat? htmlPath? - - GenomeConfig config = new GenomeConfig(); - config.setId(this.genomeStanza.getProperty("genome")); - if (this.genomeStanza.hasProperty("scientificName")) { - config.setName(this.genomeStanza.getProperty("scientificName")); - } else if (this.genomeStanza.hasProperty("organism")) { - config.setName(this.genomeStanza.getProperty("organism")); - } else if (this.genomeStanza.hasProperty("description")) { - config.setName(this.genomeStanza.getProperty("description")); - } - if (config.getName() == null) { - config.setName(config.getId()); - } else { - config.setName(config.getName() + " (" + config.getId() + ")"); - } - - config.setTwoBitURL(getDataURL(this.genomeStanza.getProperty("twoBitPath"))); - config.setNameSet("ucsc"); - config.setWholeGenomeView(false); - - if (this.genomeStanza.hasProperty("defaultPos")) { - config.setDefaultPos(this.genomeStanza.getProperty("defaultPos")); - } - - config.setDescription(config.getId()); - - if (this.genomeStanza.hasProperty("blat")) { - config.setBlat(getDataURL(this.genomeStanza.getProperty("blat"))); - } - if (this.genomeStanza.hasProperty("chromAliasBb")) { - config.setChromAliasBbURL(getDataURL(this.genomeStanza.getProperty("chromAliasBb"))); - } - if (this.genomeStanza.hasProperty("twoBitBptURL")) { - config.setTwoBitBptURL(getDataURL(this.genomeStanza.getProperty("twoBitBptURL"))); - } - - if (this.genomeStanza.hasProperty("twoBitBptUrl")) { - config.setTwoBitBptURL(getDataURL(this.genomeStanza.getProperty("twoBitBptUrl"))); - } - - // chromSizes can take a very long time to load, and is not useful with the default WGV = off - // if (this.genomeStanza.hasProperty("chromSizes")) { - // config.chromSizes = this.baseURL + this.genomeStanza.getProperty("chromSizes") - // } - - if (this.genomeStanza.hasProperty("description")) { - config.setDescription(config.getDescription() + "\n" + this.genomeStanza.getProperty("description")); - } - if (this.genomeStanza.hasProperty("organism")) { - config.setDescription(config.getDescription() + "\n" + this.genomeStanza.getProperty("organism")); - } - if (this.genomeStanza.hasProperty("scientificName")) { - config.setDescription(config.getDescription() + "\n" + this.genomeStanza.getProperty("scientificName")); - } - - if (this.genomeStanza.hasProperty("htmlPath")) { - config.setInfoURL(getDataURL(this.genomeStanza.getProperty("htmlPath"))); - } - - // Search for cytoband - /* - track cytoBandIdeo - shortLabel Chromosome Band (Ideogram) - longLabel Ideogram for Orientation - group map - visibility dense - type bigBed 4 + - bigDataUrl bbi/GCA_004027145.1_DauMad_v1_BIUU.cytoBand.bb - */ - for (Stanza t : this.trackStanzas) { - if ("cytoBandIdeo".equals(t.name) && t.hasProperty("bigDataUrl")) { - config.setCytobandBbURL(getDataURL(t.getProperty("bigDataUrl"))); - break; - } - } - - // Tracks. To prevent loading tracks set `includeTrackGroups`to false or "none" - if (includeTracks) { - Function filter = (t) -> !Hub.filterTracks.contains(t.name) && - (!"hide".equals(t.getProperty("visibility"))); - config.setTracks(this.getTracksConfigs(filter)); - } - - // config.trackConfigurations = this.#getGroupedTrackConfigurations() - - return config; - } - - private String getDataURL(String relativeURL) { - return relativeURL.startsWith("/") ? this.host + relativeURL : this.baseURL + relativeURL; - } - - public List getGroupedTrackConfigurations() { - - // Organize track configs by group - LinkedHashMap> trackConfigMap = new LinkedHashMap<>(); - java.util.function.Function filter = (stanza -> !stanza.name.equals("cytoBandIdeo")); - for (TrackConfig c : this.getTracksConfigs(filter)) { - String groupName = c.getGroup() != null ? c.getGroup() : "other"; - trackConfigMap.computeIfAbsent(groupName, k -> new ArrayList<>()).add(c); - } - - // Extract map of group names - Map groupNamesMap = new HashMap<>(); - if (this.groupStanzas != null) { - for (Stanza groupStanza : this.groupStanzas) { - groupNamesMap.put(groupStanza.getProperty("name"), groupStanza.getProperty("label")); - } - } - - // Use linked has map to maintain order - List groupedTrackConfigurations = new ArrayList<>(); - for (Map.Entry> entry : trackConfigMap.entrySet()) { - String group = entry.getKey(); - String label = groupNamesMap.containsKey(group) ? groupNamesMap.get(group) : group; - groupedTrackConfigurations.add(new TrackConfigGroup(label, entry.getValue())); - - } - return groupedTrackConfigurations; - } - - /** - * Return an array of igv track config objects that satisfy the filter - */ - List getTracksConfigs(java.util.function.Function filter) { - return this.trackStanzas.stream().filter(t -> { - return supportedTypes.contains(t.format()) && t.hasProperty("bigDataUrl") && (filter == null || filter.apply(t)); - }) - .map(t -> this.getTrackConfig(t)) - .toList(); - } - - TrackConfig getTrackConfig(Stanza t) { - - String format = t.format(); - String url = getDataURL(t.getProperty("bigDataUrl")); - TrackConfig config = new TrackConfig(url); - - config.setPanelName(IGV.DATA_PANEL_NAME); - - config.setId(t.getProperty("track")); - config.setName(t.getProperty("shortLabel")); - - // TODO -- work on recognizing big* formats - // config.format = t.format(); - - config.setUrl(getDataURL(t.getProperty("bigDataUrl"))); - - // Expanded display mode does not work well in IGV desktop for some tracks - //config.displayMode = t.displayMode(); - - if ("vcfTabix".equals(format)) { - config.setIndexURL(config.getUrl() + ".tbi"); - } - - if (t.hasProperty("longLabel") && t.hasProperty("html")) { - config.setDescription("" + t.getProperty("longLabel") + "")); - } else if (t.hasProperty("longLabel")) { - config.setDescription(t.getProperty("longLabel")); - } - - if (t.hasProperty("html")) { - config.setHtml(getDataURL(t.getProperty("html"))); - } - - String visibility = t.getProperty("visibility"); - config.setVisible(visibility != null && !("hide".equals(visibility))); - - if (t.hasProperty("autoScale")) { - config.setAutoscale(t.getProperty("autoScale").toLowerCase().equals("on")); - } - if (t.hasProperty("maxHeightPixels")) { - String[] tokens = t.getProperty("maxHeightPixels").split(":"); - config.setMaxHeight(Integer.parseInt(tokens[0])); - config.setHeight(Integer.parseInt(tokens[1])); - config.setMinHeight(Integer.parseInt(tokens[2])); - } - // TODO -- graphTypeDefault - // TODO -- windowingFunction - if (t.hasProperty("color")) { - String c = t.getProperty("color"); - config.setColor(c.indexOf(",") > 0 ? "rgb(" + c + ")" : c); - } - if (t.hasProperty("altColor")) { - String c = t.getProperty("altColor"); - config.setAltColor(c.indexOf(",") > 0 ? "rgb(" + c + ")" : c); - } - if (t.hasProperty("viewLimits")) { - String[] tokens = t.getProperty("viewLimits").split(":"); - config.setMin(Float.parseFloat(tokens[0])); - if (tokens.length > 1) { - config.setMax(Float.parseFloat(tokens[1])); - } - } - if (t.hasProperty("itemRgb")) { - // TODO -- this not supported yet - } - if ("hide".equals(t.getProperty("visibility"))) { - config.setVisible(false); - } - if (t.hasProperty("url")) { - config.setInfoURL(t.getProperty("url")); - } - if (t.hasProperty("searchIndex")) { - config.setSearchIndex(t.getProperty("searchIndex")); - } - if (t.hasProperty("searchTrix")) { - config.setTrixURL(getDataURL(t.getProperty("searchTrix"))); - } - - if (t.hasProperty("group")) { - config.setGroup(t.getProperty("group")); - if (this.groupPriorityMap != null && this.groupPriorityMap.containsKey(config.getGroup())) { - int nextPriority = this.groupPriorityMap.get(config.getGroup()) + 1; - config.setOrder(nextPriority); - this.groupPriorityMap.put(config.getGroup(), nextPriority); - } - } - - return config; - } - - public String getUrl() { - return url; - } - - - static class Stanza { - - private final String type; - private final String name; - private final int order; - - private Map properties; - - Stanza parent; - - Stanza(int order, String type, String name) { - this.order = order; - this.type = type; - this.name = name; - this.properties = new HashMap<>(); - } - - void setProperty(String key, String value) { - this.properties.put(key, value); - } - - String getProperty(String key) { - if (this.properties.containsKey(key)) { - return this.properties.get(key); - } else if (this.parent != null) { - return this.parent.getProperty(key); - } else { - return null; - } - } - - boolean hasProperty(String key) { - if (this.properties.containsKey(key)) { - return true; - } else if (this.parent != null) { - return this.parent.hasProperty(key); - } else { - return false; - } - } - - String format() { - String type = this.getProperty("type"); - if (type != null) { - // Trim extra bed qualifiers (e.g. bigBed + 4) - return firstWord(type); - } - return null; // unknown type - } - - /** - * IGV display mode - */ - String displayMode() { - String viz = this.getProperty("visibility"); - if (viz == null) { - return "COLLAPSED"; - } else { - viz = viz.toLowerCase(); - switch (viz) { - case "dense": - return "COLLAPSED"; - case "pack": - return "EXPANDED"; - case "squish": - return "SQUISHED"; - default: - return "COLLAPSED"; - } - } - } - } - - static String firstWord(String str) { - return Globals.whitespacePattern.split(str)[0]; - } - -} diff --git a/src/main/java/org/broad/igv/ucsc/HubTrackSelectionDialog.java b/src/main/java/org/broad/igv/ucsc/HubTrackSelectionDialog.java deleted file mode 100644 index 2fe6b1b5c9..0000000000 --- a/src/main/java/org/broad/igv/ucsc/HubTrackSelectionDialog.java +++ /dev/null @@ -1,396 +0,0 @@ -package org.broad.igv.ucsc; - -import org.broad.igv.Globals; -import org.broad.igv.feature.genome.load.TrackConfig; -import org.broad.igv.ui.panel.CollapsiblePanel; -import org.broad.igv.ui.util.HyperlinkFactory; - -import javax.swing.*; -import java.awt.*; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.util.*; -import java.util.List; -import java.util.stream.Collectors; - - -/** - * Dialog to enable selection of tracks defined by track hubs. Modifies the "visible" property of - * supplied track configurations in place. - */ -public class HubTrackSelectionDialog extends JDialog { - - private static final Dimension TRACK_SPACER = new Dimension(10, 5); - private Map configMap; - - public HubTrackSelectionDialog(List groupedTrackConfigurations, Frame owner) { - super(owner); - setModal(true); - init(groupedTrackConfigurations); - setLocationRelativeTo(owner); - } - - - void init(List trackConfigurations) { - - setTitle("Select tracks to load"); - - configMap = new HashMap<>(); - - JPanel mainPanel = new JPanel(); - mainPanel.setLayout(new BorderLayout()); - // JScrollPane scrollPane = new JScrollPane(mainPanel); - // add(scrollPane, BorderLayout.CENTER); - - add(mainPanel); - - JPanel categoryContainer = new JPanel(); - categoryContainer.setLayout(new BoxLayout(categoryContainer, BoxLayout.PAGE_AXIS)); - JScrollPane scrollPane = new JScrollPane(categoryContainer); - mainPanel.add(scrollPane, BorderLayout.CENTER); - - // Panel for select all/none - JPanel checkAllPanel = new JPanel(); - ((FlowLayout) checkAllPanel.getLayout()).setAlignment(FlowLayout.LEFT); - JButton selectAllButton = new JButton("Select All"); - selectAllButton.setFocusPainted(false); - selectAllButton.addActionListener(e -> configMap.keySet().forEach(cb -> cb.setSelected(true))); - checkAllPanel.add(selectAllButton); - JButton selectNoneButton = new JButton("Select None"); - selectNoneButton.addActionListener(e -> configMap.keySet().forEach(cb -> cb.setSelected(false))); - checkAllPanel.add(selectNoneButton); - categoryContainer.add(checkAllPanel); - //mainPanel.add(checkAllPanel, BorderLayout.NORTH); - - - // Loop through track groups - List cpl = new ArrayList<>(); - for (TrackConfigGroup configGroup : trackConfigurations) { - categoryContainer.add(Box.createVerticalStrut(10)); - JPanel categoryPanel = createCategoryPanel(configGroup); - categoryContainer.add(categoryPanel); - cpl.add(categoryPanel); - } - - JPanel buttonPanel = new JPanel(); - ((FlowLayout) buttonPanel.getLayout()).setAlignment(FlowLayout.RIGHT); - - JButton cancelButton = new JButton("Cancel"); - cancelButton.addActionListener(e -> setVisible(false)); - - JButton okButton = new JButton("OK"); - okButton.addActionListener(e -> okAction()); - - if (Globals.IS_MAC) { - buttonPanel.add(cancelButton); - buttonPanel.add(okButton); - } else { - buttonPanel.add(okButton); - buttonPanel.add(cancelButton); - } - - getRootPane().setDefaultButton(okButton); - - mainPanel.add(buttonPanel, BorderLayout.SOUTH); - - // Try to adjust height to "just enough", BoxLayout will potentially leave empty space otherwise. There might - // be a better way to achieve this. - int h = 75; - for (JPanel p : cpl) h += p.getMinimumSize().height; - setSize(new Dimension(800, Math.min(800, h))); - pack(); - revalidate(); - - } - - private void okAction() { - for (Map.Entry entry : configMap.entrySet()) { - final TrackConfig trackConfig = entry.getValue(); - if (entry.getKey().isSelected()) { - trackConfig.setVisible(true); - } else { - trackConfig.setVisible(false); - } - } - setVisible(false); - } - - /** - * Create a JPanel for a particular track category, including a label and checkboxes for contained tracks. - * - * @param configGroup - * @return - */ - private JPanel createCategoryPanel(TrackConfigGroup configGroup) { - -// JPanel container = new JPanel(); -// container.setLayout(new BorderLayout()); -// Border border = BorderFactory.createLineBorder(Color.lightGray);// BorderFactory.createLoweredBevelBorder(); -// container.setBorder(BorderFactory.createTitledBorder(border, configGroup.label)); - - JPanel trackContainer = new JPanel(); - final WrapLayout wrapLayout = new WrapLayout(); - wrapLayout.setAlignment(FlowLayout.LEFT); - trackContainer.setLayout(wrapLayout); - - //container.add(trackContainer); - boolean isSelected = false; - for (TrackConfig trackConfig : configGroup.tracks) { - - JPanel p = new JPanel(); - final JCheckBox checkBox = new JCheckBox(); - configMap.put(checkBox, trackConfig); - checkBox.setSelected(trackConfig.getVisible()); - isSelected = isSelected || trackConfig.getVisible(); - - JLabel label = trackConfig.getHtml() == null ? - new JLabel(trackConfig.getName()) : - HyperlinkFactory.createLink(trackConfig.getName(), trackConfig.getHtml()); - label.setLabelFor(checkBox); - - p.add(checkBox); - p.add(label); - p.add(Box.createRigidArea(TRACK_SPACER)); - trackContainer.add(p); - } - - return new CollapsiblePanel(configGroup.label, trackContainer, isSelected); - // return container; - } - - /** - * Convenience method to extract and return selected track configurations. - * - * @return - */ - public List getSelectedConfigs() { - List selected = configMap.values().stream().filter(trackConfig -> trackConfig.getVisible()).collect(Collectors.toList()); - selected.sort((o1, o2) -> o1.getOrder() - o2.getOrder()); - return selected; - } - - /** - * A FlowLayout extension that wraps components as needed and updates the preferre size accordingly. - * FlowLayout itself does not update the preferred size, so the component width grows without bounds. - *

- * Code courtesy Rob Camick. See https://tips4java.wordpress.com/2008/11/06/wrap-layout/ - *

- * MIT License - *

- * Copyright (c) 2023 Rob Camick - *

- * 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. - */ - static class WrapLayout extends FlowLayout { - private Dimension preferredLayoutSize; - - /** - * Constructs a new WrapLayout with a left - * alignment and a default 5-unit horizontal and vertical gap. - */ - public WrapLayout() { - super(); - } - - /** - * Constructs a new FlowLayout with the specified - * alignment and a default 5-unit horizontal and vertical gap. - * The value of the alignment argument must be one of - * WrapLayout, WrapLayout, - * or WrapLayout. - * - * @param align the alignment value - */ - public WrapLayout(int align) { - super(align); - } - - /** - * Creates a new flow layout manager with the indicated alignment - * and the indicated horizontal and vertical gaps. - *

- * The value of the alignment argument must be one of - * WrapLayout, WrapLayout, - * or WrapLayout. - * - * @param align the alignment value - * @param hgap the horizontal gap between components - * @param vgap the vertical gap between components - */ - public WrapLayout(int align, int hgap, int vgap) { - super(align, hgap, vgap); - } - - /** - * Returns the preferred dimensions for this layout given the - * visible components in the specified target container. - * - * @param target the component which needs to be laid out - * @return the preferred dimensions to lay out the - * subcomponents of the specified container - */ - @Override - public Dimension preferredLayoutSize(Container target) { - return layoutSize(target, true); - } - - /** - * Returns the minimum dimensions needed to layout the visible - * components contained in the specified target container. - * - * @param target the component which needs to be laid out - * @return the minimum dimensions to lay out the - * subcomponents of the specified container - */ - @Override - public Dimension minimumLayoutSize(Container target) { - Dimension minimum = layoutSize(target, false); - minimum.width -= (getHgap() + 1); - return minimum; - } - - /** - * Returns the minimum or preferred dimension needed to layout the target - * container. - * - * @param target target to get layout size for - * @param preferred should preferred size be calculated - * @return the dimension to layout the target container - */ - private Dimension layoutSize(Container target, boolean preferred) { - synchronized (target.getTreeLock()) { - // Each row must fit with the width allocated to the containter. - // When the container width = 0, the preferred width of the container - // has not yet been calculated so lets ask for the maximum. - - int targetWidth = target.getSize().width; - Container container = target; - - while (container.getSize().width == 0 && container.getParent() != null) { - container = container.getParent(); - } - - targetWidth = container.getSize().width; - - if (targetWidth == 0) - targetWidth = Integer.MAX_VALUE; - - int hgap = getHgap(); - int vgap = getVgap(); - Insets insets = target.getInsets(); - int horizontalInsetsAndGap = insets.left + insets.right + (hgap * 2); - int maxWidth = targetWidth - horizontalInsetsAndGap; - - // Fit components into the allowed width - - Dimension dim = new Dimension(0, 0); - int rowWidth = 0; - int rowHeight = 0; - - int nmembers = target.getComponentCount(); - - for (int i = 0; i < nmembers; i++) { - Component m = target.getComponent(i); - - if (m.isVisible()) { - Dimension d = preferred ? m.getPreferredSize() : m.getMinimumSize(); - - // Can't add the component to current row. Start a new row. - - if (rowWidth + d.width > maxWidth) { - addRow(dim, rowWidth, rowHeight); - rowWidth = 0; - rowHeight = 0; - } - - // Add a horizontal gap for all components after the first - - if (rowWidth != 0) { - rowWidth += hgap; - } - - rowWidth += d.width; - rowHeight = Math.max(rowHeight, d.height); - } - } - - addRow(dim, rowWidth, rowHeight); - - dim.width += horizontalInsetsAndGap; - dim.height += insets.top + insets.bottom + vgap * 2; - - // When using a scroll pane or the DecoratedLookAndFeel we need to - // make sure the preferred size is less than the size of the - // target containter so shrinking the container size works - // correctly. Removing the horizontal gap is an easy way to do this. - - Container scrollPane = SwingUtilities.getAncestorOfClass(JScrollPane.class, target); - - if (scrollPane != null && target.isValid()) { - dim.width -= (hgap + 1); - } - - return dim; - } - } - - /* - * A new row has been completed. Use the dimensions of this row - * to update the preferred size for the container. - * - * @param dim update the width and height when appropriate - * @param rowWidth the width of the row to add - * @param rowHeight the height of the row to add - */ - private void addRow(Dimension dim, int rowWidth, int rowHeight) { - dim.width = Math.max(dim.width, rowWidth); - - if (dim.height > 0) { - dim.height += getVgap(); - } - - dim.height += rowHeight; - } - } - - - /** - * main for testing and development - * - * @param args - * @throws InterruptedException - * @throws InvocationTargetException - * @throws IOException - */ - public static void main(String[] args) throws InterruptedException, InvocationTargetException, IOException { - - String hubFile = "https://hgdownload.soe.ucsc.edu/gbdb/hs1/hubs/public/hub.txt"; - Hub hub = Hub.loadHub(hubFile); - List groupedTrackConfigurations = hub.getGroupedTrackConfigurations(); - - final HubTrackSelectionDialog dlf = new HubTrackSelectionDialog(groupedTrackConfigurations, null); - dlf.setSize(800, 500); - dlf.setVisible(true); - - for (TrackConfig config : dlf.getSelectedConfigs()) { - System.out.println(config.getName()); - } - } - -} diff --git a/src/main/java/org/broad/igv/ucsc/TrackConfigGroup.java b/src/main/java/org/broad/igv/ucsc/TrackConfigGroup.java deleted file mode 100644 index ef266db7a0..0000000000 --- a/src/main/java/org/broad/igv/ucsc/TrackConfigGroup.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.broad.igv.ucsc; - -import org.broad.igv.feature.genome.load.TrackConfig; - -import java.util.List; - -public class TrackConfigGroup { - - public String label; - public List tracks; - - public TrackConfigGroup(String label, List tracks) { - this.label = label; - this.tracks = tracks; - } -} diff --git a/src/main/java/org/broad/igv/ui/panel/CollapsiblePanel.java b/src/main/java/org/broad/igv/ucsc/hub/CollapsiblePanel.java similarity index 64% rename from src/main/java/org/broad/igv/ui/panel/CollapsiblePanel.java rename to src/main/java/org/broad/igv/ucsc/hub/CollapsiblePanel.java index 249c979fc6..f4ace5e5c8 100644 --- a/src/main/java/org/broad/igv/ui/panel/CollapsiblePanel.java +++ b/src/main/java/org/broad/igv/ucsc/hub/CollapsiblePanel.java @@ -1,4 +1,4 @@ -package org.broad.igv.ui.panel; +package org.broad.igv.ucsc.hub; import org.broad.igv.ui.FontManager; import org.broad.igv.ui.util.IconFactory; @@ -9,10 +9,12 @@ public class CollapsiblePanel extends JPanel { - public static final Color HEADER_BG = new Color(210, 210, 210); + public static final Color HEADER_BG = new Color(180,204,226); private final JButton collapseButton; + private final JComponent content; + private final JPanel header; private ImageIcon openIcon; private ImageIcon closeIcon; @@ -23,8 +25,7 @@ public CollapsiblePanel(String label, JComponent content) { public CollapsiblePanel(String label, JComponent content, boolean isOpen) { setLayout(new BorderLayout()); - - setBorder(BorderFactory.createLineBorder(Color.BLACK)); + this.content = content; this.openIcon = IconFactory.getInstance().getIcon(IconFactory.IconID.MINUS); this.closeIcon = IconFactory.getInstance().getIcon(IconFactory.IconID.PLUS); @@ -32,7 +33,7 @@ public CollapsiblePanel(String label, JComponent content, boolean isOpen) { content.setVisible(isOpen); this.add(content, BorderLayout.CENTER); - JPanel header = new JPanel(); + header = new JPanel(); header.setLayout(new BorderLayout()); header.setBackground(HEADER_BG); @@ -44,6 +45,7 @@ public CollapsiblePanel(String label, JComponent content, boolean isOpen) { collapseButton.addActionListener(e -> { collapseButton.setIcon(content.isVisible() ? closeIcon : openIcon); content.setVisible(!content.isVisible()); + this.getParent().revalidate(); }); header.add(collapseButton, BorderLayout.WEST); @@ -56,6 +58,34 @@ public CollapsiblePanel(String label, JComponent content, boolean isOpen) { } + public void collapse() { + collapseButton.setIcon(closeIcon); + content.setVisible(false); + } + + public void expand() { + collapseButton.setIcon(openIcon); + content.setVisible(true); + } + + + /** + * Constrain the maximum height to prevent BoxLayout from needlessly resizing the panel to fill space. This is + * rather hardcoded for the TrackHubSelectionDialog. + * + * @return + */ + + @Override + public Dimension getMaximumSize() { + Dimension d4 = header.getMinimumSize(); + if(!content.isVisible()) { + return new Dimension(Integer.MAX_VALUE, d4.height); + } else { + Dimension d5 = content.getMinimumSize(); + return new Dimension(Integer.MAX_VALUE, d4.height + (int) (1.2 * d5.height)); + } + } public static void main(String[] args) { @@ -68,3 +98,5 @@ public static void main(String[] args) { f.setVisible(true); } } + + diff --git a/src/main/java/org/broad/igv/ucsc/hub/Hub.java b/src/main/java/org/broad/igv/ucsc/hub/Hub.java new file mode 100644 index 0000000000..65add34a9e --- /dev/null +++ b/src/main/java/org/broad/igv/ucsc/hub/Hub.java @@ -0,0 +1,189 @@ +package org.broad.igv.ucsc.hub; + +import org.broad.igv.feature.genome.load.GenomeConfig; +import org.broad.igv.logging.LogManager; +import org.broad.igv.logging.Logger; + +import java.io.IOException; +import java.util.*; + + +public class Hub { + + private static Logger log = LogManager.getLogger(Hub.class); + private final String url; + private final String trackDbURL; + Stanza hubStanza; + Stanza genomeStanza; + List trackStanzas; + List groupStanzas; + TrackDbHub trackHub; + + Hub(String url, + String trackDbURL, + Stanza hubStanza, + Stanza genomeStanza, + List trackStanzas, + List groupStanzas) throws IOException { + + this.url = url; + this.trackDbURL = trackDbURL; + + // Validation checks + this.hubStanza = hubStanza; + this.genomeStanza = genomeStanza; + this.trackStanzas = trackStanzas; + + // load includes. Nested includes (includes within includes) are not supported +// List includes = stanzas.stream().filter(s -> "include".equals(s.type)).toList(); +// for (Stanza s : includes) { +// List includeStanzas = HubParser.loadStanzas(getDataURL(s.getProperty("include"))); +// for (Stanza inc : includeStanzas) { +// inc.setProperty("visibility", "hide"); +// stanzas.add(inc); +// } +// } + + if (trackStanzas != null) { + this.trackHub = new TrackDbHub(trackStanzas, groupStanzas); + } + } + + + public String getShortLabel() { + return hubStanza.hasProperty("shortLabel") ? hubStanza.getProperty("shortLabel") : this.url; + } + + public String getLongLabel() { + return hubStanza.hasProperty("longLabel") ? hubStanza.getProperty("longLabel") : this.url; + } + + public String getDescriptionURL() { + return hubStanza.hasProperty("descriptionUrl") ? hubStanza.getProperty("descriptionUrl") : + hubStanza.hasProperty("desriptionUrl") ? hubStanza.getProperty("desriptionUrl") : null; + } + + + /** + * Return the priority for the group. The priority format is uncertain, but extends to at least 2 levels (e.g. 3.4). + * Ignore levels > 3 + * + * @param priorityString Priority as a string (e.g. 3.4) + * @return A priority as an integer + */ + static int getPriority(String priorityString) { + try { + String[] tokens = priorityString.trim().split("\\."); + int p = Integer.parseInt(tokens[0]) * 100; + if (tokens.length > 1) { + p += Integer.parseInt(tokens[1]) * 10; + } + if (tokens.length > 2) { + p += Integer.parseInt(tokens[2]); + } + return p; + } catch (Exception e) { + log.error("Error parsing priority string: " + priorityString, e); + return Integer.MAX_VALUE; + } + } + + public GenomeConfig getGenomeConfig() { + // TODO -- add blat? htmlPath? + + GenomeConfig config = new GenomeConfig(); + config.setId(this.genomeStanza.getProperty("genome")); + if (this.genomeStanza.hasProperty("scientificName")) { + config.setName(this.genomeStanza.getProperty("scientificName")); + } else if (this.genomeStanza.hasProperty("organism")) { + config.setName(this.genomeStanza.getProperty("organism")); + } else if (this.genomeStanza.hasProperty("description")) { + config.setName(this.genomeStanza.getProperty("description")); + } + if (config.getName() == null) { + config.setName(config.getId()); + } else { + config.setName(config.getName() + " (" + config.getId() + ")"); + } + + config.setTwoBitURL(this.genomeStanza.getProperty("twoBitPath")); + config.setNameSet("ucsc"); + config.setWholeGenomeView(false); + + if (this.genomeStanza.hasProperty("defaultPos")) { + config.setDefaultPos(this.genomeStanza.getProperty("defaultPos")); + } + + config.setDescription(config.getId()); + + if (genomeStanza.hasProperty("blat")) { + config.setBlat(genomeStanza.getProperty("blat")); + } + if (genomeStanza.hasProperty("chromAliasBb")) { + config.setChromAliasBbURL(genomeStanza.getProperty("chromAliasBb")); + } + if (genomeStanza.hasProperty("twoBitBptURL")) { + config.setTwoBitBptURL(genomeStanza.getProperty("twoBitBptURL")); + } + if (genomeStanza.hasProperty("twoBitBptUrl")) { + config.setTwoBitBptURL(genomeStanza.getProperty("twoBitBptUrl")); + } + + // chromSizes can take a very long time to load, and is not useful with the default WGV = off + // if (this.genomeStanza.hasProperty("chromSizes")) { + // config.chromSizes = this.baseURL + this.genomeStanza.getProperty("chromSizes") + // } + + if (genomeStanza.hasProperty("description")) { + config.setDescription(config.getDescription() + "\n" + genomeStanza.getProperty("description")); + } + if (genomeStanza.hasProperty("organism")) { + config.setDescription(config.getDescription() + "\n" + genomeStanza.getProperty("organism")); + } + if (genomeStanza.hasProperty("scientificName")) { + config.setDescription(config.getDescription() + "\n" + genomeStanza.getProperty("scientificName")); + } + + if (genomeStanza.hasProperty("htmlPath")) { + config.setInfoURL(genomeStanza.getProperty("htmlPath")); + } + + // Search for cytoband + /* + track cytoBandIdeo + shortLabel Chromosome Band (Ideogram) + longLabel Ideogram for Orientation + group map + visibility dense + type bigBed 4 + + bigDataUrl bbi/GCA_004027145.1_DauMad_v1_BIUU.cytoBand.bb + */ + for (Stanza t : this.trackStanzas) { + if ("cytoBandIdeo".equals(t.name) && t.hasProperty("bigDataUrl")) { + config.setCytobandBbURL(t.getProperty("bigDataUrl")); + break; + } + } + + return config; + } + + public List getGroupedTrackConfigurations() { + if (this.trackHub == null) { + try { + List trackStanzas = HubParser.loadStanzas(this.trackDbURL); + this.trackHub = new TrackDbHub(trackStanzas, this.groupStanzas); + } catch (IOException e) { + throw new RuntimeException("Error loading track configurations: " + e.getMessage(), e); + } + } + return trackHub.getGroupedTrackConfigurations(); + } + + + public String getUrl() { + return url; + } + + +} diff --git a/src/main/java/org/broad/igv/ucsc/hub/HubParser.java b/src/main/java/org/broad/igv/ucsc/hub/HubParser.java new file mode 100644 index 0000000000..7282ee8da7 --- /dev/null +++ b/src/main/java/org/broad/igv/ucsc/hub/HubParser.java @@ -0,0 +1,202 @@ +package org.broad.igv.ucsc.hub; + +import org.broad.igv.Globals; +import org.broad.igv.logging.LogManager; +import org.broad.igv.logging.Logger; +import org.broad.igv.util.ParsingUtils; + +import java.io.BufferedReader; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.*; + +public class HubParser { + + private static Logger log = LogManager.getLogger(HubParser.class); + + private static Set urlProperties = new HashSet<>(Arrays.asList("descriptionUrl","desriptionUrl", + "twoBitPath", "blat", "chromAliasBb", "twoBitBptURL", "twoBitBptUrl", "htmlPath", "bigDataUrl", + "genomesFile","trackDb","groups", "include", "html", "searchTrix")); + + public static Hub loadAssemblyHub(String url) throws IOException { + return loadHub(url, null); + } + + public static Hub loadHub(String url, String genomeId) throws IOException { + + boolean assemblyHub = genomeId == null; + + int idx = url.lastIndexOf("/"); + String baseURL = url.substring(0, idx + 1); + String host = getHost(url); + + // Load stanzas from the hub.txt file, which might be all stanzas if 'useOneFile' is on + List stanzas = HubParser.loadStanzas(url); + + // Validation checks + if (stanzas.size() < 1) { + throw new RuntimeException("Expected at least 1 stanza"); + } + if (!"hub".equals(stanzas.get(0).type)) { + throw new RuntimeException("Unexpected hub.txt file -- does the first line start with 'hub'?"); + } + Stanza hubStanza = stanzas.get(0); + Stanza genomeStanza = null; + List trackStanzas = null; + List groupStanzas = null; + String trackDbURL = null; + + if ("on".equals(stanzas.get(0).getProperty("useOneFile"))) { + + if (!"genome".equals(stanzas.get(1).type)) { + throw new RuntimeException("Unexpected hub file -- expected 'genome' stanza but found " + stanzas.get(1).type); + } + genomeStanza = stanzas.get(1); + trackStanzas = new ArrayList<>(stanzas.subList(2, stanzas.size())); + + } else { + + if (!hubStanza.hasProperty("genomesFile")) { + throw new RuntimeException("hub.txt must specify 'genomesFile'"); + } + + // Load genome stanza(s) + List genomeStanzaList = HubParser.loadStanzas(hubStanza.getProperty("genomesFile")); + + // Find stanza for requested genome, or if no requested genome the first. + for (Stanza s : genomeStanzaList) { + if (genomeId == null || genomeId.equals(s.getProperty("genome"))) { + stanzas.add(s); + genomeStanza = s; + trackDbURL = genomeStanza.getProperty("trackDb"); + break; + } + } + } + + // Assembly hub validation + if(assemblyHub) { + if (!genomeStanza.hasProperty("twoBitPath")) { + throw new RuntimeException("Assembly hubs must specify 'twoBitPath'"); + } + if (!genomeStanza.hasProperty("groups")) { + throw new RuntimeException("Assembly hubs must specify 'groups'"); + } + } + + + if (genomeStanza.hasProperty("groups")) { + if (genomeStanza.hasProperty("groups")) { + String groupsTxtURL = genomeStanza.getProperty("groups"); + groupStanzas = HubParser.loadStanzas(groupsTxtURL); + } + } + + // load includes. Nested includes (includes within includes) are not supported + List includes = stanzas.stream().filter(s -> "include".equals(s.type)).toList(); + for (Stanza s : includes) { + List includeStanzas = HubParser.loadStanzas(s.getProperty("include")); + trackStanzas.addAll(includeStanzas); + } + + return new Hub(url, trackDbURL, hubStanza, genomeStanza, trackStanzas, groupStanzas); + } + + private static String getHost(String url) { + String host; + if (url.startsWith("https://") || url.startsWith("http://")) { + try { + URL tmp = new URL(url); + host = tmp.getProtocol() + "://" + tmp.getHost(); + } catch (MalformedURLException e) { + // This should never happen + log.error("Error parsing base URL host", e); + throw new RuntimeException(e); + } + } else { + // Local file, no host + host = ""; + } + return host; + } + + + static List loadStanzas(String url) throws IOException { + + int idx = url.lastIndexOf("/"); + String baseURL = url.substring(0, idx + 1); + String host = getHost(url); + + List nodes = new ArrayList<>(); + Stanza currentNode = null; + boolean startNewNode = true; + int order = 0; + try (BufferedReader br = ParsingUtils.openBufferedReader(url)) { + String line; + while ((line = br.readLine()) != null) { + + int indent = indentLevel(line); + int i = line.indexOf(" ", indent); + if (i < 0) { + // Break - start a new node + startNewNode = true; + } else { + String key = line.substring(indent, i); + if (key.startsWith("#")) continue; + String value = line.substring(i + 1); + + if(urlProperties.contains(key) || value.endsWith("URL") || value.endsWith("Url")) { + value = getDataURL(value, host, baseURL); + } + + if (startNewNode) { + // Start a new node -- indent is currently ignored as igv.js does not support sub-tracks, + // so track stanzas are flattened + Stanza newNode = new Stanza(key, value); + nodes.add(newNode); + currentNode = newNode; + startNewNode = false; + } + currentNode.setProperty(key, value); + } + } + } + + return resolveParents(nodes); + } + + private static int indentLevel(String str) { + int level; + for (level = 0; level < str.length(); level++) { + char c = str.charAt(level); + if (c != ' ' && c != '\t') break; + } + return level; + } + + private static List resolveParents(List nodes) { + Map nodeMap = new HashMap<>(); + for (Stanza n : nodes) { + nodeMap.put(n.name, n); + } + for (Stanza n : nodes) { + if (n.properties.containsKey("parent")) { + String parentName = firstWord(n.properties.get("parent")); + n.parent = nodeMap.get(parentName); + } + } + return nodes; + } + + + static String firstWord(String str) { + return Globals.whitespacePattern.split(str)[0]; + } + + private static String getDataURL(String url, String host, String baseURL) { + return url.startsWith("http://") || url.startsWith("https://") ? url : + url.startsWith("/") ? host + url : baseURL + url; + } + +} diff --git a/src/main/java/org/broad/igv/ucsc/hub/Stanza.java b/src/main/java/org/broad/igv/ucsc/hub/Stanza.java new file mode 100644 index 0000000000..dcff273bd6 --- /dev/null +++ b/src/main/java/org/broad/igv/ucsc/hub/Stanza.java @@ -0,0 +1,82 @@ +package org.broad.igv.ucsc.hub; + +import org.broad.igv.Globals; + +import java.util.*; + +class Stanza { + + private static Set parentOverrideProperties = new HashSet<>(Arrays.asList("visibility", "priority", "group")); + final String type; + final String name; + Stanza parent; + Map properties; + + + Stanza(String type, String name) { + + this.type = type; + this.name = name; + this.properties = new HashMap<>(); + } + + static String firstWord(String str) { + return Globals.whitespacePattern.split(str)[0]; + } + + public String getType() { + return type; + } + + public String getName() { + return name; + } + + void setProperty(String key, String value) { + this.properties.put(key, value); + } + + String getOwnProperty(String key) { + return this.properties.get(key); + } + + String getProperty(String key) { + + if (parentOverrideProperties.contains(key) && this.parent != null && this.parent.hasProperty(key)) { + return this.parent.getProperty(key); + } else if (this.properties.containsKey(key)) { + return this.properties.get(key); + } else if (this.parent != null) { + return this.parent.getProperty(key); + } else { + return null; + } + } + + boolean hasProperty(String key) { + if (this.properties.containsKey(key)) { + return true; + } else if (this.parent != null) { + return this.parent.hasProperty(key); + } else { + return false; + } + } + + String format() { + String type = this.getProperty("type"); + if (type != null) { + // Trim extra bed qualifiers (e.g. bigBed + 4) + return firstWord(type); + } + return null; // unknown type + } + + public Stanza getParent() { + return parent; + } + + public void setParent(Stanza parent) { + this.parent = parent; + } +} diff --git a/src/main/java/org/broad/igv/ucsc/hub/TrackConfigGroup.java b/src/main/java/org/broad/igv/ucsc/hub/TrackConfigGroup.java new file mode 100644 index 0000000000..72fe9202b8 --- /dev/null +++ b/src/main/java/org/broad/igv/ucsc/hub/TrackConfigGroup.java @@ -0,0 +1,27 @@ +package org.broad.igv.ucsc.hub; + +import org.broad.igv.feature.genome.load.TrackConfig; + +import java.util.ArrayList; +import java.util.List; + +public class TrackConfigGroup { + + public int priority; + public String name; + public String label; + public boolean defaultOpen; + public List tracks; + + public TrackConfigGroup(String name, String label, int priority, boolean defaultOpen) { + this.name = name; + this.priority = priority; + this.label = label; + this.defaultOpen = defaultOpen; + this.tracks = new ArrayList<>(); + } + + public boolean isEmpty() { + return tracks.isEmpty(); + } +} diff --git a/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java b/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java new file mode 100644 index 0000000000..cb86945677 --- /dev/null +++ b/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java @@ -0,0 +1,235 @@ +package org.broad.igv.ucsc.hub; + +import org.broad.igv.feature.genome.load.TrackConfig; +import org.broad.igv.ui.IGV; + +import java.util.*; +import java.util.stream.Collectors; + +import static org.broad.igv.ucsc.hub.Hub.getPriority; + +public class TrackDbHub { + + List trackStanzas; + List groupStanzas; + + static Set supportedTypes = new HashSet(Arrays.asList("bigBed", "bigWig", "bigGenePred", "vcfTabix")); + + static Set filterTracks = new HashSet(Arrays.asList("cytoBandIdeo", "assembly", "gap", "gapOverlap", "allGaps", + "cpgIslandExtUnmasked", "windowMasker")); + + static Map vizModeMap = Map.of( + "pack", "EXPANDED", + "full", "EXPANDED", + "squish", "SQUISHED", + "dense", "COLLAPSED"); + + + public TrackDbHub(List trackStanzas, List groupStanzas) { + + this.groupStanzas = groupStanzas; + this.trackStanzas = trackStanzas; + } + + public List getGroupedTrackConfigurations() { + + // Build map of group objects + Map groupMap = new HashMap<>(); + // Create a group for tracks without a group + groupMap.put("", new TrackConfigGroup("", "", 0, true)); + + if (this.groupStanzas != null) { + for (Stanza groupStanza : this.groupStanzas) { + String name = groupStanza.getProperty("name"); + boolean defaultOpen = "0".equals(groupStanza.getProperty("defaultIsClosed")); + int priority = groupStanza.hasProperty("priority") ? getPriority(groupStanza.getProperty("priority")) : Integer.MAX_VALUE - 1; + groupMap.put(name, new TrackConfigGroup(name, groupStanza.getProperty("label"), priority, defaultOpen)); + } + } + + // Build map of stanzas to resolve parents + Map trackStanzaMap = new HashMap<>(); + for (Stanza s : this.trackStanzas) { + trackStanzaMap.put(s.getProperty("track"), s); + } + + // Initialized cache of track containers. Use linked hashmap to maintain insertion order + LinkedHashMap parentCache = new LinkedHashMap<>(); + + // Tracks + final List trackConfigs = this.getTrackConfigs(stanza -> !filterTracks.contains(stanza.name)); + for (TrackConfig c : trackConfigs) { + String groupName = c.getGroup() != null ? c.getGroup() : ""; + + // Some heads (at least one, the washu epigenomics hub) reference groups that are not defined. + if (!groupMap.containsKey(groupName)) { + groupMap.put(groupName, new TrackConfigGroup(groupName, groupName, Integer.MAX_VALUE, false)); + } + + final TrackConfigGroup trackConfigGroup = groupMap.get(groupName); + int priority = trackConfigGroup.priority; + trackConfigGroup.tracks.add(c); + + String parentName = c.getStanzaParent(); + if (parentName != null && trackStanzaMap.containsKey(parentName)) { + // Create a contingent container, will be used if a sufficient # of tracks belong to this container + TrackConfigGroup container = parentCache.get(parentName); + if (container == null) { + Stanza s = trackStanzaMap.get(parentName); + String label = trackConfigGroup.label.equals("") ? + s.getProperty("shortLabel") : + trackConfigGroup.label + " - " + s.getProperty("shortLabel"); + int p = s.hasProperty("priority") ? getPriority(s.getProperty("priority")) : priority + 1; + container = new TrackConfigGroup(parentName, label, p, false); + parentCache.put(parentName, container); + } + container.tracks.add(c); + } + } + + // Flatten the track groups into a list. Remove empty groups. + List groupedTrackConfigurations = groupMap.values().stream() + .filter(g -> !g.isEmpty()).collect(Collectors.toList()); + + // Promote contingent embedded track groups (e.g. composite tracks) to top level group if # of tracks > threshold + // Member tracks must also be removed from existing top level categories + List tmp = new ArrayList<>(); + Set toRemove = new HashSet<>(); + for (TrackConfigGroup parent : parentCache.values()) { + if (parent.tracks.size() > 5) { + tmp.add(parent); + toRemove.addAll(parent.tracks); + } + } + if (toRemove.size() > 0) { + for (TrackConfigGroup g : groupedTrackConfigurations) { + g.tracks = g.tracks.stream().filter(t -> !toRemove.contains(t)).collect(Collectors.toList()); + } + } + + // Filter empty groups + groupedTrackConfigurations = groupedTrackConfigurations.stream().filter(t -> t.tracks.size() > 0).collect(Collectors.toList()); + groupedTrackConfigurations.addAll(tmp); + Collections.sort(groupedTrackConfigurations, Comparator.comparingInt(o -> o.priority)); + return groupedTrackConfigurations; + } + + /** + * Return an array of igv track config objects that satisfy the filter + */ + List getTrackConfigs(java.util.function.Function filter) { + return this.trackStanzas.stream().filter(t -> { + return supportedTypes.contains(t.format()) && t.hasProperty("bigDataUrl") && (filter == null || filter.apply(t)); + }) + .map(t -> this.getTrackConfig(t)) + .toList(); + } + + + private TrackConfig getTrackConfig(Stanza t) { + + String format = t.format(); + String url = t.getProperty("bigDataUrl"); + TrackConfig config = new TrackConfig(url); + + config.setPanelName(IGV.DATA_PANEL_NAME); + + config.setId(t.getProperty("track")); + config.setName(t.getProperty("shortLabel")); + + // A rather complex workaround for some composite tracks + String longLabel = t.getOwnProperty("longLabel"); + if(longLabel == null) { + String inheritedLongLabel = t.getProperty("longLabel"); + longLabel = inheritedLongLabel.length() > config.getName().length() ? inheritedLongLabel : config.getName(); + } + config.setLongLabel(longLabel); + + // TODO -- work on recognizing big* formats + // config.format = t.format(); + + config.setUrl(t.getProperty("bigDataUrl")); + + // Expanded display mode does not work well in IGV desktop for some tracks + //config.displayMode = t.displayMode(); + + if ("vcfTabix".equals(format)) { + config.setIndexURL(config.getUrl() + ".tbi"); + } + + if (t.hasProperty("longLabel") && t.hasProperty("html")) { + config.setDescription("" + t.getProperty("longLabel") + "")); + } else if (t.hasProperty("longLabel")) { + config.setDescription(t.getProperty("longLabel")); + } + + if (t.hasProperty("html")) { + config.setHtml(t.getProperty("html")); + } + + String vizProperty = t.getProperty("visibility"); + + if (vizProperty != null && vizModeMap.containsKey(vizProperty)) { + config.setDisplayMode(vizModeMap.get(vizProperty)); + } + + boolean visibility = t.hasProperty("compositeTrack") ? + "on".equals(t.getProperty("compositeTrack")) : + !("hide".equals(vizProperty)); + + config.setVisible(visibility); + + if (t.hasProperty("autoScale")) { + config.setAutoscale(t.getProperty("autoScale").toLowerCase().equals("on")); + } + if (t.hasProperty("maxHeightPixels")) { + String[] tokens = t.getProperty("maxHeightPixels").split(":"); + config.setMaxHeight(Integer.parseInt(tokens[0])); + config.setHeight(Integer.parseInt(tokens[1])); + config.setMinHeight(Integer.parseInt(tokens[2])); + } + // TODO -- graphTypeDefault + // TODO -- windowingFunction + if (t.hasProperty("color")) { + String c = t.getProperty("color"); + config.setColor(c.indexOf(",") > 0 ? "rgb(" + c + ")" : c); + } + if (t.hasProperty("altColor")) { + String c = t.getProperty("altColor"); + config.setAltColor(c.indexOf(",") > 0 ? "rgb(" + c + ")" : c); + } + if (t.hasProperty("viewLimits")) { + String[] tokens = t.getProperty("viewLimits").split(":"); + config.setMin(Float.parseFloat(tokens[0])); + if (tokens.length > 1) { + config.setMax(Float.parseFloat(tokens[1])); + } + } + if (t.hasProperty("itemRgb")) { + // TODO -- this not supported yet + } + if ("hide".equals(t.getProperty("visibility"))) { + config.setVisible(false); + } + if (t.hasProperty("url")) { + config.setInfoURL(t.getProperty("url")); + } + if (t.hasProperty("searchIndex")) { + config.setSearchIndex(t.getProperty("searchIndex")); + } + if (t.hasProperty("searchTrix")) { + config.setTrixURL(t.getProperty("searchTrix")); + } + + if (t.hasProperty("group")) { + config.setGroup(t.getProperty("group")); + } + + if (t.parent != null) { + config.setStanzaParent(t.parent.name); + } + + return config; + } + +} diff --git a/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java b/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java new file mode 100644 index 0000000000..4181b1196e --- /dev/null +++ b/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java @@ -0,0 +1,270 @@ +package org.broad.igv.ucsc.hub; + +import org.broad.igv.Globals; +import org.broad.igv.feature.genome.load.TrackConfig; +import org.broad.igv.logging.LogManager; +import org.broad.igv.logging.Logger; +import org.broad.igv.ui.util.HyperlinkFactory; +import org.broad.igv.ui.util.IconFactory; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.net.URI; +import java.util.*; +import java.util.List; + +import java.util.stream.Collectors; + + +/** + * Dialog to enable selection of tracks defined by track hubs. Modifies the "visible" property of + * supplied track configurations in place. + */ +public class TrackHubSelectionDialog extends JDialog { + + private static Logger log = LogManager.getLogger(TrackHubSelectionDialog.class); + + Hub hub; + private Map configMap; + private ArrayList categoryPanels; + + public TrackHubSelectionDialog(Hub hub, List groupedTrackConfigurations, Frame owner) { + super(owner); + setModal(true); + this.hub = hub; + init(groupedTrackConfigurations); + setLocationRelativeTo(owner); + } + + + void init(List trackConfigurations) { + + setTitle(this.hub.getLongLabel()); + + setSize(new Dimension(1000, 800)); + + configMap = new HashMap<>(); + categoryPanels = new ArrayList<>(); + + JPanel mainPanel = new JPanel(); + mainPanel.setLayout(new BorderLayout()); + add(mainPanel); + + JPanel topPanel = new JPanel(); + topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.Y_AXIS)); + topPanel.add(getLabeledHyperlink("Hub URL: ", hub.getUrl())); + String descriptionURL = hub.getDescriptionURL(); + if (descriptionURL != null) { + topPanel.add(getLabeledHyperlink("Description: ", descriptionURL)); + } + + // Panel for select all/none + JPanel topButtonPanel = new JPanel(); + ((FlowLayout) topButtonPanel.getLayout()).setAlignment(FlowLayout.LEFT); + JButton expandAllButton = new JButton("Expand All"); + expandAllButton.addActionListener(e -> { + categoryPanels.forEach(cp -> cp.expand()); + this.revalidate(); + }); + topButtonPanel.add(expandAllButton); + + JButton collapseAllButton = new JButton("Collapse All"); + collapseAllButton.addActionListener(e -> { + categoryPanels.forEach(cp -> cp.collapse()); + this.revalidate(); + }); + topButtonPanel.add(collapseAllButton); + + topPanel.add(topButtonPanel); + mainPanel.add(topPanel, BorderLayout.NORTH); + + // Panel for category boxes + JPanel categoryContainer = new JPanel(); + categoryContainer.setLayout(new BoxLayout(categoryContainer, BoxLayout.Y_AXIS)); + JScrollPane scrollPane = new JScrollPane(categoryContainer); + //scrollPane.setBorder(BorderFactory.createLineBorder(Color.gray)); + mainPanel.add(scrollPane, BorderLayout.CENTER); + + // Loop through track groups + for (TrackConfigGroup configGroup : trackConfigurations) { + categoryContainer.add(Box.createVerticalStrut(10)); + CollapsiblePanel categoryPanel = createCategoryPanel(configGroup); + categoryContainer.add(categoryPanel); + categoryPanels.add(categoryPanel); + } + + // If only a single category expand it + if (categoryPanels.size() == 1) { + categoryPanels.get(0).expand(); + } + + JPanel buttonPanel = new JPanel(); + ((FlowLayout) buttonPanel.getLayout()).setAlignment(FlowLayout.RIGHT); + + JButton cancelButton = new JButton("Cancel"); + cancelButton.addActionListener(e -> setVisible(false)); + + JButton okButton = new JButton("OK"); + okButton.addActionListener(e -> okAction()); + + if (Globals.IS_MAC) { + buttonPanel.add(cancelButton); + buttonPanel.add(okButton); + } else { + buttonPanel.add(okButton); + buttonPanel.add(cancelButton); + } + + getRootPane().setDefaultButton(okButton); + + mainPanel.add(buttonPanel, BorderLayout.SOUTH); + + mainPanel.validate(); + + } + + private JPanel getLabeledHyperlink(String label, String url) { + JPanel hubURLPanel = new JPanel(); + ((FlowLayout) hubURLPanel.getLayout()).setAlignment(FlowLayout.LEFT); + hubURLPanel.setBorder(BorderFactory.createEmptyBorder(2, 6, 0, 0)); + hubURLPanel.add(new JLabel(label)); + hubURLPanel.add(HyperlinkFactory.createLink(url, url)); + return hubURLPanel; + } + + private void okAction() { + for (Map.Entry entry : configMap.entrySet()) { + final TrackConfig trackConfig = entry.getValue(); + if (entry.getKey().isSelected()) { + trackConfig.setVisible(true); + } else { + trackConfig.setVisible(false); + } + } + setVisible(false); + } + + /** + * Create a JPanel for a particular track category, including a label and checkboxes for contained tracks. + * + * @param configGroup + * @return + */ + private CollapsiblePanel createCategoryPanel(TrackConfigGroup configGroup) { + + JPanel trackContainer = new JPanel(); + final WrapLayout wrapLayout = new WrapLayout(); + wrapLayout.setAlignment(FlowLayout.LEFT); + trackContainer.setLayout(wrapLayout); + + boolean isSelected = false; + for (TrackConfig trackConfig : configGroup.tracks) { + + final JCheckBox checkBox = new JCheckBox(); + configMap.put(checkBox, trackConfig); + checkBox.setSelected(trackConfig.getVisible()); + isSelected = isSelected || trackConfig.getVisible(); + + String infoLink = trackConfig.getHtml(); + JLabel label = new JLabel(trackConfig.getName()); + SelectionBox p = new SelectionBox(checkBox, label, infoLink); + + String longLabel = trackConfig.getLongLabel(); + if (longLabel != null) { + p.setToolTipText(longLabel); + } + + trackContainer.add(p); + } + + return new CollapsiblePanel(configGroup.label, trackContainer, isSelected || configGroup.defaultOpen); + } + + /** + * Convenience method to extract and return selected track configurations. + * + * @return + */ + public List getSelectedConfigs() { + List selected = configMap.values().stream().filter(trackConfig -> trackConfig.getVisible()).collect(Collectors.toList()); + return selected; + } + + static class SelectionBox extends JPanel { + + public SelectionBox(JCheckBox checkBox, JLabel label, String infoLink) { + this.setLayout(new BorderLayout()); + label.setLabelFor(checkBox); + add(checkBox, BorderLayout.WEST); + + if (infoLink == null || "".equals(infoLink.trim())) { + add(label, BorderLayout.CENTER); + } else { + ImageIcon icon = IconFactory.getInstance().getIcon(IconFactory.IconID.INFO); + JLabel iconLabel = new JLabel(icon); + iconLabel.setToolTipText(infoLink); + iconLabel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + add(iconLabel, BorderLayout.CENTER); + iconLabel.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + try { + Desktop.getDesktop().browse(new URI(infoLink)); + } catch (Exception ex) { + log.error("Error following hyperlink: " + infoLink, ex); + } + } + }); + + JPanel panel = new JPanel(); + panel.setLayout(new FlowLayout(FlowLayout.LEFT)); + panel.add(label); + panel.add(iconLabel); + add(panel, BorderLayout.CENTER); + } + } + + @Override + public Dimension getPreferredSize() { + return new Dimension(250, 20); + } + + @Override + public Dimension getMinimumSize() { + return getPreferredSize(); + } + + @Override + public Dimension getMaximumSize() { + return getPreferredSize(); + } + } + + + /** + * main for testing and development + * + * @param args + * @throws InterruptedException + * @throws InvocationTargetException + * @throws IOException + */ + public static void main(String[] args) throws InterruptedException, InvocationTargetException, IOException { + + String hubFile = "https://hgdownload.soe.ucsc.edu/gbdb/hs1/hubs/public/hub.txt"; + Hub hub = HubParser.loadHub(hubFile, "hs1"); + List groupedTrackConfigurations = hub.getGroupedTrackConfigurations(); + + final TrackHubSelectionDialog dlf = new TrackHubSelectionDialog(hub, groupedTrackConfigurations, null); + dlf.setVisible(true); + + for (TrackConfig config : dlf.getSelectedConfigs()) { + System.out.println(config.getName()); + } + } + +} diff --git a/src/main/java/org/broad/igv/ucsc/hub/WrapLayout.java b/src/main/java/org/broad/igv/ucsc/hub/WrapLayout.java new file mode 100644 index 0000000000..2e5a194aec --- /dev/null +++ b/src/main/java/org/broad/igv/ucsc/hub/WrapLayout.java @@ -0,0 +1,204 @@ +package org.broad.igv.ucsc.hub; + +import javax.swing.*; +import java.awt.*; + +/** + * A FlowLayout extension that wraps components as needed and updates the preferre size accordingly. + * FlowLayout itself does not update the preferred size, so the component width grows without bounds. + *

+ * Code courtesy Rob Camick. See https://tips4java.wordpress.com/2008/11/06/wrap-layout/ + *

+ * MIT License + *

+ * Copyright (c) 2023 Rob Camick + *

+ * 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. + */ +class WrapLayout extends FlowLayout { + private Dimension preferredLayoutSize; + + /** + * Constructs a new WrapLayout with a left + * alignment and a default 5-unit horizontal and vertical gap. + */ + public WrapLayout() { + super(); + } + + /** + * Constructs a new FlowLayout with the specified + * alignment and a default 5-unit horizontal and vertical gap. + * The value of the alignment argument must be one of + * WrapLayout, WrapLayout, + * or WrapLayout. + * + * @param align the alignment value + */ + public WrapLayout(int align) { + super(align); + } + + /** + * Creates a new flow layout manager with the indicated alignment + * and the indicated horizontal and vertical gaps. + *

+ * The value of the alignment argument must be one of + * WrapLayout, WrapLayout, + * or WrapLayout. + * + * @param align the alignment value + * @param hgap the horizontal gap between components + * @param vgap the vertical gap between components + */ + public WrapLayout(int align, int hgap, int vgap) { + super(align, hgap, vgap); + } + + /** + * Returns the preferred dimensions for this layout given the + * visible components in the specified target container. + * + * @param target the component which needs to be laid out + * @return the preferred dimensions to lay out the + * subcomponents of the specified container + */ + @Override + public Dimension preferredLayoutSize(Container target) { + return layoutSize(target, true); + } + + /** + * Returns the minimum dimensions needed to layout the visible + * components contained in the specified target container. + * + * @param target the component which needs to be laid out + * @return the minimum dimensions to lay out the + * subcomponents of the specified container + */ + @Override + public Dimension minimumLayoutSize(Container target) { + Dimension minimum = layoutSize(target, false); + minimum.width -= (getHgap() + 1); + return minimum; + } + + /** + * Returns the minimum or preferred dimension needed to layout the target + * container. + * + * @param target target to get layout size for + * @param preferred should preferred size be calculated + * @return the dimension to layout the target container + */ + private Dimension layoutSize(Container target, boolean preferred) { + synchronized (target.getTreeLock()) { + // Each row must fit with the width allocated to the containter. + // When the container width = 0, the preferred width of the container + // has not yet been calculated so lets ask for the maximum. + + int targetWidth = target.getSize().width; + Container container = target; + + while (container.getSize().width == 0 && container.getParent() != null) { + container = container.getParent(); + } + + targetWidth = container.getSize().width; + + if (targetWidth == 0) + targetWidth = Integer.MAX_VALUE; + + int hgap = getHgap(); + int vgap = getVgap(); + Insets insets = target.getInsets(); + int horizontalInsetsAndGap = insets.left + insets.right + (hgap * 2); + int maxWidth = targetWidth - horizontalInsetsAndGap; + + // Fit components into the allowed width + + Dimension dim = new Dimension(0, 0); + int rowWidth = 0; + int rowHeight = 0; + + int nmembers = target.getComponentCount(); + + for (int i = 0; i < nmembers; i++) { + Component m = target.getComponent(i); + + if (m.isVisible()) { + Dimension d = preferred ? m.getPreferredSize() : m.getMinimumSize(); + + // Can't add the component to current row. Start a new row. + + if (rowWidth + d.width > maxWidth) { + addRow(dim, rowWidth, rowHeight); + rowWidth = 0; + rowHeight = 0; + } + + // Add a horizontal gap for all components after the first + + if (rowWidth != 0) { + rowWidth += hgap; + } + + rowWidth += d.width; + rowHeight = Math.max(rowHeight, d.height); + } + } + + addRow(dim, rowWidth, rowHeight); + + dim.width += horizontalInsetsAndGap; + dim.height += insets.top + insets.bottom + vgap * 2; + + // When using a scroll pane or the DecoratedLookAndFeel we need to + // make sure the preferred size is less than the size of the + // target containter so shrinking the container size works + // correctly. Removing the horizontal gap is an easy way to do this. + + Container scrollPane = SwingUtilities.getAncestorOfClass(JScrollPane.class, target); + + if (scrollPane != null && target.isValid()) { + dim.width -= (hgap + 1); + } + + return dim; + } + } + + /* + * A new row has been completed. Use the dimensions of this row + * to update the preferred size for the container. + * + * @param dim update the width and height when appropriate + * @param rowWidth the width of the row to add + * @param rowHeight the height of the row to add + */ + private void addRow(Dimension dim, int rowWidth, int rowHeight) { + dim.width = Math.max(dim.width, rowWidth); + + if (dim.height > 0) { + dim.height += getVgap(); + } + + dim.height += rowHeight; + } +} diff --git a/src/main/java/org/broad/igv/ui/IGVMenuBar.java b/src/main/java/org/broad/igv/ui/IGVMenuBar.java index 6ec97373f7..c9659150ae 100644 --- a/src/main/java/org/broad/igv/ui/IGVMenuBar.java +++ b/src/main/java/org/broad/igv/ui/IGVMenuBar.java @@ -40,6 +40,7 @@ import org.broad.igv.feature.genome.ChromSizesUtils; import org.broad.igv.track.AttributeManager; import org.broad.igv.track.Track; +import org.broad.igv.ucsc.hub.Hub; import org.broad.igv.ui.commandbar.HostedGenomeSelectionDialog; import org.broad.igv.util.GoogleUtils; import org.broad.igv.oauth.OAuthProvider; @@ -108,16 +109,11 @@ public class IGVMenuBar extends JMenuBar implements IGVEventObserver { * we can't access genome server list */ private JMenuItem loadGenomeFromServerMenuItem; - private JMenuItem loadTracksFromServerMenuItem; private JMenuItem selectGenomeAnnotationsItem; - private JMenuItem encodeUCSCMenuItem; - private List encodeMenuItems = new ArrayList<>(); - private JMenuItem reloadSessionItem; private JMenuItem recentFilesMenu; - static IGVMenuBar createInstance(IGV igv) { if (instance != null) { if (igv == instance.igv) { @@ -170,9 +166,10 @@ public void showAboutDialog() { private List createMenus() { - List menus = new ArrayList(); + List menus = new ArrayList<>(); - menus.add(createFileMenu()); + JMenu fileMenu = createFileMenu(GenomeManager.getInstance().getCurrentGenome()); + menus.add(fileMenu); menus.add(createGenomesMenu()); menus.add(createViewMenu()); menus.add(createTracksMenu()); @@ -273,18 +270,22 @@ public void enableExtrasMenu() { } - JMenu createFileMenu() { + JMenu createFileMenu(Genome genome) { - Genome genome = GenomeManager.getInstance().getCurrentGenome(); - String genomeId = genome == null ? null : genome.getId(); + if (genome == null) { + return new JMenu("File"); + } - List menuItems = new ArrayList(); - MenuAction menuAction = null; + String genomeId = genome.getId(); + + List menuItems = new ArrayList<>(); + + MenuAction menuAction; menuItems.add(new JSeparator()); // Load menu items - menuAction = new LoadFilesMenuAction("Load from File...", KeyEvent.VK_L, igv); + menuAction = new LoadFilesMenuAction("Load Tracks from File...", KeyEvent.VK_L, igv); menuAction.setToolTipText(UIConstants.LOAD_TRACKS_TOOLTIP); menuItems.add(MenuAndToolbarUtils.createMenuItem(menuAction)); @@ -292,46 +293,58 @@ JMenu createFileMenu() { menuAction.setToolTipText(UIConstants.LOAD_TRACKS_TOOLTIP); menuItems.add(MenuAndToolbarUtils.createMenuItem(menuAction)); - menuAction = new LoadFromServerAction("Load From Server...", KeyEvent.VK_S, igv); - menuAction.setToolTipText(UIConstants.LOAD_SERVER_DATA_TOOLTIP); - loadTracksFromServerMenuItem = MenuAndToolbarUtils.createMenuItem(menuAction); - menuItems.add(loadTracksFromServerMenuItem); + if (genomeId != null && LoadFromServerAction.getNodeURLs(genomeId) != null) { + menuAction = new LoadFromServerAction("Load Tracks From IGV Server...", KeyEvent.VK_S, igv); + menuAction.setToolTipText(UIConstants.LOAD_SERVER_DATA_TOOLTIP); + JMenuItem loadTracksFromServerMenuItem = MenuAndToolbarUtils.createMenuItem(menuAction); + menuItems.add(loadTracksFromServerMenuItem); + } + recentFilesMenu = new RecentUrlsMenu(); menuItems.add(recentFilesMenu); - if (PreferencesManager.getPreferences().getAsBoolean(DB_ENABLED)) { - menuAction = new LoadFromDatabaseAction("Load from Database...", 0, igv); - menuItems.add(MenuAndToolbarUtils.createMenuItem(menuAction)); + // Track hubs + if (genome.getTrackHubs().size() > 0) { + menuItems.add(new JSeparator()); + menuItems.add(new JLabel("Track Hubs")); + for (Hub trackHub : genome.getTrackHubs()) { + menuAction = new SelectHubTracksAction(trackHub.getShortLabel(), igv, trackHub); + menuAction.setToolTipText(trackHub.getLongLabel()); + JMenuItem selectGenomeAnnotationsItem = MenuAndToolbarUtils.createMenuItem(menuAction); + selectGenomeAnnotationsItem.setToolTipText(trackHub.getLongLabel()); + menuItems.add(selectGenomeAnnotationsItem); + } } // ENCODE items. These will be hidden / shown depending on genome chosen - JSeparator separator = new JSeparator(); - menuItems.add(separator); + if (EncodeTrackChooser.genomeSupportedUCSC(genomeId) || EncodeTrackChooser.genomeSupported(genomeId)) { - // Post 2012 ENCODE menu - JMenuItem chipItem = new JMenuItem(); - chipItem.setAction(new BrowseEncodeAction("ENCODE ChIP Signals ...", 0, BrowseEncodeAction.Type.SIGNALS_CHIP, igv)); - encodeMenuItems.add(chipItem); + JSeparator separator = new JSeparator(); + menuItems.add(separator); - JMenuItem otherSignalsItem = new JMenuItem(); - otherSignalsItem.setAction(new BrowseEncodeAction("ENCODE Other Signals ...", 0, BrowseEncodeAction.Type.SIGNALS_OTHER, igv)); - encodeMenuItems.add(otherSignalsItem); + // Post 2012 ENCODE menu + if (EncodeTrackChooser.genomeSupported(genomeId)) { + JMenuItem chipItem = new JMenuItem(); + chipItem.setAction(new BrowseEncodeAction("ENCODE ChIP Signals ...", 0, BrowseEncodeAction.Type.SIGNALS_CHIP, igv)); + menuItems.add(chipItem); - JMenuItem otherItem = new JMenuItem(); - otherItem.setAction(new BrowseEncodeAction("ENCODE Other ...", 0, BrowseEncodeAction.Type.OTHER, igv)); - encodeMenuItems.add(otherItem); + JMenuItem otherSignalsItem = new JMenuItem(); + otherSignalsItem.setAction(new BrowseEncodeAction("ENCODE Other Signals ...", 0, BrowseEncodeAction.Type.SIGNALS_OTHER, igv)); + menuItems.add(otherSignalsItem); - for(JComponent item : encodeMenuItems) { - menuItems.add(item); - item.setVisible(EncodeTrackChooser.genomeSupported(genomeId)); - } + JMenuItem otherItem = new JMenuItem(); + otherItem.setAction(new BrowseEncodeAction("ENCODE Other ...", 0, BrowseEncodeAction.Type.OTHER, igv)); + menuItems.add(otherItem); + } - // UCSC hosted ENCODE menu. - encodeUCSCMenuItem = MenuAndToolbarUtils.createMenuItem( - new BrowseEncodeAction("ENCODE 2012 UCSC Repository ...", KeyEvent.VK_E, BrowseEncodeAction.Type.UCSC, igv)); - encodeUCSCMenuItem.setVisible(EncodeTrackChooser.genomeSupportedUCSC(genomeId)); - menuItems.add(encodeUCSCMenuItem); + // UCSC hosted ENCODE menu. + if (EncodeTrackChooser.genomeSupportedUCSC(genomeId)) { + JMenuItem encodeUCSCMenuItem = MenuAndToolbarUtils.createMenuItem( + new BrowseEncodeAction("ENCODE 2012 UCSC Repository ...", KeyEvent.VK_E, BrowseEncodeAction.Type.UCSC, igv)); + menuItems.add(encodeUCSCMenuItem); + } + } menuItems.add(new JSeparator()); menuAction = new ReloadTracksMenuAction("Reload Tracks", -1, igv); @@ -408,7 +421,8 @@ public void actionPerformed(ActionEvent e) { recentSessionsSep.setVisible(false); menuItems.add(recentSessionsSep); //menuItems.addAll(addRecentSessionMenuItems()); - menuItems.add(new JSeparator());; + menuItems.add(new JSeparator()); + ; MenuAction fileMenuAction = new MenuAction("File", null, KeyEvent.VK_F); JMenu fileMenu = MenuAndToolbarUtils.createMenu(menuItems, fileMenuAction); @@ -423,40 +437,6 @@ public void actionPerformed(ActionEvent e) { return fileMenu; } - private void addEncodeItems(List menuItems, String genomeId) { - - JSeparator separator = new JSeparator(); - menuItems.add(separator); - - JLabel encodeLabel = new JLabel(" ENCODE"); - encodeLabel.setFont(encodeLabel.getFont().deriveFont(Font.BOLD)); - menuItems.add(encodeLabel); - - // Post 2012 ENCODE menu - JMenuItem chipItem = new JMenuItem(); - chipItem.setAction(new BrowseEncodeAction("CHiP - Signals", 0, BrowseEncodeAction.Type.SIGNALS_CHIP, igv)); - encodeMenuItems.add(chipItem); - - JMenuItem otherSignalsItem = new JMenuItem(); - otherSignalsItem.setAction(new BrowseEncodeAction("Other - Signals", 0, BrowseEncodeAction.Type.SIGNALS_OTHER, igv)); - encodeMenuItems.add(otherSignalsItem); - - JMenuItem otherItem = new JMenuItem(); - otherItem.setAction(new BrowseEncodeAction("Other (peaks, calls, ...)", 0, BrowseEncodeAction.Type.OTHER, igv)); - encodeMenuItems.add(otherItem); - - for (JComponent item : encodeMenuItems) { - menuItems.add(item); - item.setVisible(EncodeTrackChooser.genomeSupported(genomeId)); - } - - // UCSC hosted ENCODE menu. - encodeUCSCMenuItem = MenuAndToolbarUtils.createMenuItem( - new BrowseEncodeAction("ENCODE (2012)...", KeyEvent.VK_E, BrowseEncodeAction.Type.UCSC, igv)); - encodeUCSCMenuItem.setVisible(EncodeTrackChooser.genomeSupportedUCSC(genomeId)); - menuItems.add(encodeUCSCMenuItem); - } - private JMenu createGenomesMenu() { JMenu menu = new JMenu("Genomes"); @@ -501,10 +481,10 @@ private JMenu createGenomesMenu() { MenuAction genArkAction = new UCSCGenArkAction("Load Genome from UCSC GenArk...", 0, igv); menu.add(MenuAndToolbarUtils.createMenuItem(genArkAction)); - MenuAction menuAction = new SelectGenomeAnnotationTracksAction("Select GenArk Tracks...", igv); + MenuAction menuAction = new SelectHubTracksAction("Select GenArk Genome Tracks...", igv, null); selectGenomeAnnotationsItem = MenuAndToolbarUtils.createMenuItem(menuAction); Genome newGenome = GenomeManager.getInstance().getCurrentGenome(); - selectGenomeAnnotationsItem.setEnabled(newGenome != null && newGenome.getHub() != null); + selectGenomeAnnotationsItem.setEnabled(newGenome != null && newGenome.getGenomeHub() != null); menu.add(selectGenomeAnnotationsItem); menu.add(new JSeparator()); @@ -521,7 +501,7 @@ public void actionPerformed(ActionEvent event) { menu.addMenuListener((MenuSelectedListener) e -> { Genome genome1 = GenomeManager.getInstance().getCurrentGenome(); - selectGenomeAnnotationsItem.setEnabled(genome1 != null && genome1.getHub() != null); + selectGenomeAnnotationsItem.setEnabled(genome1 != null && genome1.getGenomeHub() != null); }); return menu; @@ -1186,13 +1166,10 @@ public void receiveEvent(final IGVEvent event) { if (event instanceof GenomeChangeEvent) { UIUtilities.invokeOnEventThread(() -> { + IGVMenuBar.this.remove(0); final Genome genome = ((GenomeChangeEvent) event).genome(); - final String genomeId = genome.getId(); - encodeUCSCMenuItem.setVisible(EncodeTrackChooser.genomeSupportedUCSC(genomeId)); - for (JComponent item : encodeMenuItems) { - item.setVisible(EncodeTrackChooser.genomeSupported(genomeId)); - } - + JMenu fileMenu = createFileMenu(genome); + IGVMenuBar.this.add(fileMenu, 0); }); } } @@ -1201,7 +1178,7 @@ public void enableReloadSession() { this.reloadSessionItem.setEnabled(true); } - public void showRecentFilesMenu(){ + public void showRecentFilesMenu() { this.recentFilesMenu.setVisible(true); } diff --git a/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java b/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java index d464afa991..5bdda10fca 100644 --- a/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java +++ b/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java @@ -54,7 +54,7 @@ */ public class LoadFromURLMenuAction extends MenuAction { - public static final String LOAD_FROM_URL = "Load from URL..."; + public static final String LOAD_FROM_URL = "Load Track from URL..."; public static final String LOAD_GENOME_FROM_URL = "Load Genome from URL..."; public static final String LOAD_FROM_HTSGET = "Load from htsget Server..."; public static final String LOAD_TRACKHUB = "Load Track Hub..."; @@ -126,6 +126,7 @@ private void loadUrls(List inputs, List indexes, boolean isHtsGe } } + // Note: this is not currently used private static void loadTrackHub(JPanel ta) { String urlOrAccension = JOptionPane.showInputDialog(IGV.getInstance().getMainFrame(), ta, "Enter GCA or GCF accession, or URL to hub.txt file", JOptionPane.QUESTION_MESSAGE); diff --git a/src/main/java/org/broad/igv/ui/action/SelectGenomeAnnotationTracksAction.java b/src/main/java/org/broad/igv/ui/action/SelectGenomeAnnotationTracksAction.java deleted file mode 100644 index d6671aa3bd..0000000000 --- a/src/main/java/org/broad/igv/ui/action/SelectGenomeAnnotationTracksAction.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * 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. - */ - -/* - * To change this template, choose Tools | Templates - * and open the template in the editor. - */ -package org.broad.igv.ui.action; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.broad.igv.feature.genome.Genome; -import org.broad.igv.feature.genome.GenomeManager; -import org.broad.igv.feature.genome.load.TrackConfig; -import org.broad.igv.logging.LogManager; -import org.broad.igv.logging.Logger; -import org.broad.igv.prefs.PreferencesManager; -import org.broad.igv.track.Track; -import org.broad.igv.ucsc.Hub; -import org.broad.igv.ucsc.TrackConfigGroup; -import org.broad.igv.ucsc.HubTrackSelectionDialog; -import org.broad.igv.ui.IGV; -import org.broad.igv.ui.util.MessageUtils; -import org.broad.igv.util.ResourceLocator; - -import java.awt.event.ActionEvent; -import java.util.*; - -/** - * @author jrobinso - */ -public class SelectGenomeAnnotationTracksAction extends MenuAction { - - static Logger log = LogManager.getLogger(SelectGenomeAnnotationTracksAction.class); - IGV mainFrame; - - // Keep track of authorization failures so user isn't constantly harranged - static HashSet failedURLs = new HashSet(); - - - public SelectGenomeAnnotationTracksAction(String label, IGV mainFrame) { - super(label, null); - this.mainFrame = mainFrame; - } - - @Override - public void actionPerformed(ActionEvent evt) { - - Genome genome = GenomeManager.getInstance().getCurrentGenome(); - Hub hub = genome.getHub(); - if (hub == null) { - // This should not happen - MessageUtils.showMessage("No annotation tracks available for current genome."); - } - - final List loadedTracks = IGV.getInstance().getAllTracks().stream().filter(t -> t.getResourceLocator() != null).toList(); - Set loadedTrackPaths = new HashSet<>(loadedTracks.stream().map(t -> t.getResourceLocator().getPath()).toList()); - List groups = hub.getGroupedTrackConfigurations(); - for (TrackConfigGroup g : groups) { - for (TrackConfig config : g.tracks) { - config.setVisible(loadedTrackPaths.contains(config.getUrl())); - } - } - - HubTrackSelectionDialog dlg = new HubTrackSelectionDialog(groups, IGV.getInstance().getMainFrame()); - dlg.setVisible(true); - - // The dialog action will modify the visible state for each track config - Set trackPathsToRemove = new HashSet<>(); - List tracksToLoad = new ArrayList<>(); - List selected = new ArrayList<>(); - for (TrackConfigGroup g : groups) { - for (TrackConfig config : g.tracks) { - if (config.getVisible()) { - selected.add(config); - if (!loadedTrackPaths.contains(config.getUrl())) { - tracksToLoad.add(config); - } - } else { - trackPathsToRemove.add(config.getUrl()); - } - } - } - - List tracksToRemove = loadedTracks.stream().filter(t -> trackPathsToRemove.contains(t.getResourceLocator().getPath())).toList(); - IGV.getInstance().deleteTracks(tracksToRemove); - - List locators = tracksToLoad.stream().map(t -> ResourceLocator.fromTrackConfig(t)).toList(); - IGV.getInstance().loadTracks(locators); - - // Update genome - genome.setAnnotationResources(locators); - - // Update preferences - String key = "hub:" + hub.getUrl(); - PreferencesManager.getPreferences().put(key, String.join(",", selected.stream().map(c -> c.getName()).toList())); - - } - - -} diff --git a/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java b/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java new file mode 100644 index 0000000000..07f8b1edb8 --- /dev/null +++ b/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java @@ -0,0 +1,137 @@ +/* + * 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. + */ + +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package org.broad.igv.ui.action; + +import org.broad.igv.feature.genome.Genome; +import org.broad.igv.feature.genome.GenomeManager; +import org.broad.igv.feature.genome.load.TrackConfig; +import org.broad.igv.logging.LogManager; +import org.broad.igv.logging.Logger; +import org.broad.igv.prefs.PreferencesManager; +import org.broad.igv.track.Track; +import org.broad.igv.ucsc.hub.Hub; +import org.broad.igv.ucsc.hub.TrackConfigGroup; +import org.broad.igv.ucsc.hub.TrackHubSelectionDialog; +import org.broad.igv.ui.IGV; +import org.broad.igv.ui.util.MessageUtils; +import org.broad.igv.util.ResourceLocator; + +import java.awt.event.ActionEvent; +import java.util.*; + +/** + * Select tracks from a track hub. This action is used in 2 modes, (1) load tracks from a specific hub, and (2) select + * default annotation tracks for the currently loaded genome. Tracks are loaded in both modes, but in the + * second selected tracks are also added to the genome definition. + * + * @author jrobinso + */ +public class SelectHubTracksAction extends MenuAction { + + static Logger log = LogManager.getLogger(SelectHubTracksAction.class); + private Hub hub; + IGV mainFrame; + + // Keep track of authorization failures so user isn't constantly harranged + static HashSet failedURLs = new HashSet(); + + boolean updateGenome; + + public SelectHubTracksAction(String label, IGV mainFrame, Hub hub) { + super(label, null); + this.mainFrame = mainFrame; + this.updateGenome = hub == null; + this.hub = hub; + } + + @Override + public void actionPerformed(ActionEvent evt) { + + try { + Genome genome = GenomeManager.getInstance().getCurrentGenome(); + if (hub == null) { + hub = genome.getGenomeHub(); + if (hub == null) { + // This should not happen + MessageUtils.showMessage("No tracks available for current genome."); + } + } + + final List loadedTracks = IGV.getInstance().getAllTracks().stream().filter(t -> t.getResourceLocator() != null).toList(); + Set loadedTrackPaths = new HashSet<>(loadedTracks.stream().map(t -> t.getResourceLocator().getPath()).toList()); + List groups = hub.getGroupedTrackConfigurations(); + for (TrackConfigGroup g : groups) { + for (TrackConfig config : g.tracks) { + config.setVisible(loadedTrackPaths.contains(config.getUrl())); + } + } + + TrackHubSelectionDialog dlg = new TrackHubSelectionDialog(hub, groups, IGV.getInstance().getMainFrame()); + dlg.setVisible(true); + + // The dialog action will modify the visible state for each track config + Set trackPathsToRemove = new HashSet<>(); + List tracksToLoad = new ArrayList<>(); + List selected = new ArrayList<>(); + for (TrackConfigGroup g : groups) { + for (TrackConfig config : g.tracks) { + if (config.getVisible()) { + selected.add(config); + if (!loadedTrackPaths.contains(config.getUrl())) { + tracksToLoad.add(config); + } + } else { + trackPathsToRemove.add(config.getUrl()); + } + } + } + + List tracksToRemove = loadedTracks.stream().filter(t -> trackPathsToRemove.contains(t.getResourceLocator().getPath())).toList(); + IGV.getInstance().deleteTracks(tracksToRemove); + + List locators = tracksToLoad.stream().map(t -> ResourceLocator.fromTrackConfig(t)).toList(); + IGV.getInstance().loadTracks(locators); + + // Update genome + if (updateGenome) { + genome.setAnnotationResources(locators); + // Update preferences + String key = "hub:" + hub.getUrl(); + PreferencesManager.getPreferences().put(key, String.join(",", selected.stream().map(c -> c.getName()).toList())); + } + } catch (Exception e) { + log.error("Error loading track configurations", e); + MessageUtils.showMessage(e.getMessage()); + } + + } + + +} diff --git a/src/main/java/org/broad/igv/ui/util/IconFactory.java b/src/main/java/org/broad/igv/ui/util/IconFactory.java index e29d07d211..4931b31e39 100644 --- a/src/main/java/org/broad/igv/ui/util/IconFactory.java +++ b/src/main/java/org/broad/igv/ui/util/IconFactory.java @@ -53,7 +53,8 @@ public enum IconID { FIST, CLOSE, PLUS, - MINUS + MINUS, + INFO } private Map icons; @@ -110,6 +111,8 @@ private IconFactory() { createImageIcon("/images/plus_sm.gif" , "plus")); icons.put(IconID.MINUS, createImageIcon("/images/minus_sm.gif", "minus")); + icons.put(IconID.INFO, + createImageIcon("/images/info.gif", "info")); } public ImageIcon getIcon(IconID id) { diff --git a/src/test/java/org/broad/igv/feature/genome/load/HubGenomeLoaderTest.java b/src/test/java/org/broad/igv/feature/genome/load/GenomeHubLoaderTest.java similarity index 92% rename from src/test/java/org/broad/igv/feature/genome/load/HubGenomeLoaderTest.java rename to src/test/java/org/broad/igv/feature/genome/load/GenomeHubLoaderTest.java index fa95068435..86e2d58fc0 100644 --- a/src/test/java/org/broad/igv/feature/genome/load/HubGenomeLoaderTest.java +++ b/src/test/java/org/broad/igv/feature/genome/load/GenomeHubLoaderTest.java @@ -4,7 +4,7 @@ import static org.junit.Assert.*; -public class HubGenomeLoaderTest { +public class GenomeHubLoaderTest { @Test public void convert() { diff --git a/src/test/java/org/broad/igv/ucsc/HubTest.java b/src/test/java/org/broad/igv/ucsc/hub/HubTest.java similarity index 57% rename from src/test/java/org/broad/igv/ucsc/HubTest.java rename to src/test/java/org/broad/igv/ucsc/hub/HubTest.java index c2960b8ef8..a760200b81 100644 --- a/src/test/java/org/broad/igv/ucsc/HubTest.java +++ b/src/test/java/org/broad/igv/ucsc/hub/HubTest.java @@ -1,4 +1,4 @@ -package org.broad.igv.ucsc; +package org.broad.igv.ucsc.hub; import org.broad.igv.feature.genome.load.GenomeConfig; import org.broad.igv.util.TestUtils; @@ -15,12 +15,12 @@ public class HubTest { public void testGetGenomeConfig() throws IOException { String hubFile = TestUtils.DATA_DIR + "hubs/hub.txt"; - Hub hub = Hub.loadHub(hubFile); - assertNotNull(hub.hub); + Hub hub = HubParser.loadAssemblyHub(hubFile); + assertNotNull(hub.hubStanza); assertNotNull(hub.genomeStanza); assertEquals(22, hub.trackStanzas.size()); - GenomeConfig genomeConfig = hub.getGenomeConfig(true); + GenomeConfig genomeConfig = hub.getGenomeConfig(); assertNotNull(genomeConfig); assertEquals("GCF_000186305.1", genomeConfig.getId()); assertEquals("Python bivittatus (GCF_000186305.1)", genomeConfig.getName()); @@ -33,9 +33,19 @@ public void testGetGenomeConfig() throws IOException { @Test public void testGetGroupedTrackConfigurations() throws IOException { String hubFile = TestUtils.DATA_DIR + "hubs/hub.txt"; - Hub hub = Hub.loadHub(hubFile); - List groupedTrackConfigurations = hub.getGroupedTrackConfigurations(); + Hub hub = HubParser.loadAssemblyHub(hubFile); + List groupedTrackConfigurations = hub.getGroupedTrackConfigurations(); assertEquals(5, groupedTrackConfigurations.size()); } + @Test + public void testNCBIHostedHub() throws IOException { + + String hubFile = "https://ftp.ncbi.nlm.nih.gov/snp/population_frequency/TrackHub/latest/hub.txt"; + Hub hub = HubParser.loadHub(hubFile, "hg38"); + List groupedTrackConfigurations = hub.getGroupedTrackConfigurations(); + assertEquals(1, groupedTrackConfigurations.size()); + assertEquals(12, groupedTrackConfigurations.get(0).tracks.size()); + + } } \ No newline at end of file From b1a1701a72e0948a4ab3afa3a3f0c680217aec02 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Fri, 7 Feb 2025 20:42:01 -0800 Subject: [PATCH 106/130] Merge branch 'main' into track_hubs From 066f3af7895528dac4a2223183730eae0c02f478 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Mon, 10 Feb 2025 12:59:22 -0800 Subject: [PATCH 107/130] Bug fix - "Select hosted genomes" did not download. --- src/main/java/org/broad/igv/ucsc/hub/Hub.java | 3 --- src/main/java/org/broad/igv/ui/IGVMenuBar.java | 5 ++--- .../java/org/broad/igv/ui/commandbar/GenomeComboBox.java | 2 +- .../igv/ui/commandbar/HostedGenomeSelectionDialog.java | 8 ++------ 4 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/broad/igv/ucsc/hub/Hub.java b/src/main/java/org/broad/igv/ucsc/hub/Hub.java index 6aaadc188f..dcaf9429b7 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/Hub.java +++ b/src/main/java/org/broad/igv/ucsc/hub/Hub.java @@ -125,9 +125,6 @@ public GenomeConfig getGenomeConfig() { if (genomeStanza.hasProperty("chromAliasBb")) { config.setChromAliasBbURL(genomeStanza.getProperty("chromAliasBb")); } - if (genomeStanza.hasProperty("twoBitBptURL")) { - config.setTwoBitBptURL(genomeStanza.getProperty("twoBitBptURL")); - } if (genomeStanza.hasProperty("twoBitBptUrl")) { config.setTwoBitBptURL(genomeStanza.getProperty("twoBitBptUrl")); } diff --git a/src/main/java/org/broad/igv/ui/IGVMenuBar.java b/src/main/java/org/broad/igv/ui/IGVMenuBar.java index c9659150ae..2f65cd4599 100644 --- a/src/main/java/org/broad/igv/ui/IGVMenuBar.java +++ b/src/main/java/org/broad/igv/ui/IGVMenuBar.java @@ -307,7 +307,6 @@ JMenu createFileMenu(Genome genome) { // Track hubs if (genome.getTrackHubs().size() > 0) { menuItems.add(new JSeparator()); - menuItems.add(new JLabel("Track Hubs")); for (Hub trackHub : genome.getTrackHubs()) { menuAction = new SelectHubTracksAction(trackHub.getShortLabel(), igv, trackHub); menuAction.setToolTipText(trackHub.getLongLabel()); @@ -441,8 +440,8 @@ private JMenu createGenomesMenu() { JMenu menu = new JMenu("Genomes"); - loadGenomeFromServerMenuItem = new JMenuItem("Select Hosted Genome..."); - loadGenomeFromServerMenuItem.addActionListener(e -> HostedGenomeSelectionDialog.selectHostedGenome()); + loadGenomeFromServerMenuItem = new JMenuItem("Download Hosted Genome..."); + loadGenomeFromServerMenuItem.addActionListener(e -> HostedGenomeSelectionDialog.downloadHostedGenome()); loadGenomeFromServerMenuItem.setToolTipText(LOAD_GENOME_SERVER_TOOLTIP); menu.add(loadGenomeFromServerMenuItem); diff --git a/src/main/java/org/broad/igv/ui/commandbar/GenomeComboBox.java b/src/main/java/org/broad/igv/ui/commandbar/GenomeComboBox.java index 687eee0a18..ea8a2fcdad 100644 --- a/src/main/java/org/broad/igv/ui/commandbar/GenomeComboBox.java +++ b/src/main/java/org/broad/igv/ui/commandbar/GenomeComboBox.java @@ -96,7 +96,7 @@ public void actionPerformed(ActionEvent actionEvent) { if (genomeListItem != null && genomeListItem.getPath() != null) { if (genomeListItem == GenomeListItem.DOWNLOAD_ITEM) { - HostedGenomeSelectionDialog.selectHostedGenome(); + HostedGenomeSelectionDialog.downloadHostedGenome(); } else { try { diff --git a/src/main/java/org/broad/igv/ui/commandbar/HostedGenomeSelectionDialog.java b/src/main/java/org/broad/igv/ui/commandbar/HostedGenomeSelectionDialog.java index 4cf472c7de..29eebb844b 100644 --- a/src/main/java/org/broad/igv/ui/commandbar/HostedGenomeSelectionDialog.java +++ b/src/main/java/org/broad/igv/ui/commandbar/HostedGenomeSelectionDialog.java @@ -88,7 +88,7 @@ public class HostedGenomeSelectionDialog extends org.broad.igv.ui.IGVDialog { * Open a selection list to load a genome from the server. This method is static because its used by multiple * UI elements (menu bar and genome selection pulldown). */ - public static void selectHostedGenome() { + public static void downloadHostedGenome() { Runnable showDialog = () -> { @@ -109,11 +109,7 @@ public static void selectHostedGenome() { boolean downloadSequence = dialog.isDownloadSequence(); boolean downloadAnnotations = dialog.isDownloadAnnotations(); - File downloadPath = null; - if (downloadSequence || downloadAnnotations || selectedItem.getPath().endsWith(".genome")) { - downloadPath = GenomeManager.getInstance().downloadGenome(selectedItem, downloadSequence, downloadAnnotations); - } - + File downloadPath = GenomeManager.getInstance().downloadGenome(selectedItem, downloadSequence, downloadAnnotations); try { if (downloadPath != null) { From 0d627c79d490fa21ec50336509f61c2bb1aae2c3 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Mon, 10 Feb 2025 13:15:33 -0800 Subject: [PATCH 108/130] Revert hub change --- src/main/java/org/broad/igv/ucsc/hub/Hub.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/broad/igv/ucsc/hub/Hub.java b/src/main/java/org/broad/igv/ucsc/hub/Hub.java index dcaf9429b7..6aaadc188f 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/Hub.java +++ b/src/main/java/org/broad/igv/ucsc/hub/Hub.java @@ -125,6 +125,9 @@ public GenomeConfig getGenomeConfig() { if (genomeStanza.hasProperty("chromAliasBb")) { config.setChromAliasBbURL(genomeStanza.getProperty("chromAliasBb")); } + if (genomeStanza.hasProperty("twoBitBptURL")) { + config.setTwoBitBptURL(genomeStanza.getProperty("twoBitBptURL")); + } if (genomeStanza.hasProperty("twoBitBptUrl")) { config.setTwoBitBptURL(genomeStanza.getProperty("twoBitBptUrl")); } From 620cf91ae9d973e85106dd08189787f48b079339 Mon Sep 17 00:00:00 2001 From: Louis Bergelson Date: Tue, 11 Feb 2025 16:47:15 -0500 Subject: [PATCH 109/130] Add a module requirement on commons compress (#1656) This fixes a confusing issue with cram files silently failing to load due to a missing class file. --- src/main/java/module-info.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 075e7b3e75..8efafeb4e5 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -15,6 +15,7 @@ exports org.broad.igv.ucsc.hub; requires com.google.common; + requires org.apache.commons.compress; requires commons.math3; requires com.google.gson; requires htsjdk; From 3c45e56f0e80c96b8efd3e83011ebdca15f8ce83 Mon Sep 17 00:00:00 2001 From: Louis Bergelson Date: Tue, 11 Feb 2025 23:49:51 -0500 Subject: [PATCH 110/130] Catch error instead of exception in longrunning task (#1655) * Catch Error instead of Exception when calling long running task * horrible bug --- src/main/java/org/broad/igv/util/LongRunningTask.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/broad/igv/util/LongRunningTask.java b/src/main/java/org/broad/igv/util/LongRunningTask.java index a37d21804d..f1f02be54b 100644 --- a/src/main/java/org/broad/igv/util/LongRunningTask.java +++ b/src/main/java/org/broad/igv/util/LongRunningTask.java @@ -71,7 +71,7 @@ public Void call() { CursorToken token = WaitCursorManager.showWaitCursor(); try { runnable.run(); - } catch (Exception e) { + } catch (Throwable e) { MessageUtils.showMessage("Unexpected error: " + e.getMessage() + ".
See igv.log for more details"); log.error("Exception running task", e); } finally { From 74acc210899297aabcbb272cfa10ad1aac9db59b Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Thu, 13 Feb 2025 13:28:49 -0800 Subject: [PATCH 111/130] Account for hard clipping in reported read sequence length. See #1658 --- .../java/org/broad/igv/sam/SAMAlignment.java | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/broad/igv/sam/SAMAlignment.java b/src/main/java/org/broad/igv/sam/SAMAlignment.java index 8e9121a484..cdfd35b28f 100644 --- a/src/main/java/org/broad/igv/sam/SAMAlignment.java +++ b/src/main/java/org/broad/igv/sam/SAMAlignment.java @@ -29,11 +29,7 @@ */ package org.broad.igv.sam; -import htsjdk.samtools.Cigar; -import htsjdk.samtools.SAMFileHeader; -import htsjdk.samtools.SAMReadGroupRecord; -import htsjdk.samtools.SAMRecord; -import htsjdk.samtools.SAMTag; +import htsjdk.samtools.*; import htsjdk.samtools.util.SequenceUtil; import org.broad.igv.logging.*; import org.broad.igv.Globals; @@ -228,7 +224,6 @@ public List getAttributes() { } - private Object getAttribute(SAMTag key) { return key == null ? null : record.getAttribute(key); } @@ -317,12 +312,24 @@ public int getAlignmentEnd() { } public String getReadLengthString() { - String rs = record.getReadString(); - if (rs.equals("*") || rs.equals("")) { - return "undefined"; - } else { - return Globals.DECIMAL_FORMAT.format(rs.length()) + "bp"; + + Cigar cigar = getCigar(); + int readSequenceLength = cigar.getReadLength(); + int clippedLength = 0; + CigarElement first = cigar.getFirstCigarElement(); + if (first.getOperator() == htsjdk.samtools.CigarOperator.H) { + clippedLength += first.getLength(); } + CigarElement last = cigar.getLastCigarElement(); + if(last.getOperator() == htsjdk.samtools.CigarOperator.H) { + clippedLength += last.getLength(); + } + String readLengthString = Globals.DECIMAL_FORMAT.format(readSequenceLength + clippedLength) + " bp"; + if(clippedLength > 0) { + readLengthString += " (" + Globals.DECIMAL_FORMAT.format(readSequenceLength) + " sequence + " + Globals.DECIMAL_FORMAT.format(clippedLength) + " hard clipped)"; + } + + return readLengthString; } public String getSample() { @@ -721,7 +728,7 @@ public String getAlignmentValueString(double position, int mouseX, AlignmentTrac if (this.insertions != null) { for (AlignmentBlock block : this.insertions) { if (block.containsPixel(mouseX)) { - if(hideSmallIndels && block.getBasesLength() < smallIndelThreshold) { + if (hideSmallIndels && block.getBasesLength() < smallIndelThreshold) { continue; } ByteSubarray bases = block.getBases(); @@ -1238,7 +1245,7 @@ private static boolean operatorIsMatch(boolean showSoftClipped, char operator) { } - ///// EXPERIMENTAL + /// // EXPERIMENTAL String haplotypeName; From fac0c02836e9c4ee2775bbc173d1038a5ce19513 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Sat, 15 Feb 2025 12:02:54 -0800 Subject: [PATCH 112/130] Track hub refinements --- .../igv/feature/genome/load/GenomeConfig.java | 4 ++++ .../feature/genome/load/HubGenomeLoader.java | 9 ++++++++ .../org/broad/igv/track/TrackMenuUtils.java | 5 +++- .../org/broad/igv/ucsc/hub/HubParser.java | 6 +++-- .../org/broad/igv/ucsc/hub/TrackDbHub.java | 10 +++++--- .../igv/ucsc/hub/TrackHubSelectionDialog.java | 23 +++++++++++++++---- .../java/org/broad/igv/ui/IGVMenuBar.java | 5 ++-- 7 files changed, 50 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/broad/igv/feature/genome/load/GenomeConfig.java b/src/main/java/org/broad/igv/feature/genome/load/GenomeConfig.java index 4b97b3f8c0..ddd931ebbd 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/GenomeConfig.java +++ b/src/main/java/org/broad/igv/feature/genome/load/GenomeConfig.java @@ -72,6 +72,10 @@ public static GenomeConfig fromJson(String json) { public GenomeConfig() { } + public String toJson() { + return new Gson().toJson(this); + } + public List getHubs() { return hubs; } diff --git a/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java b/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java index 3f3f82e1f7..cba4d0c75b 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java +++ b/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java @@ -1,5 +1,7 @@ package org.broad.igv.feature.genome.load; +import org.apache.commons.io.FileUtils; +import org.broad.igv.DirectoryManager; import org.broad.igv.Globals; import org.broad.igv.feature.genome.Genome; import org.broad.igv.prefs.PreferencesManager; @@ -9,6 +11,7 @@ import org.broad.igv.ui.IGV; import org.broad.igv.ucsc.hub.TrackHubSelectionDialog; +import java.io.File; import java.io.IOException; import java.util.*; import java.util.stream.Collectors; @@ -93,6 +96,12 @@ else if (IGV.hasInstance() && !Globals.isBatch() && !Globals.isHeadless() && !Gl Genome genome = new Genome(config); genome.setGenomeHub(hub); + + + String genomeJson = config.toJson(); + File dir = DirectoryManager.getGenomeCacheDirectory(); + FileUtils.write(new File(dir, config.getId() + ".json"), genomeJson); + return genome; diff --git a/src/main/java/org/broad/igv/track/TrackMenuUtils.java b/src/main/java/org/broad/igv/track/TrackMenuUtils.java index cfbf1d90db..ad2c757a8d 100644 --- a/src/main/java/org/broad/igv/track/TrackMenuUtils.java +++ b/src/main/java/org/broad/igv/track/TrackMenuUtils.java @@ -1326,7 +1326,10 @@ public static JMenuItem getShowFeatureNames(final Collection selectedTrac boolean currentValue = selectedTracks.stream().allMatch(t -> t.isShowFeatureNames()); String label = currentValue ? "Hide Feature Names" : "Show Feature Names"; JMenuItem item = new JMenuItem(label); - item.addActionListener(evt -> selectedTracks.stream().forEach(t -> t.setShowFeatureNames(!currentValue))); + item.addActionListener(evt -> selectedTracks.stream().forEach(t -> { + t.setShowFeatureNames(!currentValue); + IGV.getInstance().repaint(selectedTracks); + })); return item; } diff --git a/src/main/java/org/broad/igv/ucsc/hub/HubParser.java b/src/main/java/org/broad/igv/ucsc/hub/HubParser.java index 7282ee8da7..e37ef69170 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/HubParser.java +++ b/src/main/java/org/broad/igv/ucsc/hub/HubParser.java @@ -28,8 +28,6 @@ public static Hub loadHub(String url, String genomeId) throws IOException { boolean assemblyHub = genomeId == null; int idx = url.lastIndexOf("/"); - String baseURL = url.substring(0, idx + 1); - String host = getHost(url); // Load stanzas from the hub.txt file, which might be all stanzas if 'useOneFile' is on List stanzas = HubParser.loadStanzas(url); @@ -136,6 +134,10 @@ static List loadStanzas(String url) throws IOException { String line; while ((line = br.readLine()) != null) { + if(line.startsWith("#")) { + continue; + } + int indent = indentLevel(line); int i = line.indexOf(" ", indent); if (i < 0) { diff --git a/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java b/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java index cb86945677..bb8e9fa70c 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java +++ b/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java @@ -13,7 +13,7 @@ public class TrackDbHub { List trackStanzas; List groupStanzas; - static Set supportedTypes = new HashSet(Arrays.asList("bigBed", "bigWig", "bigGenePred", "vcfTabix")); + static Set supportedTypes = new HashSet(Arrays.asList("bigBed", "bigWig", "bigGenePred", "vcfTabix", "refgene")); static Set filterTracks = new HashSet(Arrays.asList("cytoBandIdeo", "assembly", "gap", "gapOverlap", "allGaps", "cpgIslandExtUnmasked", "windowMasker")); @@ -153,8 +153,8 @@ private TrackConfig getTrackConfig(Stanza t) { // Expanded display mode does not work well in IGV desktop for some tracks //config.displayMode = t.displayMode(); - if ("vcfTabix".equals(format)) { - config.setIndexURL(config.getUrl() + ".tbi"); + if(t.hasProperty("bigDataIndex")) { + config.setIndexURL(t.getProperty("bigDataIndex")); } if (t.hasProperty("longLabel") && t.hasProperty("html")) { @@ -173,6 +173,10 @@ private TrackConfig getTrackConfig(Stanza t) { config.setDisplayMode(vizModeMap.get(vizProperty)); } + if(t.hasProperty("maxWindowToDraw")) { + config.setVisibilityWindow(Integer.parseInt(t.getProperty("maxWindowToDraw"))); + } + boolean visibility = t.hasProperty("compositeTrack") ? "on".equals(t.getProperty("compositeTrack")) : !("hide".equals(vizProperty)); diff --git a/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java b/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java index 98d1555595..ae9d21a59a 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java +++ b/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java @@ -25,9 +25,9 @@ * supplied track configurations in place. */ public class TrackHubSelectionDialog extends JDialog { - + private static Logger log = LogManager.getLogger(TrackHubSelectionDialog.class); - + Hub hub; private Map configMap; private ArrayList categoryPanels; @@ -102,7 +102,7 @@ void init(List trackConfigurations) { if (categoryPanels.size() == 1) { categoryPanels.get(0).expand(); } - + JPanel buttonPanel = new JPanel(); ((FlowLayout) buttonPanel.getLayout()).setAlignment(FlowLayout.RIGHT); @@ -162,6 +162,8 @@ private CollapsiblePanel createCategoryPanel(TrackConfigGroup configGroup) { wrapLayout.setAlignment(FlowLayout.LEFT); trackContainer.setLayout(wrapLayout); + int maxWidth = 0; + List selectionBoxes = new ArrayList<>(); boolean isSelected = false; for (TrackConfig trackConfig : configGroup.tracks) { @@ -173,6 +175,8 @@ private CollapsiblePanel createCategoryPanel(TrackConfigGroup configGroup) { String infoLink = trackConfig.getHtml(); JLabel label = new JLabel(trackConfig.getName()); SelectionBox p = new SelectionBox(checkBox, label, infoLink); + selectionBoxes.add(p); + maxWidth = Math.max(maxWidth, p.getPreferredSize().width); String longLabel = trackConfig.getLongLabel(); if (longLabel != null) { @@ -182,6 +186,10 @@ private CollapsiblePanel createCategoryPanel(TrackConfigGroup configGroup) { trackContainer.add(p); } + for(SelectionBox selectionBox : selectionBoxes) { + selectionBox.setPreferredWidth(maxWidth); + } + return new CollapsiblePanel(configGroup.label, trackContainer, isSelected || configGroup.defaultOpen); } @@ -197,6 +205,9 @@ public List getSelectedConfigs() { static class SelectionBox extends JPanel { + int preferredWidth = -1; + private int minWidth; + public SelectionBox(JCheckBox checkBox, JLabel label, String infoLink) { this.setLayout(new BorderLayout()); label.setLabelFor(checkBox); @@ -227,12 +238,16 @@ public void mouseClicked(MouseEvent e) { panel.add(iconLabel); add(panel, BorderLayout.CENTER); } + } + public void setPreferredWidth(int width) { + minWidth = 200; + this.preferredWidth = Math.max(minWidth, width); } @Override public Dimension getPreferredSize() { - return new Dimension(250, 20); + return preferredWidth > 0 ? new Dimension(preferredWidth, 20) : super.getPreferredSize(); } @Override diff --git a/src/main/java/org/broad/igv/ui/IGVMenuBar.java b/src/main/java/org/broad/igv/ui/IGVMenuBar.java index 2f65cd4599..0751393d47 100644 --- a/src/main/java/org/broad/igv/ui/IGVMenuBar.java +++ b/src/main/java/org/broad/igv/ui/IGVMenuBar.java @@ -293,14 +293,15 @@ JMenu createFileMenu(Genome genome) { menuAction.setToolTipText(UIConstants.LOAD_TRACKS_TOOLTIP); menuItems.add(MenuAndToolbarUtils.createMenuItem(menuAction)); - if (genomeId != null && LoadFromServerAction.getNodeURLs(genomeId) != null) { + if (genomeId != null && + genome.getTrackHubs().isEmpty() && + LoadFromServerAction.getNodeURLs(genomeId) != null) { menuAction = new LoadFromServerAction("Load Tracks From IGV Server...", KeyEvent.VK_S, igv); menuAction.setToolTipText(UIConstants.LOAD_SERVER_DATA_TOOLTIP); JMenuItem loadTracksFromServerMenuItem = MenuAndToolbarUtils.createMenuItem(menuAction); menuItems.add(loadTracksFromServerMenuItem); } - recentFilesMenu = new RecentUrlsMenu(); menuItems.add(recentFilesMenu); From 0c11f188e66f1af7de1512380240a64716715238 Mon Sep 17 00:00:00 2001 From: Jim Robinson <933148+jrobinso@users.noreply.github.com> Date: Mon, 17 Feb 2025 23:26:37 -0800 Subject: [PATCH 113/130] Track hub load improvements. Add search ability if # of tracks in a category > a threshold (20) --- .../org/broad/igv/encode/DCCEncodeUtils.java | 52 ---- .../igv/encode/EncodeTrackChooserFactory.java | 253 +++++++++++++++ ...{EncodeFileRecord.java => FileRecord.java} | 35 +-- ...odeTrackChooser.java => TrackChooser.java} | 293 +++--------------- ...TableModel.java => TrackChooserModel.java} | 37 ++- .../org/broad/igv/encode/UCSCEncodeUtils.java | 178 ----------- .../org/broad/igv/feature/genome/Genome.java | 2 +- .../feature/genome/load/HubGenomeLoader.java | 12 +- .../igv/feature/genome/load/TrackConfig.java | 11 + .../broad/igv/ucsc/hub/CollapsiblePanel.java | 17 +- .../org/broad/igv/ucsc/hub/TrackDbHub.java | 52 +++- .../igv/ucsc/hub/TrackHubSelectionDialog.java | 157 ++++++++-- src/main/java/org/broad/igv/ui/IGV.java | 32 +- .../java/org/broad/igv/ui/IGVMenuBar.java | 8 +- .../igv/ui/action/BrowseEncodeAction.java | 98 ++++-- .../igv/ui/action/SelectHubTracksAction.java | 122 +++++--- .../broad/igv/ucsc/hub/TrackDbHubTest.java | 29 ++ 17 files changed, 730 insertions(+), 658 deletions(-) delete mode 100644 src/main/java/org/broad/igv/encode/DCCEncodeUtils.java create mode 100644 src/main/java/org/broad/igv/encode/EncodeTrackChooserFactory.java rename src/main/java/org/broad/igv/encode/{EncodeFileRecord.java => FileRecord.java} (66%) rename src/main/java/org/broad/igv/encode/{EncodeTrackChooser.java => TrackChooser.java} (52%) rename src/main/java/org/broad/igv/encode/{EncodeTableModel.java => TrackChooserModel.java} (77%) delete mode 100644 src/main/java/org/broad/igv/encode/UCSCEncodeUtils.java create mode 100644 src/test/java/org/broad/igv/ucsc/hub/TrackDbHubTest.java diff --git a/src/main/java/org/broad/igv/encode/DCCEncodeUtils.java b/src/main/java/org/broad/igv/encode/DCCEncodeUtils.java deleted file mode 100644 index 5ea688d9e2..0000000000 --- a/src/main/java/org/broad/igv/encode/DCCEncodeUtils.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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.encode; - -/** - * Created by jrobinso on 6/4/15. - */ -public class DCCEncodeUtils { - - /* - var query2 = "https://www.encodeproject.org/search/?" + - "type=experiment&" + - // "assembly=hg19&" + - "files.output_type=peaks&" + - "files.file_format=bed&" + - "format=json&" + - "field=lab.title&" + - "field=biosample_term_name&" + - "field=assay_term_name&" + - "field=target.label&" + - "field=files.file_format&" + - "field=files.output_type&" + - "field=files.href&" + - "field=files.replicate.technical_replicate_number&" + - "field=files.replicate.biological_replicate_number&" + - "field=files.assembly&" + - "limit=all"; - */ -} diff --git a/src/main/java/org/broad/igv/encode/EncodeTrackChooserFactory.java b/src/main/java/org/broad/igv/encode/EncodeTrackChooserFactory.java new file mode 100644 index 0000000000..1d35673c1e --- /dev/null +++ b/src/main/java/org/broad/igv/encode/EncodeTrackChooserFactory.java @@ -0,0 +1,253 @@ +/* + * 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. + */ + +/* + * Created by JFormDesigner on Thu Oct 31 22:31:02 EDT 2013 + */ + +package org.broad.igv.encode; + +import java.awt.*; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.*; +import java.util.List; +import java.util.stream.Collectors; +import javax.swing.*; +import javax.swing.text.NumberFormatter; + +import org.broad.igv.logging.*; +import org.broad.igv.Globals; +import org.broad.igv.prefs.Constants; +import org.broad.igv.prefs.PreferencesManager; +import org.broad.igv.ui.IGV; +import org.broad.igv.ui.action.BrowseEncodeAction; +import org.broad.igv.util.Pair; +import org.broad.igv.util.ParsingUtils; + +/** + * @author Jim Robinson + */ +public class EncodeTrackChooserFactory { + + private static Logger log = LogManager.getLogger(EncodeTrackChooserFactory.class); + + private static Map instanceMap = Collections.synchronizedMap(new HashMap<>()); + private static NumberFormatter numberFormatter = new NumberFormatter(); + + private static String ENCODE_HOST = "https://www.encodeproject.org"; + private static Set filteredColumns = new HashSet(Arrays.asList("ID", "Assembly", "HREF", "path")); + + private static List filteredExtensions = Arrays.asList("tsv", "tsv.gz"); + + private static Map speciesNames = Map.of( + "ce10", "Caenorhabditis elegans", + "ce11", "Caenorhabditis elegans", + "dm3", "Drosophila melanogaster", + "dm6", "Drosophila melanogaster", + "GRCh38", "Homo sapiens", + "hg19", "Homo sapiens", + "mm10", "Mus musculus", + "mm9", "Mus musculus" + ); + + static HashSet ucscSupportedGenomes = new HashSet<>(Arrays.asList("hg19", "mm9")); + static HashSet supportedGenomes = new HashSet<>( + Arrays.asList("ce10", "ce11", "dm3", "dm6", "GRCh38", "hg19", "mm10", "mm9")); + + /** + * Return a new or cached instance of a track chooser for the given genome and type. + * + * @param genomeId + * @param type + * @return + * @throws IOException + */ + public synchronized static TrackChooser getInstance(String genomeId, BrowseEncodeAction.Type type) throws IOException { + + String encodeGenomeId = getEncodeGenomeID(genomeId); + String key = encodeGenomeId + type.toString(); + TrackChooser instance = instanceMap.get(key); + if (instance == null) { + Pair, List> records = getEncodeFileRecords(encodeGenomeId, type); + if (records == null) { + return null; + } + Frame parent = IGV.hasInstance() ? IGV.getInstance().getMainFrame() : null; + final List headings = records.getFirst(); + final List rows = records.getSecond(); + final String title = getDialogTitle(genomeId, type); + instance = new TrackChooser(parent, headings, rows, title); + instanceMap.put(key, instance); + } + + return instance; + } + + private static String getDialogTitle(String genomeId, BrowseEncodeAction.Type type) { + + if (type == BrowseEncodeAction.Type.UCSC) { + return "ENCODE data hosted at UCSC (2012)"; + } else { + switch (type) { + case SIGNALS_CHIP: + return "ENCODE CHiP Seq - Signals"; + case SIGNALS_OTHER: + return "ENCODE Non CHiP Data - Signals"; + default: + return "ENCODE"; + } + } + } + + public static boolean genomeSupportedUCSC(String genomeId) { + return genomeId != null && ucscSupportedGenomes.contains(getEncodeGenomeID(genomeId)); + } + + public static boolean genomeSupported(String genomeId) { + return genomeId != null && supportedGenomes.contains(getEncodeGenomeID(genomeId)); + } + + + private static String getEncodeGenomeID(String genomeId) { + switch (genomeId) { + case "hg38": + case "hg38_1kg": + return "GRCh38"; + case "b37": + case "1kg_v37": + return "hg19"; + default: + return genomeId; + } + + } + + private static Pair, List> getEncodeFileRecords(String genomeId, BrowseEncodeAction.Type type) throws IOException { + + try (InputStream is = getStreamFor(genomeId, type)) { + if (is == null) { + return null; + } + Pair, List> headingRecordPair = parseRecords(is, type, genomeId); + + if (IGV.hasInstance()) { + Set loadedPaths = IGV.getInstance().getDataResourceLocators().stream() + .map(rl -> rl.getPath()) + .collect(Collectors.toSet()); + + for (FileRecord fileRecord : headingRecordPair.getSecond()) { + if (loadedPaths.contains(fileRecord.getPath())) { + fileRecord.setSelected(true); + } + } + } + return headingRecordPair; + } + } + + private static InputStream getStreamFor(String genomeId, BrowseEncodeAction.Type type) throws IOException { + if (type == BrowseEncodeAction.Type.UCSC) { + return EncodeTrackChooserFactory.class.getResourceAsStream("encode." + genomeId + ".txt"); + } else { + String root = PreferencesManager.getPreferences().get(Constants.ENCODE_FILELIST_URL) + genomeId + "."; + String url = null; + switch (type) { + case SIGNALS_CHIP: + url = root + "signals.chip.txt"; + break; + case SIGNALS_OTHER: + url = root + "signals.other.txt"; + break; + case OTHER: + url = root + "other.txt"; + break; + } + if (url == null) { + throw new RuntimeException("Unknown encode data collection type: " + type); + } + return ParsingUtils.openInputStream(url); + } + } + + private static Pair parseRecords(InputStream is, BrowseEncodeAction.Type type, String genomeId) throws IOException { + + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + + String[] headers = Globals.tabPattern.split(reader.readLine()); + + int pathColumn = type == BrowseEncodeAction.Type.UCSC ? 0 : Arrays.asList(headers).indexOf("HREF"); + + List records = new ArrayList<>(20000); + String nextLine; + while ((nextLine = reader.readLine()) != null) { + if (!nextLine.startsWith("#")) { + + String[] tokens = Globals.tabPattern.split(nextLine, -1); + String path = type == BrowseEncodeAction.Type.UCSC ? tokens[pathColumn] : ENCODE_HOST + tokens[pathColumn]; + + if (filteredExtensions.stream().anyMatch(e -> path.endsWith(e))) { + continue; + } + + Map attributes = new LinkedHashMap<>(); + for (int i = 0; i < headers.length; i++) { + String value = i < tokens.length ? tokens[i] : ""; + if (value.length() > 0) { + attributes.put(headers[i], shortenField(value, genomeId)); + } + } + final FileRecord record = new FileRecord(path, attributes); + records.add(record); + + } + } + + List filteredHeaders = Arrays.stream(headers).filter(h -> !filteredColumns.contains(h)).collect(Collectors.toList()); + + return new Pair(filteredHeaders, records); + } + + private static String shortenField(String value, String genomeId) { + String species = speciesNames.get(genomeId); + return species == null ? + value : + value.replace("(" + species + ")", "").replace(species, "").trim(); + } + + + /** + * Main function for testing only + * + * @param args + * @throws IOException + */ + public static void main(String[] args) throws IOException { + getInstance("hg19", BrowseEncodeAction.Type.UCSC).setVisible(true); + } + +} diff --git a/src/main/java/org/broad/igv/encode/EncodeFileRecord.java b/src/main/java/org/broad/igv/encode/FileRecord.java similarity index 66% rename from src/main/java/org/broad/igv/encode/EncodeFileRecord.java rename to src/main/java/org/broad/igv/encode/FileRecord.java index ca95a8d1a2..613a78e750 100644 --- a/src/main/java/org/broad/igv/encode/EncodeFileRecord.java +++ b/src/main/java/org/broad/igv/encode/FileRecord.java @@ -34,14 +34,13 @@ * Date: 10/31/13 * Time: 10:11 PM */ -public class EncodeFileRecord { +public class FileRecord { boolean selected = false; String path; Map attributes; - String trackName; - public EncodeFileRecord(String path, Map attributes) { + public FileRecord(String path, Map attributes) { this.path = path; this.attributes = attributes; } @@ -62,7 +61,9 @@ public String getFileType() { public String getAttributeValue(String name) { String value = attributes.get(name); - if (name.equals("type") && value == null) value = getFileType(); + if (value != null) { + if (name.equals("type") && value == null) value = getFileType(); + } return value; } @@ -74,35 +75,13 @@ public Map getAttributes() { return attributes; } - boolean isSelected() { + public boolean isSelected() { return selected; } - void setSelected(boolean selected) { + public void setSelected(boolean selected) { this.selected = selected; } - /** - * Return a friendly name for the track. Unfortunately it is neccessary to hardcode certain attributes. - * - * @return - */ - public String getTrackName() { - - if (trackName == null) { - StringBuffer sb = new StringBuffer(); - if(attributes.containsKey("cell")) sb.append(attributes.get("cell") + " "); - if(attributes.containsKey("antibody")) sb.append(attributes.get("antibody") + " "); - if(attributes.containsKey("dataType")) sb.append(attributes.get("dataType") + " "); - if(attributes.containsKey("view")) sb.append(attributes.get("view") + " "); - if(attributes.containsKey("replicate")) sb.append("rep " + attributes.get("replicate")); - - trackName = sb.toString().trim(); - if(sb.length() == 0) trackName = (new File(path)).getName(); - } - - return trackName; - - } } diff --git a/src/main/java/org/broad/igv/encode/EncodeTrackChooser.java b/src/main/java/org/broad/igv/encode/TrackChooser.java similarity index 52% rename from src/main/java/org/broad/igv/encode/EncodeTrackChooser.java rename to src/main/java/org/broad/igv/encode/TrackChooser.java index 294ad85456..55424ba8a3 100644 --- a/src/main/java/org/broad/igv/encode/EncodeTrackChooser.java +++ b/src/main/java/org/broad/igv/encode/TrackChooser.java @@ -29,233 +29,54 @@ package org.broad.igv.encode; +import com.jidesoft.swing.JideBoxLayout; +import org.broad.igv.Globals; +import org.broad.igv.logging.LogManager; +import org.broad.igv.logging.Logger; +import org.broad.igv.util.Pair; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.table.TableColumn; +import javax.swing.text.NumberFormatter; import java.awt.*; -import java.awt.event.*; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; +import java.awt.event.ActionEvent; +import java.awt.event.MouseEvent; import java.text.ParseException; -import java.util.*; import java.util.List; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; -import javax.swing.*; -import javax.swing.border.*; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import javax.swing.text.NumberFormatter; - -import com.jidesoft.swing.JideBoxLayout; -import org.broad.igv.logging.*; -import org.broad.igv.Globals; -import org.broad.igv.prefs.Constants; -import org.broad.igv.prefs.PreferencesManager; -import org.broad.igv.ui.IGV; -import org.broad.igv.ui.action.BrowseEncodeAction; -import org.broad.igv.util.FileUtils; -import org.broad.igv.util.Pair; -import org.broad.igv.util.ParsingUtils; -import org.broad.igv.util.ResourceLocator; /** * @author Jim Robinson */ -public class EncodeTrackChooser extends org.broad.igv.ui.IGVDialog { +public class TrackChooser extends org.broad.igv.ui.IGVDialog { - private static Logger log = LogManager.getLogger(EncodeTrackChooser.class); + private static Logger log = LogManager.getLogger(TrackChooser.class); - private static Map instanceMap = Collections.synchronizedMap(new HashMap<>()); private static NumberFormatter numberFormatter = new NumberFormatter(); - private static String ENCODE_HOST = "https://www.encodeproject.org"; - private static Set filteredColumns = new HashSet(Arrays.asList("ID", "Assembly", "HREF", "path")); - - private static List filteredExtensions = Arrays.asList("tsv", "tsv.gz"); - - private static Map speciesNames = Map.of( - "ce10", "Caenorhabditis elegans", - "ce11", "Caenorhabditis elegans", - "dm3", "Drosophila melanogaster", - "dm6", "Drosophila melanogaster", - "GRCh38", "Homo sapiens", - "hg19", "Homo sapiens", - "mm10", "Mus musculus", - "mm9", "Mus musculus" - ); - - static HashSet ucscSupportedGenomes = new HashSet<>(Arrays.asList("hg19", "mm9")); - static HashSet supportedGenomes = new HashSet<>( - Arrays.asList("ce10", "ce11", "dm3", "dm6", "GRCh38", "hg19", "mm10", "mm9")); - - - JTable table; JTextField filterTextField; JLabel rowCountLabel; - EncodeTableModel model; + TrackChooserModel model; private boolean canceled; - /** - * Return a new or cached instance of a track chooser for the given genome and type. - * - * @param genomeId - * @param type - * @return - * @throws IOException - */ - public synchronized static EncodeTrackChooser getInstance(String genomeId, BrowseEncodeAction.Type type) throws IOException { - - String encodeGenomeId = getEncodeGenomeID(genomeId); - String key = encodeGenomeId + type.toString(); - EncodeTrackChooser instance = instanceMap.get(key); - if (instance == null) { - Pair, List> records = getEncodeFileRecords(encodeGenomeId, type); - if (records == null) { - return null; - } - Frame parent = IGV.hasInstance() ? IGV.getInstance().getMainFrame() : null; - final List headings = records.getFirst(); - final List rows = records.getSecond(); - final String title = getDialogTitle(genomeId, type); - instance = new EncodeTrackChooser(parent, new EncodeTableModel(headings, rows), title); - instanceMap.put(key, instance); - } - - return instance; - } - - private static String getDialogTitle(String genomeId, BrowseEncodeAction.Type type) { - - if (type == BrowseEncodeAction.Type.UCSC) { - return "ENCODE data hosted at UCSC (2012)"; - } else { - switch (type) { - case SIGNALS_CHIP: - return "ENCODE CHiP Seq - Signals"; - case SIGNALS_OTHER: - return "ENCODE Non CHiP Data - Signals"; - default: - return "ENCODE"; - } - } - } - - public static boolean genomeSupportedUCSC(String genomeId) { - return genomeId != null && ucscSupportedGenomes.contains(getEncodeGenomeID(genomeId)); - } - - public static boolean genomeSupported(String genomeId) { - return genomeId != null && supportedGenomes.contains(getEncodeGenomeID(genomeId)); - } - - - private static String getEncodeGenomeID(String genomeId) { - switch (genomeId) { - case "hg38": - case "hg38_1kg": - return "GRCh38"; - case "b37": - case "1kg_v37": - return "hg19"; - default: - return genomeId; - } - - } - - private static Pair, List> getEncodeFileRecords(String genomeId, BrowseEncodeAction.Type type) throws IOException { - - try (InputStream is = getStreamFor(genomeId, type)) { - if (is == null) { - return null; - } - return parseRecords(is, type, genomeId); - } - } - - private static InputStream getStreamFor(String genomeId, BrowseEncodeAction.Type type) throws IOException { - if (type == BrowseEncodeAction.Type.UCSC) { - return EncodeTrackChooser.class.getResourceAsStream("encode." + genomeId + ".txt"); - } else { - String root = PreferencesManager.getPreferences().get(Constants.ENCODE_FILELIST_URL) + genomeId + "."; - String url = null; - switch (type) { - case SIGNALS_CHIP: - url = root + "signals.chip.txt"; - break; - case SIGNALS_OTHER: - url = root + "signals.other.txt"; - break; - case OTHER: - url = root + "other.txt"; - break; - } - if (url == null) { - throw new RuntimeException("Unknown encode data collection type: " + type); - } - return ParsingUtils.openInputStream(url); - } - } - - private static Pair parseRecords(InputStream is, BrowseEncodeAction.Type type, String genomeId) throws IOException { - - BufferedReader reader = new BufferedReader(new InputStreamReader(is)); - - String[] headers = Globals.tabPattern.split(reader.readLine()); - - int pathColumn = type == BrowseEncodeAction.Type.UCSC ? 0 : Arrays.asList(headers).indexOf("HREF"); - - List records = new ArrayList<>(20000); - String nextLine; - while ((nextLine = reader.readLine()) != null) { - if (!nextLine.startsWith("#")) { - - String[] tokens = Globals.tabPattern.split(nextLine, -1); - String path = type == BrowseEncodeAction.Type.UCSC ? tokens[pathColumn] : ENCODE_HOST + tokens[pathColumn]; - - if(filteredExtensions.stream().anyMatch(e -> path.endsWith(e))) { - continue; - } - - Map attributes = new LinkedHashMap<>(); - for (int i = 0; i < headers.length; i++) { - String value = i < tokens.length ? tokens[i] : ""; - if (value.length() > 0) { - attributes.put(headers[i], shortenField(value, genomeId)); - } - } - final EncodeFileRecord record = new EncodeFileRecord(path, attributes); - records.add(record); - - } - } - - List filteredHeaders = Arrays.stream(headers).filter(h -> !filteredColumns.contains(h)).collect(Collectors.toList()); - - return new Pair(filteredHeaders, records); - } - - private static String shortenField(String value, String genomeId) { - String species = speciesNames.get(genomeId); - return species == null ? - value : - value.replace("(" + species + ")", "").replace(species, "").trim(); - } - - - private EncodeTrackChooser(Frame owner, EncodeTableModel model, String title) { + public TrackChooser(Frame owner, final List headings, final List rows, String title) { super(owner); setTitle(title); - this.model = model; + this.model = new TrackChooserModel(headings, rows); setModal(true); initComponents(owner); init(model); } - private void init(final EncodeTableModel model) { + private void init(final TrackChooserModel model) { setModal(true); table.setAutoCreateRowSorter(true); @@ -264,12 +85,16 @@ private void init(final EncodeTableModel model) { try { rowCountLabel.setText(numberFormatter.valueToString(table.getRowCount()) + " rows"); } catch (ParseException e) { - + log.error("Error parsing row count", e); } table.setRowSelectionAllowed(false); table.setColumnSelectionAllowed(false); + TableColumn selectColumn = table.getColumnModel().getColumn(0); + selectColumn.setPreferredWidth(25); + selectColumn.setMaxWidth(30); + filterTextField.getDocument().addDocumentListener( new DocumentListener() { public void changedUpdate(DocumentEvent e) { @@ -287,6 +112,14 @@ public void removeUpdate(DocumentEvent e) { } + @Override + public void setVisible(boolean b) { + if (b) { + this.model.updateSelections(); + } + super.setVisible(b); + } + /** * Update the row filter regular expression from the expression in * the text box. @@ -294,7 +127,7 @@ public void removeUpdate(DocumentEvent e) { private void updateFilter() { - RowFilter rf = null; + RowFilter rf = null; //If current expression doesn't parse, don't update. try { rf = new RegexFilter(filterTextField.getText()); @@ -306,7 +139,7 @@ private void updateFilter() { try { rowCountLabel.setText(numberFormatter.valueToString(table.getRowCount()) + " rows"); } catch (ParseException e) { - e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. + log.error("Error parsing row count", e); } } @@ -325,40 +158,17 @@ public boolean isCanceled() { return canceled; } - private Set getLoadedPaths() { - - if (!IGV.hasInstance()) return new HashSet(); - - Collection locators = IGV.getInstance().getDataResourceLocators(); - HashSet loadedPaths = new HashSet(locators.size()); - for (ResourceLocator locator : locators) { - loadedPaths.add(locator.getPath()); - } - return loadedPaths; + public List getSelectedRecords() { + return model.getRecords().stream() + .filter(record -> record.isSelected()) + .collect(Collectors.toUnmodifiableList()); } - /** - * @return the list of VISIBLE selected records. Filtered records are not returned even if record.selected == true - * @throws IOException - */ - public List getSelectedRecords() throws IOException { - - List selectedRecords = new ArrayList(); - List allRecords = model.getRecords(); - - int rowCount = table.getRowCount(); - for (int i = 0; i < rowCount; i++) { - int modelIdx = table.convertRowIndexToModel(i); - EncodeFileRecord record = allRecords.get(modelIdx); - if (record.isSelected()) { - selectedRecords.add(record); - record.setSelected(false); // Prevent loading twice - } - } - - return selectedRecords; + public List getAllRecords() { + return model.getRecords(); } + private class RegexFilter extends RowFilter { List> matchers; @@ -368,7 +178,7 @@ private class RegexFilter extends RowFilter { if (text == null) { throw new IllegalArgumentException("Pattern must be non-null"); } - matchers = new ArrayList>(); + matchers = new ArrayList<>(); String[] tokens = Globals.whitespacePattern.split(text); for (String t : tokens) { // If token contains an = sign apply to specified column only @@ -433,14 +243,13 @@ public boolean include(Entry value) { } - private void initComponents(Frame owner) { // All this to have tool tip text! table = new JTable() { @Override public String getToolTipText(MouseEvent e) { - java.awt.Point p = e.getPoint(); + Point p = e.getPoint(); int rowIndex = rowAtPoint(p); int colIndex = columnAtPoint(p); int realColumnIndex = convertColumnIndexToModel(colIndex); @@ -500,7 +309,7 @@ public String getToolTipText(MouseEvent e) { ((GridBagLayout) buttonBar.getLayout()).columnWeights = new double[]{1.0, 0.0, 0.0}; //---- okButton ---- - JButton okButton = new JButton("Load"); + JButton okButton = new JButton("OK"); getRootPane().setDefaultButton(okButton); okButton.addActionListener(e -> loadButtonActionPerformed(e)); buttonBar.add(okButton, new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0, @@ -525,14 +334,4 @@ public String getToolTipText(MouseEvent e) { } - /** - * Main function for testing only - * - * @param args - * @throws IOException - */ - public static void main(String[] args) throws IOException { - getInstance("hg19", BrowseEncodeAction.Type.UCSC).setVisible(true); - } - } diff --git a/src/main/java/org/broad/igv/encode/EncodeTableModel.java b/src/main/java/org/broad/igv/encode/TrackChooserModel.java similarity index 77% rename from src/main/java/org/broad/igv/encode/EncodeTableModel.java rename to src/main/java/org/broad/igv/encode/TrackChooserModel.java index 4c8fc15a92..ceb775924f 100644 --- a/src/main/java/org/broad/igv/encode/EncodeTableModel.java +++ b/src/main/java/org/broad/igv/encode/TrackChooserModel.java @@ -25,33 +25,36 @@ package org.broad.igv.encode; +import org.broad.igv.ui.IGV; + import javax.swing.table.AbstractTableModel; import javax.swing.table.TableModel; import javax.swing.table.TableRowSorter; import javax.swing.table.TableStringConverter; import java.util.ArrayList; import java.util.List; +import java.util.Set; /** * @author jrobinso - * Date: 10/31/13 - * Time: 10:09 PM + * Date: 10/31/13 + * Time: 10:09 PM */ -public class EncodeTableModel extends AbstractTableModel { +public class TrackChooserModel extends AbstractTableModel { private String[] columnHeadings; - private List records; - private final TableRowSorter sorter; + private List records; + private final TableRowSorter sorter; - public EncodeTableModel(List headings, List records) { + public TrackChooserModel(List headings, List records) { this.records = records; List tmp = new ArrayList(); tmp.add(""); // Checkbox heading - for(String h : headings) { + for (String h : headings) { String heading = h.trim(); - if(heading.length() > 0 && !"path".equals(heading)) { + if (heading.length() > 0 && !"path".equals(heading)) { tmp.add(heading); } } @@ -69,7 +72,7 @@ public String toString(TableModel model, int row, int column) { }); } - public TableRowSorter getSorter() { + public TableRowSorter getSorter() { return sorter; } @@ -100,7 +103,7 @@ public Object getValueAt(int rowIndex, int columnIndex) { return null; } - EncodeFileRecord record = records.get(rowIndex); + FileRecord record = records.get(rowIndex); if (columnIndex == 0) { return record.isSelected(); } else { @@ -117,15 +120,25 @@ public boolean isCellEditable(int rowIndex, int columnIndex) { @Override public void setValueAt(Object value, int row, int col) { - if(col == 0) { + if (col == 0) { records.get(row).setSelected((Boolean) value); } fireTableCellUpdated(row, col); } + public void updateSelections() { + + Set loadedPaths = IGV.getInstance().getLoadedPaths(); + for (int row = 0; row < records.size(); row++) { + FileRecord record = records.get(row); + if (loadedPaths.contains(record.getPath())) { + record.setSelected(true); + } + } + } - public List getRecords() { + public List getRecords() { return records; } } \ No newline at end of file diff --git a/src/main/java/org/broad/igv/encode/UCSCEncodeUtils.java b/src/main/java/org/broad/igv/encode/UCSCEncodeUtils.java deleted file mode 100644 index a731770d21..0000000000 --- a/src/main/java/org/broad/igv/encode/UCSCEncodeUtils.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * 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.encode; - -import org.broad.igv.Globals; -import org.broad.igv.util.HttpUtils; -import org.broad.igv.util.ParsingUtils; - -import java.io.*; -import java.util.*; -import java.util.List; - -/** - * @author jrobinso - * Date: 10/31/13 - * Time: 12:16 PM - */ -public class UCSCEncodeUtils { - - private static Set labs = new HashSet<>(); - private static Set dataTypes = new HashSet<>(); - private static Set cells = new HashSet<>(); - private static Set antibodies = new HashSet<>(); - private static Set fileTypes = new HashSet<>(); - private static Set allHeaders = new LinkedHashSet<>(); - - private static List rnaChipQualifiers = Arrays.asList("CellTotal", "Longnonpolya", "Longpolya", - "NucleolusTotal", "ChromatinTotal", "ChromatinTotal", "NucleoplasmTotal"); - - public static void main(String[] args) throws IOException { - - updateEncodeTableFile(args[0], args[1]); - - } - - - static String[] columnHeadings = {"cell", "dataType", "antibody", "view", "replicate", "type", "lab"}; - - private static void updateEncodeTableFile(String inputFile, String outputFile) throws IOException { - - List records = new ArrayList<>(); - - try (BufferedReader reader = ParsingUtils.openBufferedReader(inputFile)) { - String rootPath = reader.readLine(); - - String hub = null; - String nextLine; - while ((nextLine = reader.readLine()) != null) { - - if (nextLine.startsWith("#")) { - hub = nextLine.startsWith("#hub=") ? nextLine.substring(5) : hub; - } else { - String dir = nextLine.equals(".") ? rootPath : rootPath + nextLine; - String filesDotTxt = dir + "/files.txt"; - if (HttpUtils.getInstance().resourceAvailable(filesDotTxt)) { - try { - parseFilesDotTxt(filesDotTxt, records); - } catch (IOException e) { - // e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. - } - } - } - - } - fileTypes.forEach(System.out::println); - - outputRecords(outputFile, records, hub); - } - } - - private static void outputRecords(String outputFile, List records, String hub) throws IOException { - try (PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(outputFile)))) { - StringBuilder sb = new StringBuilder("path"); - for (String h : columnHeadings) { - sb.append("\t").append(h); - } - if (hub != null) { - sb.append("\thub"); - } - pw.println(sb.toString()); - - for (EncodeFileRecord rec : records) { - sb = new StringBuilder(rec.getPath()); - for (String h : columnHeadings) { - sb.append("\t").append(Optional.ofNullable(rec.getAttributeValue(h)).orElse("")); - } - if (hub != null) { - sb.append("\t").append(hub); - } - pw.println(sb.toString()); - } - } - } - - static HashSet knownFileTypes = new HashSet(Arrays.asList( - "bam", "bigBed", "bed", "bb", "bw", "bigWig", "gtf", "broadpeak", "narrowpeak", "gappedpeak", "regionpeak", "gff")); - - public static void parseFilesDotTxt(String url, List fileRecords) throws IOException { - - BufferedReader reader = null; - - reader = ParsingUtils.openBufferedReader(url); - String nextLine; - while ((nextLine = reader.readLine()) != null) { - - String[] tokens = Globals.tabPattern.split(nextLine); - if (tokens.length < 2) continue; - - String fn = tokens[0]; - - String[] attributes = Globals.semicolonPattern.split(tokens[1]); - - LinkedHashMap kvalues = new LinkedHashMap(); - for (String tk : attributes) { - - String[] kv = Globals.equalPattern.split(tk); - if (kv.length > 1) { - kvalues.put(kv[0].trim(), kv[1].trim()); - allHeaders.add(kv[0].trim()); - } - - } - - // Hack for RnaChip -- need this to disambiguate them - if ("RnaChip".equals(kvalues.get("dataType"))) { - for (String qual : rnaChipQualifiers) { - if (fn.contains(qual)) { - kvalues.put("antibody", qual); - } - } - } - - String path = fn.startsWith("http") ? fn : url.replace("files.txt", fn); - - EncodeFileRecord df = new EncodeFileRecord(path, kvalues); - - String ftype = df.getFileType() == null ? null : df.getFileType().toLowerCase(); - if (knownFileTypes.contains(ftype)) { - fileRecords.add(df); - } - - dataTypes.add(df.getAttributeValue("dataType")); - antibodies.add(df.getAttributeValue("antibody")); - cells.add(df.getAttributeValue("cell")); - labs.add(df.getAttributeValue("lab")); - fileTypes.add(df.getFileType()); - - } - - reader.close(); - - } - - -} diff --git a/src/main/java/org/broad/igv/feature/genome/Genome.java b/src/main/java/org/broad/igv/feature/genome/Genome.java index 1da7e3d34d..0a3fe8f83a 100644 --- a/src/main/java/org/broad/igv/feature/genome/Genome.java +++ b/src/main/java/org/broad/igv/feature/genome/Genome.java @@ -232,7 +232,7 @@ public Genome(GenomeConfig config) throws IOException { if(config.getHubs() != null) { for(String hubUrl : config.getHubs()) { try { - trackHubs.add(HubParser.loadHub(hubUrl, getId())); + trackHubs.add(HubParser.loadHub(hubUrl, getUCSCId())); } catch (IOException e) { log.error("Error loading hub", e); } diff --git a/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java b/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java index cba4d0c75b..ac39159649 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java +++ b/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java @@ -85,12 +85,14 @@ else if (IGV.hasInstance() && !Globals.isBatch() && !Globals.isHeadless() && !Gl TrackHubSelectionDialog dlg = new TrackHubSelectionDialog(hub, filteredGroups, IGV.getInstance().getMainFrame()); dlg.setVisible(true); - List selectedTracks = dlg.getSelectedConfigs(); - config.setTracks(selectedTracks); + if(!dlg.isCanceled()) { + List selectedTracks = dlg.getSelectedConfigs(); + config.setTracks(selectedTracks); - // Remember selections in user preferences - List names = selectedTracks.stream().map((trackConfig) -> trackConfig.getName()).toList(); - PreferencesManager.getPreferences().put(key, String.join(",", names)); + // Remember selections in user preferences + List names = selectedTracks.stream().map((trackConfig) -> trackConfig.getName()).toList(); + PreferencesManager.getPreferences().put(key, String.join(",", names)); + } } Genome genome = new Genome(config); diff --git a/src/main/java/org/broad/igv/feature/genome/load/TrackConfig.java b/src/main/java/org/broad/igv/feature/genome/load/TrackConfig.java index e1853cdf63..76317e9a31 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/TrackConfig.java +++ b/src/main/java/org/broad/igv/feature/genome/load/TrackConfig.java @@ -1,5 +1,7 @@ package org.broad.igv.feature.genome.load; +import java.util.Map; + /** * A static json-like object, emulates javascript equivalent. Created to ease port of session code from javascript. */ @@ -34,6 +36,7 @@ public class TrackConfig implements Cloneable { private String panelName; private String stanzaParent; // For supporting track hubs + private Map attributes; public TrackConfig() { } @@ -267,4 +270,12 @@ public void setStanzaParent(String stanzaParent) { protected TrackConfig clone() throws CloneNotSupportedException { return (TrackConfig) super.clone(); } + + public void setAttributes(Map attributes) { + this.attributes = attributes; + } + + public Map getAttributes() { + return attributes; + } } diff --git a/src/main/java/org/broad/igv/ucsc/hub/CollapsiblePanel.java b/src/main/java/org/broad/igv/ucsc/hub/CollapsiblePanel.java index 24371aa2ee..4965faf80b 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/CollapsiblePanel.java +++ b/src/main/java/org/broad/igv/ucsc/hub/CollapsiblePanel.java @@ -9,7 +9,7 @@ public class CollapsiblePanel extends JPanel { - public static final Color HEADER_BG = new Color(180,204,226); + public static final Color HEADER_BG = new Color(180, 204, 226); private final JButton collapseButton; @@ -18,8 +18,12 @@ public class CollapsiblePanel extends JPanel { private ImageIcon openIcon; private ImageIcon closeIcon; - public CollapsiblePanel(String label, JComponent content) { - this(label, content, false); + + + + public void addSearchButton(JComponent searchButton) { + header.add(searchButton, BorderLayout.EAST); + revalidate(); } public CollapsiblePanel(String label, JComponent content, boolean isOpen) { @@ -53,6 +57,8 @@ public CollapsiblePanel(String label, JComponent content, boolean isOpen) { jLabel.setHorizontalAlignment(SwingConstants.CENTER); header.add(jLabel, BorderLayout.CENTER); + + this.add(header, BorderLayout.NORTH); } @@ -67,7 +73,6 @@ public void expand() { content.setVisible(true); } - /** * Constrain the maximum height to prevent BoxLayout from needlessly resizing the panel to fill space. This is * rather hardcoded for the TrackHubSelectionDialog. @@ -78,7 +83,7 @@ public void expand() { @Override public Dimension getMaximumSize() { Dimension d4 = header.getMinimumSize(); - if(!content.isVisible()) { + if (!content.isVisible()) { return new Dimension(Integer.MAX_VALUE, d4.height); } else { Dimension d5 = content.getMinimumSize(); @@ -89,7 +94,7 @@ public Dimension getMaximumSize() { public static void main(String[] args) { JComponent content = new JTextArea("alskdfjalskdjflsdkjfsaldkfjsladkfjsalkfj"); - CollapsiblePanel cp = new CollapsiblePanel("Expand/Collapse", content); + CollapsiblePanel cp = new CollapsiblePanel("Expand/Collapse", content, false); JFrame f = new JFrame("test"); f.setSize(500, 500); diff --git a/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java b/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java index bb8e9fa70c..fdd1c2767e 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java +++ b/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java @@ -1,5 +1,7 @@ package org.broad.igv.ucsc.hub; +import com.google.gson.Gson; +import org.broad.igv.Globals; import org.broad.igv.feature.genome.load.TrackConfig; import org.broad.igv.ui.IGV; @@ -131,6 +133,7 @@ private TrackConfig getTrackConfig(Stanza t) { String format = t.format(); String url = t.getProperty("bigDataUrl"); TrackConfig config = new TrackConfig(url); + config.setFormat(format); config.setPanelName(IGV.DATA_PANEL_NAME); @@ -157,9 +160,7 @@ private TrackConfig getTrackConfig(Stanza t) { config.setIndexURL(t.getProperty("bigDataIndex")); } - if (t.hasProperty("longLabel") && t.hasProperty("html")) { - config.setDescription("" + t.getProperty("longLabel") + "")); - } else if (t.hasProperty("longLabel")) { + if (t.hasProperty("longLabel")) { config.setDescription(t.getProperty("longLabel")); } @@ -174,7 +175,9 @@ private TrackConfig getTrackConfig(Stanza t) { } if(t.hasProperty("maxWindowToDraw")) { - config.setVisibilityWindow(Integer.parseInt(t.getProperty("maxWindowToDraw"))); + long maxWindow = Long.parseLong(t.getProperty("maxWindowToDraw")); + int vizWindow = Math.min(Integer.MAX_VALUE, (int) maxWindow); + config.setVisibilityWindow(vizWindow); } boolean visibility = t.hasProperty("compositeTrack") ? @@ -228,6 +231,10 @@ private TrackConfig getTrackConfig(Stanza t) { if (t.hasProperty("group")) { config.setGroup(t.getProperty("group")); } + if(t.hasProperty("metadata")) { + Map metadata = parseMetadata(t.getProperty("metadata")); + config.setAttributes(metadata); + } if (t.parent != null) { config.setStanzaParent(t.parent.name); @@ -236,4 +243,41 @@ private TrackConfig getTrackConfig(Stanza t) { return config; } + //metadata differentiation="10 hour" treatment=X donor=A lab="List Meta Lab" data_set_id=ucscTest1 access=group assay=long-RNA-seq enriched_in=exon life_stage=postpartum species="Homo sapiens" ucsc_db=hg38 + static Map parseMetadata(String metadata) { + + Map attrs = new HashMap(); + while(metadata.length() > 0) { + int idx = metadata.indexOf("="); + if(idx == -1) { + break; + } + int idx2; + String key = capitalize(metadata.substring(0, idx)); + String value; + + if ('"' == metadata.charAt(idx + 1)) { + idx++; + idx2 = metadata.indexOf('"', idx + 1); + value = metadata.substring(idx + 1, idx2); + idx2++; + } else { + idx2 = metadata.indexOf(" "); + if(idx2 == -1) { + idx2 = metadata.length(); + } + value = metadata.substring(idx + 1, idx2); + } + attrs.put(key, value); + if(idx2 == metadata.length()) { + break; + } + metadata = metadata.substring(idx2 + 1); + } + return attrs; + } + + private static String capitalize(final String line) { + return Character.toUpperCase(line.charAt(0)) + line.substring(1); + } } diff --git a/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java b/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java index ae9d21a59a..effaf3ad27 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java +++ b/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java @@ -1,9 +1,12 @@ package org.broad.igv.ucsc.hub; import org.broad.igv.Globals; +import org.broad.igv.encode.FileRecord; +import org.broad.igv.encode.TrackChooser; import org.broad.igv.feature.genome.load.TrackConfig; import org.broad.igv.logging.LogManager; import org.broad.igv.logging.Logger; +import org.broad.igv.ui.IGV; import org.broad.igv.ui.util.HyperlinkFactory; import org.broad.igv.ui.util.IconFactory; @@ -28,26 +31,28 @@ public class TrackHubSelectionDialog extends JDialog { private static Logger log = LogManager.getLogger(TrackHubSelectionDialog.class); + private final List trackConfigGroups; Hub hub; - private Map configMap; private ArrayList categoryPanels; + boolean canceled = false; - public TrackHubSelectionDialog(Hub hub, List groupedTrackConfigurations, Frame owner) { + + public TrackHubSelectionDialog(Hub hub, List trackConfigGroups, Frame owner) { super(owner); setModal(true); this.hub = hub; - init(groupedTrackConfigurations); + this.trackConfigGroups = trackConfigGroups; + init(trackConfigGroups); setLocationRelativeTo(owner); } - void init(List trackConfigurations) { + void init(List trackConfigGroups) { setTitle(this.hub.getLongLabel()); setSize(new Dimension(1000, 800)); - configMap = new HashMap<>(); categoryPanels = new ArrayList<>(); JPanel mainPanel = new JPanel(); @@ -91,7 +96,7 @@ void init(List trackConfigurations) { mainPanel.add(scrollPane, BorderLayout.CENTER); // Loop through track groups - for (TrackConfigGroup configGroup : trackConfigurations) { + for (TrackConfigGroup configGroup : trackConfigGroups) { categoryContainer.add(Box.createVerticalStrut(10)); CollapsiblePanel categoryPanel = createCategoryPanel(configGroup); categoryContainer.add(categoryPanel); @@ -107,10 +112,16 @@ void init(List trackConfigurations) { ((FlowLayout) buttonPanel.getLayout()).setAlignment(FlowLayout.RIGHT); JButton cancelButton = new JButton("Cancel"); - cancelButton.addActionListener(e -> setVisible(false)); + cancelButton.addActionListener(e -> { + canceled = true; + setVisible(false); + }); JButton okButton = new JButton("OK"); - okButton.addActionListener(e -> okAction()); + okButton.addActionListener(e -> { + canceled = false; + setVisible(false); + }); if (Globals.IS_MAC) { buttonPanel.add(cancelButton); @@ -137,16 +148,9 @@ private JPanel getLabeledHyperlink(String label, String url) { return hubURLPanel; } - private void okAction() { - for (Map.Entry entry : configMap.entrySet()) { - final TrackConfig trackConfig = entry.getValue(); - if (entry.getKey().isSelected()) { - trackConfig.setVisible(true); - } else { - trackConfig.setVisible(false); - } - } - setVisible(false); + + public boolean isCanceled() { + return canceled; } /** @@ -167,14 +171,9 @@ private CollapsiblePanel createCategoryPanel(TrackConfigGroup configGroup) { boolean isSelected = false; for (TrackConfig trackConfig : configGroup.tracks) { - final JCheckBox checkBox = new JCheckBox(); - configMap.put(checkBox, trackConfig); - checkBox.setSelected(trackConfig.getVisible()); isSelected = isSelected || trackConfig.getVisible(); - String infoLink = trackConfig.getHtml(); - JLabel label = new JLabel(trackConfig.getName()); - SelectionBox p = new SelectionBox(checkBox, label, infoLink); + SelectionBox p = new SelectionBox(trackConfig); selectionBoxes.add(p); maxWidth = Math.max(maxWidth, p.getPreferredSize().width); @@ -186,11 +185,77 @@ private CollapsiblePanel createCategoryPanel(TrackConfigGroup configGroup) { trackContainer.add(p); } - for(SelectionBox selectionBox : selectionBoxes) { + for (SelectionBox selectionBox : selectionBoxes) { selectionBox.setPreferredWidth(maxWidth); } - return new CollapsiblePanel(configGroup.label, trackContainer, isSelected || configGroup.defaultOpen); + final CollapsiblePanel collapsiblePanel = new CollapsiblePanel(configGroup.label, trackContainer, isSelected || configGroup.defaultOpen); + + // Add a search button for categories with large numbers of records + + if (configGroup.tracks.size() > 20) { + + JButton searchButton = new JButton("Search"); + + searchButton.addActionListener(e -> { + + collapsiblePanel.expand(); + + Set attributeNames = new LinkedHashSet<>(); + attributeNames.add("Name"); + attributeNames.add("Description"); + attributeNames.add("Format"); + + Map recordSelectionBoxMap = new HashMap<>(); + + List records = new ArrayList<>(); + + for (SelectionBox selectionBox : selectionBoxes) { + TrackConfig trackConfig = selectionBox.getTrackConfig(); + final Map trackConfigAttributes = trackConfig.getAttributes(); + Map attributes = trackConfigAttributes; + if (attributes == null) { + attributes = new LinkedHashMap<>(); + } + attributes.put("Name", trackConfig.getName()); + attributes.put("Description", trackConfig.getDescription()); + attributes.put("Format", trackConfig.getFormat()); + + if(trackConfigAttributes != null) { + attributes.putAll(trackConfigAttributes); + attributeNames.addAll(trackConfigAttributes.keySet()); + } + + final FileRecord record = new FileRecord(trackConfig.getUrl(), attributes); + record.setSelected(trackConfig.getVisible()); + records.add(record); + recordSelectionBoxMap.put(record, selectionBox); + } + + List headings = new ArrayList<>(attributeNames); + + TrackChooser chooser = new TrackChooser( + IGV.getInstance().getMainFrame(), + headings, + records, + "Search " + configGroup.label); + chooser.setSize(this.getSize()); + chooser.setLocationRelativeTo(getOwner()); + chooser.setVisible(true); + + if (!chooser.isCanceled()) { + Set selectedRecords = new HashSet<>(chooser.getSelectedRecords()); + for (Map.Entry entry : recordSelectionBoxMap.entrySet()) { + entry.getValue().setSelected(selectedRecords.contains(entry.getKey())); + } + } + }); + + collapsiblePanel.addSearchButton(searchButton); + } + + + return collapsiblePanel; } /** @@ -199,19 +264,36 @@ private CollapsiblePanel createCategoryPanel(TrackConfigGroup configGroup) { * @return */ public List getSelectedConfigs() { - List selected = configMap.values().stream().filter(trackConfig -> trackConfig.getVisible()).collect(Collectors.toList()); - return selected; + return trackConfigGroups.stream() + .flatMap(group -> group.tracks.stream()) + .filter(trackConfig -> trackConfig.getVisible()) + .collect(Collectors.toList()); } static class SelectionBox extends JPanel { + TrackConfig trackConfig; + private JCheckBox checkbox; int preferredWidth = -1; private int minWidth; - public SelectionBox(JCheckBox checkBox, JLabel label, String infoLink) { + + public SelectionBox(TrackConfig trackConfig) { + this.setLayout(new BorderLayout()); - label.setLabelFor(checkBox); - add(checkBox, BorderLayout.WEST); + this.trackConfig = trackConfig; + + this.checkbox = new JCheckBox(); + checkbox.setSelected(trackConfig.getVisible()); + checkbox.addActionListener(e -> { + trackConfig.setVisible(checkbox.isSelected()); + }); + + JLabel label = new JLabel(trackConfig.getName()); + label.setLabelFor(checkbox); + add(checkbox, BorderLayout.WEST); + + String infoLink = trackConfig.getHtml(); if (infoLink == null || "".equals(infoLink.trim())) { add(label, BorderLayout.CENTER); @@ -259,6 +341,19 @@ public Dimension getMinimumSize() { public Dimension getMaximumSize() { return getPreferredSize(); } + + public void setSelected(boolean selected) { + checkbox.setSelected(selected); + trackConfig.setVisible(selected); + } + + public boolean isSelected() { + return checkbox.isSelected(); + } + + public TrackConfig getTrackConfig() { + return trackConfig; + } } diff --git a/src/main/java/org/broad/igv/ui/IGV.java b/src/main/java/org/broad/igv/ui/IGV.java index ba830bdd58..fe25f126c3 100644 --- a/src/main/java/org/broad/igv/ui/IGV.java +++ b/src/main/java/org/broad/igv/ui/IGV.java @@ -296,6 +296,7 @@ public void windowGainedFocus(WindowEvent windowEvent) { } } + public JRootPane getRootPane() { return rootPane; } @@ -1382,18 +1383,6 @@ public TrackPanel getPanelFor(ResourceLocator locator) { } } - public Set getLoadedTypes() { - Set 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 * @@ -1664,6 +1653,25 @@ public Set getDataResourceLocators() { } + public Set getLoadedTypes() { + Set types = new HashSet<>(); + for (Track t : getAllTracks()) { + if (t != null) { + TrackType type = t.getTrackType(); + types.add(type); + } + } + return types; + } + + + public Set getLoadedPaths() { + return getDataResourceLocators().stream() + .map(rl -> rl.getPath()) + .collect(Collectors.toSet()); + } + + public void setAllTrackHeights(int newHeight) { for (Track track : getAllTracks()) { track.setHeight(newHeight, true); diff --git a/src/main/java/org/broad/igv/ui/IGVMenuBar.java b/src/main/java/org/broad/igv/ui/IGVMenuBar.java index 0751393d47..3c6982550c 100644 --- a/src/main/java/org/broad/igv/ui/IGVMenuBar.java +++ b/src/main/java/org/broad/igv/ui/IGVMenuBar.java @@ -64,7 +64,7 @@ import org.broad.igv.util.BrowserLauncher; import org.broad.igv.util.LongRunningTask; import org.broad.igv.util.blat.BlatClient; -import org.broad.igv.encode.EncodeTrackChooser; +import org.broad.igv.encode.EncodeTrackChooserFactory; import javax.swing.*; import javax.swing.event.MenuEvent; @@ -318,13 +318,13 @@ JMenu createFileMenu(Genome genome) { } // ENCODE items. These will be hidden / shown depending on genome chosen - if (EncodeTrackChooser.genomeSupportedUCSC(genomeId) || EncodeTrackChooser.genomeSupported(genomeId)) { + if (EncodeTrackChooserFactory.genomeSupportedUCSC(genomeId) || EncodeTrackChooserFactory.genomeSupported(genomeId)) { JSeparator separator = new JSeparator(); menuItems.add(separator); // Post 2012 ENCODE menu - if (EncodeTrackChooser.genomeSupported(genomeId)) { + if (EncodeTrackChooserFactory.genomeSupported(genomeId)) { JMenuItem chipItem = new JMenuItem(); chipItem.setAction(new BrowseEncodeAction("ENCODE ChIP Signals ...", 0, BrowseEncodeAction.Type.SIGNALS_CHIP, igv)); menuItems.add(chipItem); @@ -339,7 +339,7 @@ JMenu createFileMenu(Genome genome) { } // UCSC hosted ENCODE menu. - if (EncodeTrackChooser.genomeSupportedUCSC(genomeId)) { + if (EncodeTrackChooserFactory.genomeSupportedUCSC(genomeId)) { JMenuItem encodeUCSCMenuItem = MenuAndToolbarUtils.createMenuItem( new BrowseEncodeAction("ENCODE 2012 UCSC Repository ...", KeyEvent.VK_E, BrowseEncodeAction.Type.UCSC, igv)); menuItems.add(encodeUCSCMenuItem); diff --git a/src/main/java/org/broad/igv/ui/action/BrowseEncodeAction.java b/src/main/java/org/broad/igv/ui/action/BrowseEncodeAction.java index c5eb6c64dc..3afda18867 100644 --- a/src/main/java/org/broad/igv/ui/action/BrowseEncodeAction.java +++ b/src/main/java/org/broad/igv/ui/action/BrowseEncodeAction.java @@ -25,24 +25,27 @@ package org.broad.igv.ui.action; +import org.broad.igv.encode.TrackChooser; import org.broad.igv.feature.genome.GenomeManager; +import org.broad.igv.feature.genome.load.TrackConfig; import org.broad.igv.logging.*; import org.broad.igv.feature.genome.Genome; import org.broad.igv.track.AttributeManager; +import org.broad.igv.track.Track; +import org.broad.igv.ucsc.hub.TrackConfigGroup; import org.broad.igv.ui.IGV; import org.broad.igv.ui.WaitCursorManager; import org.broad.igv.ui.util.MessageUtils; import org.broad.igv.util.ResourceLocator; -import org.broad.igv.encode.EncodeTrackChooser; -import org.broad.igv.encode.EncodeFileRecord; +import org.broad.igv.encode.EncodeTrackChooserFactory; +import org.broad.igv.encode.FileRecord; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; -import java.io.IOException; +import java.io.File; import java.util.*; import java.util.List; -import java.util.concurrent.ExecutionException; /** * @author jrobinso @@ -95,17 +98,17 @@ public void actionPerformed(ActionEvent event) { Genome genome = GenomeManager.getInstance().getCurrentGenome(); final WaitCursorManager.CursorToken token = WaitCursorManager.showWaitCursor(); - SwingWorker worker = new SwingWorker() { + SwingWorker worker = new SwingWorker() { @Override - protected EncodeTrackChooser doInBackground() throws Exception { - return EncodeTrackChooser.getInstance(genome.getId(), BrowseEncodeAction.this.type); + protected TrackChooser doInBackground() throws Exception { + return EncodeTrackChooserFactory.getInstance(genome.getId(), BrowseEncodeAction.this.type); } @Override protected void done() { WaitCursorManager.removeWaitCursor(token); try { - EncodeTrackChooser chooser = get(); + TrackChooser chooser = get(); if (chooser == null) { MessageUtils.showMessage("Encode data is not available for " + genome.getDisplayName() + " through IGV."); return; @@ -114,44 +117,77 @@ protected void done() { chooser.setVisible(true); if (chooser.isCanceled()) return; - java.util.List records = chooser.getSelectedRecords(); - if (records.size() > 0) { - List locators = new ArrayList<>(records.size()); - for (EncodeFileRecord record : records) { - ResourceLocator rl = new ResourceLocator(record.getPath()); - rl.setName(record.getTrackName()); + java.util.List records = chooser.getAllRecords(); - Map attributes = record.getAttributes(); + final List loadedTracks = IGV.getInstance().getAllTracks().stream().filter(t -> t.getResourceLocator() != null).toList(); + final Set loadedTrackPaths = new HashSet<>(loadedTracks.stream().map(t -> t.getResourceLocator().getPath()).toList()); + final Set trackPathsToRemove = new HashSet<>(); + final List tracksToLoad = new ArrayList<>(records.size()); - String antibody = attributes.containsKey("antibody") ? attributes.get("antibody") : attributes.get("Target"); - if (antibody != null) { - rl.setColor(colors.get(antibody.toUpperCase())); + for (FileRecord record : records) { + if (record.isSelected()) { + if (!loadedTrackPaths.contains(record.getPath())) { + tracksToLoad.add(getResourceLocator(record)); } - - for (Map.Entry entry : attributes.entrySet()) { - String value = entry.getValue(); - if (value != null && value.length() > 0 && sampleInfoAttributes.contains(entry.getKey())) { - AttributeManager.getInstance().addAttribute(rl.getName(), entry.getKey(), value); - } - } - - rl.setMetadata(attributes); - - locators.add(rl); + } else { + trackPathsToRemove.add(record.getPath()); } - igv.loadTracks(locators); } + List tracksToRemove = loadedTracks.stream().filter(t -> trackPathsToRemove.contains(t.getResourceLocator().getPath())).toList(); + igv.deleteTracks(tracksToRemove); + + igv.loadTracks(tracksToLoad); + } catch (Exception e) { log.error("Error opening Encode browser", e); throw new RuntimeException(e); } } }; - worker.execute(); + } + + private ResourceLocator getResourceLocator(FileRecord record) { + ResourceLocator rl = new ResourceLocator(record.getPath()); + rl.setName(getTrackName(record)); + Map attributes = record.getAttributes(); + String antibody = attributes.containsKey("antibody") ? attributes.get("antibody") : attributes.get("Target"); + if (antibody != null) { + rl.setColor(colors.get(antibody.toUpperCase())); + } + for (Map.Entry entry : attributes.entrySet()) { + String value = entry.getValue(); + if (value != null && value.length() > 0 && sampleInfoAttributes.contains(entry.getKey())) { + AttributeManager.getInstance().addAttribute(rl.getName(), entry.getKey(), value); + } + } + rl.setMetadata(attributes); + return rl; + } + /** + * Return a friendly name for the track. Unfortunately it is neccessary to hardcode certain attributes. + * + * @return + */ + public String getTrackName(FileRecord record) { + + Map attributes = record.getAttributes(); + StringBuffer sb = new StringBuffer(); + if (attributes.containsKey("cell")) sb.append(attributes.get("cell") + " "); + if (attributes.containsKey("antibody")) sb.append(attributes.get("antibody") + " "); + if (attributes.containsKey("dataType")) sb.append(attributes.get("dataType") + " "); + if (attributes.containsKey("view")) sb.append(attributes.get("view") + " "); + if (attributes.containsKey("replicate")) sb.append("rep " + attributes.get("replicate")); + + String trackName = sb.toString().trim(); + if (sb.length() == 0) { + trackName = (new File(record.getPath())).getName(); + } + + return trackName; } } diff --git a/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java b/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java index 33755e2596..3d828a9e27 100644 --- a/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java +++ b/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java @@ -40,9 +40,11 @@ import org.broad.igv.ucsc.hub.TrackConfigGroup; import org.broad.igv.ucsc.hub.TrackHubSelectionDialog; import org.broad.igv.ui.IGV; +import org.broad.igv.ui.WaitCursorManager; import org.broad.igv.ui.util.MessageUtils; import org.broad.igv.util.ResourceLocator; +import javax.swing.*; import java.awt.event.ActionEvent; import java.util.*; @@ -75,62 +77,88 @@ public SelectHubTracksAction(String label, IGV mainFrame, Hub hub) { @Override public void actionPerformed(ActionEvent evt) { - try { - Genome genome = GenomeManager.getInstance().getCurrentGenome(); - if (hub == null) { - hub = genome.getGenomeHub(); - if (hub == null) { - // This should not happen - MessageUtils.showMessage("No tracks available for current genome."); - } - } + final WaitCursorManager.CursorToken token = WaitCursorManager.showWaitCursor(); - final List loadedTracks = IGV.getInstance().getAllTracks().stream().filter(t -> t.getResourceLocator() != null).toList(); - Set loadedTrackPaths = new HashSet<>(loadedTracks.stream().map(t -> t.getResourceLocator().getPath()).toList()); - List groups = hub.getGroupedTrackConfigurations(); - for (TrackConfigGroup g : groups) { - for (TrackConfig config : g.tracks) { - config.setVisible(loadedTrackPaths.contains(config.getUrl())); - } - } + SwingWorker worker = new SwingWorker() { - TrackHubSelectionDialog dlg = new TrackHubSelectionDialog(hub, groups, IGV.getInstance().getMainFrame()); - dlg.setVisible(true); - - // The dialog action will modify the visible state for each track config - Set trackPathsToRemove = new HashSet<>(); - List tracksToLoad = new ArrayList<>(); - List selected = new ArrayList<>(); - for (TrackConfigGroup g : groups) { - for (TrackConfig config : g.tracks) { - if (config.getVisible()) { - selected.add(config); - if (!loadedTrackPaths.contains(config.getUrl())) { - tracksToLoad.add(config); + @Override + protected Object doInBackground() throws Exception { + try { + Genome genome = GenomeManager.getInstance().getCurrentGenome(); + if (hub == null) { + hub = genome.getGenomeHub(); + if (hub == null) { + // This should not happen + MessageUtils.showMessage("No tracks available for current genome."); } - } else { - trackPathsToRemove.add(config.getUrl()); + } + + final List loadedTracks = IGV.getInstance().getAllTracks().stream().filter(t -> t.getResourceLocator() != null).toList(); + Set loadedTrackPaths = new HashSet<>(loadedTracks.stream().map(t -> t.getResourceLocator().getPath()).toList()); + List groups = hub.getGroupedTrackConfigurations(); + for (TrackConfigGroup g : groups) { + for (TrackConfig config : g.tracks) { + config.setVisible(loadedTrackPaths.contains(config.getUrl())); + } + } + + TrackHubSelectionDialog dlg = new TrackHubSelectionDialog(hub, groups, IGV.getInstance().getMainFrame()); + + SwingUtilities.invokeAndWait(() -> dlg.setVisible(true)); + + if (!dlg.isCanceled()) { + + + // The dialog action will modify the visible state for each track config + Set trackPathsToRemove = new HashSet<>(); + List tracksToLoad = new ArrayList<>(); + List selected = new ArrayList<>(); + for (TrackConfigGroup g : groups) { + for (TrackConfig config : g.tracks) { + if (config.getVisible()) { + selected.add(config); + if (!loadedTrackPaths.contains(config.getUrl())) { + tracksToLoad.add(config); + } + } else { + trackPathsToRemove.add(config.getUrl()); + } + } + } + + List tracksToRemove = loadedTracks.stream().filter(t -> trackPathsToRemove.contains(t.getResourceLocator().getPath())).toList(); + IGV.getInstance().deleteTracks(tracksToRemove); + + List locators = tracksToLoad.stream().map(t -> ResourceLocator.fromTrackConfig(t)).toList(); + IGV.getInstance().loadTracks(locators); + + // Update genome + if (updateGenome) { + genome.setAnnotationResources(locators); + // Update preferences + String key = "hub:" + hub.getUrl(); + PreferencesManager.getPreferences().put(key, String.join(",", selected.stream().map(c -> c.getName()).toList())); + } + } + } catch (Exception e) { + log.error("Error loading track configurations", e); + MessageUtils.showMessage(e.getMessage()); + } finally { + if (token != null) { + WaitCursorManager.removeWaitCursor(token); } } + return null; } - List tracksToRemove = loadedTracks.stream().filter(t -> trackPathsToRemove.contains(t.getResourceLocator().getPath())).toList(); - IGV.getInstance().deleteTracks(tracksToRemove); + @Override + protected void done() { + WaitCursorManager.removeWaitCursor(token); + } + }; - List locators = tracksToLoad.stream().map(t -> ResourceLocator.fromTrackConfig(t)).toList(); - IGV.getInstance().loadTracks(locators); + worker.execute(); - // Update genome - if (updateGenome) { - genome.setAnnotationResources(locators); - // Update preferences - String key = "hub:" + hub.getUrl(); - PreferencesManager.getPreferences().put(key, String.join(",", selected.stream().map(c -> c.getName()).toList())); - } - } catch (Exception e) { - log.error("Error loading track configurations", e); - MessageUtils.showMessage(e.getMessage()); - } } } diff --git a/src/test/java/org/broad/igv/ucsc/hub/TrackDbHubTest.java b/src/test/java/org/broad/igv/ucsc/hub/TrackDbHubTest.java new file mode 100644 index 0000000000..5ec76400ab --- /dev/null +++ b/src/test/java/org/broad/igv/ucsc/hub/TrackDbHubTest.java @@ -0,0 +1,29 @@ +package org.broad.igv.ucsc.hub; + +import org.junit.Test; + +import java.util.Map; + +import static org.junit.Assert.*; + +public class TrackDbHubTest { + + //metadata differentiation="10 hour" treatment=X donor=A lab="List Meta Lab" data_set_id=ucscTest1 access=group assay=long-RNA-seq enriched_in=exon life_stage=postpartum species="Homo sapiens" ucsc_db=hg38 + + @Test + public void parseMetadata() { + + String metadata = "differentiation=\"10 hour\" treatment=X donor=A lab=\"List Meta Lab\" data_set_id=ucscTest1 access=group assay=long-RNA-seq enriched_in=exon life_stage=postpartum species=\"Homo sapiens\" ucsc_db=hg38"; + Map metadataMap = TrackDbHub.parseMetadata(metadata); + assertEquals(11, metadataMap.size()); + assertEquals("X", metadataMap.get("Treatment")); + assertEquals("10 hour", metadataMap.get("Differentiation")); + assertEquals("A", metadataMap.get("Donor")); + assertEquals("group", metadataMap.get("Access")); + assertEquals("hg38", metadataMap.get("Ucsc_db")); + assertEquals("Homo sapiens", metadataMap.get("Species")); + assertEquals("postpartum", metadataMap.get("Life_stage")); + assertEquals("long-RNA-seq", metadataMap.get("Assay")); + assertNotNull(metadataMap); + } +} \ No newline at end of file From 9e7dae8da5c24fd9c06c60b2c1a7ef78fd3a2500 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Tue, 18 Feb 2025 11:41:28 -0800 Subject: [PATCH 114/130] Add search button for track hub track selection --- .../org/broad/igv/encode/TrackChooser.java | 4 +- .../broad/igv/encode/TrackChooserModel.java | 3 +- .../broad/igv/ucsc/hub/CollapsiblePanel.java | 21 +-- .../igv/ucsc/hub/TrackHubSelectionDialog.java | 145 +++++++++++------- 4 files changed, 98 insertions(+), 75 deletions(-) diff --git a/src/main/java/org/broad/igv/encode/TrackChooser.java b/src/main/java/org/broad/igv/encode/TrackChooser.java index 55424ba8a3..f396991589 100644 --- a/src/main/java/org/broad/igv/encode/TrackChooser.java +++ b/src/main/java/org/broad/igv/encode/TrackChooser.java @@ -328,8 +328,8 @@ public String getToolTipText(MouseEvent e) { contentPane.add(dialogPane, BorderLayout.CENTER); - Rectangle ownerBounds = owner.getBounds(); - setSize(ownerBounds.width, 620); + int width = owner != null ? owner.getWidth() : 600; + setSize(width, 620); setLocationRelativeTo(getOwner()); } diff --git a/src/main/java/org/broad/igv/encode/TrackChooserModel.java b/src/main/java/org/broad/igv/encode/TrackChooserModel.java index ceb775924f..94917d31f3 100644 --- a/src/main/java/org/broad/igv/encode/TrackChooserModel.java +++ b/src/main/java/org/broad/igv/encode/TrackChooserModel.java @@ -32,6 +32,7 @@ import javax.swing.table.TableRowSorter; import javax.swing.table.TableStringConverter; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Set; @@ -128,7 +129,7 @@ public void setValueAt(Object value, int row, int col) { public void updateSelections() { - Set loadedPaths = IGV.getInstance().getLoadedPaths(); + Set loadedPaths = IGV.hasInstance() ? IGV.getInstance().getLoadedPaths() : Collections.emptySet(); for (int row = 0; row < records.size(); row++) { FileRecord record = records.get(row); diff --git a/src/main/java/org/broad/igv/ucsc/hub/CollapsiblePanel.java b/src/main/java/org/broad/igv/ucsc/hub/CollapsiblePanel.java index 4965faf80b..cedb3ae2f3 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/CollapsiblePanel.java +++ b/src/main/java/org/broad/igv/ucsc/hub/CollapsiblePanel.java @@ -12,20 +12,12 @@ public class CollapsiblePanel extends JPanel { public static final Color HEADER_BG = new Color(180, 204, 226); - private final JButton collapseButton; - private final JComponent content; - private final JPanel header; + private JButton collapseButton; + private JComponent content; + private JPanel header; private ImageIcon openIcon; private ImageIcon closeIcon; - - - - public void addSearchButton(JComponent searchButton) { - header.add(searchButton, BorderLayout.EAST); - revalidate(); - } - public CollapsiblePanel(String label, JComponent content, boolean isOpen) { setLayout(new BorderLayout()); @@ -57,12 +49,15 @@ public CollapsiblePanel(String label, JComponent content, boolean isOpen) { jLabel.setHorizontalAlignment(SwingConstants.CENTER); header.add(jLabel, BorderLayout.CENTER); - - this.add(header, BorderLayout.NORTH); } + public void addSearchButton(JComponent searchButton) { + header.add(searchButton, BorderLayout.EAST); + revalidate(); + } + public void collapse() { collapseButton.setIcon(closeIcon); content.setVisible(false); diff --git a/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java b/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java index effaf3ad27..9c3fb91833 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java +++ b/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java @@ -34,6 +34,7 @@ public class TrackHubSelectionDialog extends JDialog { private final List trackConfigGroups; Hub hub; private ArrayList categoryPanels; + List allSelectionBoxes; boolean canceled = false; @@ -51,9 +52,11 @@ void init(List trackConfigGroups) { setTitle(this.hub.getLongLabel()); - setSize(new Dimension(1000, 800)); + Rectangle ownerBounds = getOwner().getBounds(); + setSize(new Dimension(Math.min(ownerBounds.width, 1200), Math.min(ownerBounds.height, 1000))); categoryPanels = new ArrayList<>(); + allSelectionBoxes = new ArrayList<>(); JPanel mainPanel = new JPanel(); mainPanel.setLayout(new BorderLayout()); @@ -70,20 +73,24 @@ void init(List trackConfigGroups) { // Panel for select all/none JPanel topButtonPanel = new JPanel(); - ((FlowLayout) topButtonPanel.getLayout()).setAlignment(FlowLayout.LEFT); + topButtonPanel.setLayout(new BorderLayout()); + + JPanel expandButtonPanel = new JPanel(); + expandButtonPanel.setLayout(new FlowLayout(FlowLayout.LEFT)); JButton expandAllButton = new JButton("Expand All"); expandAllButton.addActionListener(e -> { categoryPanels.forEach(cp -> cp.expand()); this.revalidate(); }); - topButtonPanel.add(expandAllButton); + expandButtonPanel.add(expandAllButton); JButton collapseAllButton = new JButton("Collapse All"); collapseAllButton.addActionListener(e -> { categoryPanels.forEach(cp -> cp.collapse()); this.revalidate(); }); - topButtonPanel.add(collapseAllButton); + expandButtonPanel.add(collapseAllButton); + topButtonPanel.add(expandButtonPanel, BorderLayout.WEST); topPanel.add(topButtonPanel); mainPanel.add(topPanel, BorderLayout.NORTH); @@ -104,10 +111,23 @@ void init(List trackConfigGroups) { } // If only a single category expand it - if (categoryPanels.size() == 1) { - categoryPanels.get(0).expand(); + + + // If total # of tracks is small expand all + if (allSelectionBoxes.size() < 50) { + for (CollapsiblePanel panel : categoryPanels) { + panel.expand(); + } + } else { + if (categoryPanels.size() == 1) { + categoryPanels.get(0).expand(); + } } + // Search button. + JButton searchButton = createSearchButton("Search " + hub.getShortLabel() , allSelectionBoxes); + topButtonPanel.add(searchButton, BorderLayout.EAST); + JPanel buttonPanel = new JPanel(); ((FlowLayout) buttonPanel.getLayout()).setAlignment(FlowLayout.RIGHT); @@ -123,13 +143,13 @@ void init(List trackConfigGroups) { setVisible(false); }); - if (Globals.IS_MAC) { - buttonPanel.add(cancelButton); - buttonPanel.add(okButton); - } else { +// if (Globals.IS_MAC) { +// buttonPanel.add(cancelButton); +// buttonPanel.add(okButton); +// } else { buttonPanel.add(okButton); buttonPanel.add(cancelButton); - } + //} getRootPane().setDefaultButton(okButton); @@ -175,6 +195,8 @@ private CollapsiblePanel createCategoryPanel(TrackConfigGroup configGroup) { SelectionBox p = new SelectionBox(trackConfig); selectionBoxes.add(p); + allSelectionBoxes.add(p); + maxWidth = Math.max(maxWidth, p.getPreferredSize().width); String longLabel = trackConfig.getLongLabel(); @@ -193,69 +215,74 @@ private CollapsiblePanel createCategoryPanel(TrackConfigGroup configGroup) { // Add a search button for categories with large numbers of records - if (configGroup.tracks.size() > 20) { + if (configGroup.tracks.size() > 2) { + final JButton searchButton = createSearchButton("Search " + configGroup.label, selectionBoxes); + searchButton.addActionListener(e -> collapsiblePanel.expand()); + collapsiblePanel.addSearchButton(searchButton); + } - JButton searchButton = new JButton("Search"); - searchButton.addActionListener(e -> { + return collapsiblePanel; + } - collapsiblePanel.expand(); + private JButton createSearchButton(String label, List selectionBoxes) { - Set attributeNames = new LinkedHashSet<>(); - attributeNames.add("Name"); - attributeNames.add("Description"); - attributeNames.add("Format"); + JButton searchButton = new JButton("Search"); - Map recordSelectionBoxMap = new HashMap<>(); + searchButton.addActionListener(e -> { - List records = new ArrayList<>(); + Set attributeNames = new LinkedHashSet<>(); + attributeNames.add("Name"); + attributeNames.add("Description"); + attributeNames.add("Format"); - for (SelectionBox selectionBox : selectionBoxes) { - TrackConfig trackConfig = selectionBox.getTrackConfig(); - final Map trackConfigAttributes = trackConfig.getAttributes(); - Map attributes = trackConfigAttributes; - if (attributes == null) { - attributes = new LinkedHashMap<>(); - } - attributes.put("Name", trackConfig.getName()); - attributes.put("Description", trackConfig.getDescription()); - attributes.put("Format", trackConfig.getFormat()); + Map recordSelectionBoxMap = new HashMap<>(); - if(trackConfigAttributes != null) { - attributes.putAll(trackConfigAttributes); - attributeNames.addAll(trackConfigAttributes.keySet()); - } + List records = new ArrayList<>(); - final FileRecord record = new FileRecord(trackConfig.getUrl(), attributes); - record.setSelected(trackConfig.getVisible()); - records.add(record); - recordSelectionBoxMap.put(record, selectionBox); + for (SelectionBox selectionBox : selectionBoxes) { + TrackConfig trackConfig = selectionBox.getTrackConfig(); + final Map trackConfigAttributes = trackConfig.getAttributes(); + Map attributes = trackConfigAttributes; + if (attributes == null) { + attributes = new LinkedHashMap<>(); } + attributes.put("Name", trackConfig.getName()); + attributes.put("Description", trackConfig.getDescription()); + attributes.put("Format", trackConfig.getFormat()); - List headings = new ArrayList<>(attributeNames); - - TrackChooser chooser = new TrackChooser( - IGV.getInstance().getMainFrame(), - headings, - records, - "Search " + configGroup.label); - chooser.setSize(this.getSize()); - chooser.setLocationRelativeTo(getOwner()); - chooser.setVisible(true); - - if (!chooser.isCanceled()) { - Set selectedRecords = new HashSet<>(chooser.getSelectedRecords()); - for (Map.Entry entry : recordSelectionBoxMap.entrySet()) { - entry.getValue().setSelected(selectedRecords.contains(entry.getKey())); - } + if (trackConfigAttributes != null) { + attributes.putAll(trackConfigAttributes); + attributeNames.addAll(trackConfigAttributes.keySet()); } - }); - collapsiblePanel.addSearchButton(searchButton); - } + final FileRecord record = new FileRecord(trackConfig.getUrl(), attributes); + record.setSelected(trackConfig.getVisible()); + records.add(record); + recordSelectionBoxMap.put(record, selectionBox); + } + List headings = new ArrayList<>(attributeNames); - return collapsiblePanel; + Frame owner = IGV.hasInstance() ? IGV.getInstance().getMainFrame() : null; + + TrackChooser chooser = new TrackChooser( + owner, + headings, + records, + label); + chooser.setSize(this.getSize()); + chooser.setLocationRelativeTo(getOwner()); + chooser.setVisible(true); + + if (!chooser.isCanceled()) { + Set selectedRecords = new HashSet<>(chooser.getSelectedRecords()); + for (Map.Entry entry : recordSelectionBoxMap.entrySet()) { + entry.getValue().setSelected(selectedRecords.contains(entry.getKey())); + } + } + }); + return searchButton; } /** From 6b8e8bda93c90f88c6c586e7a1f6bb31aef5b5c4 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Tue, 18 Feb 2025 12:59:43 -0800 Subject: [PATCH 115/130] Add track group name to search widget --- .../igv/feature/genome/load/TrackConfig.java | 4 + .../igv/ucsc/hub/TrackHubSelectionDialog.java | 86 +++++++++++-------- 2 files changed, 54 insertions(+), 36 deletions(-) diff --git a/src/main/java/org/broad/igv/feature/genome/load/TrackConfig.java b/src/main/java/org/broad/igv/feature/genome/load/TrackConfig.java index 76317e9a31..4716f23d02 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/TrackConfig.java +++ b/src/main/java/org/broad/igv/feature/genome/load/TrackConfig.java @@ -278,4 +278,8 @@ public void setAttributes(Map attributes) { public Map getAttributes() { return attributes; } + + public void setAttribute(String group, String label) { + this.attributes.put(group, label); + } } diff --git a/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java b/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java index 9c3fb91833..5c706d8e4f 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java +++ b/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java @@ -34,7 +34,7 @@ public class TrackHubSelectionDialog extends JDialog { private final List trackConfigGroups; Hub hub; private ArrayList categoryPanels; - List allSelectionBoxes; + Map> allSelectionBoxes; boolean canceled = false; @@ -56,7 +56,7 @@ void init(List trackConfigGroups) { setSize(new Dimension(Math.min(ownerBounds.width, 1200), Math.min(ownerBounds.height, 1000))); categoryPanels = new ArrayList<>(); - allSelectionBoxes = new ArrayList<>(); + allSelectionBoxes = new LinkedHashMap<>(); JPanel mainPanel = new JPanel(); mainPanel.setLayout(new BorderLayout()); @@ -110,22 +110,19 @@ void init(List trackConfigGroups) { categoryPanels.add(categoryPanel); } - // If only a single category expand it - // If total # of tracks is small expand all if (allSelectionBoxes.size() < 50) { for (CollapsiblePanel panel : categoryPanels) { panel.expand(); } - } else { - if (categoryPanels.size() == 1) { - categoryPanels.get(0).expand(); - } + } else if (categoryPanels.size() == 1) { + categoryPanels.get(0).expand(); } + // Search button. - JButton searchButton = createSearchButton("Search " + hub.getShortLabel() , allSelectionBoxes); + JButton searchButton = createSearchButton("Search " + hub.getShortLabel(), allSelectionBoxes); topButtonPanel.add(searchButton, BorderLayout.EAST); JPanel buttonPanel = new JPanel(); @@ -147,8 +144,8 @@ void init(List trackConfigGroups) { // buttonPanel.add(cancelButton); // buttonPanel.add(okButton); // } else { - buttonPanel.add(okButton); - buttonPanel.add(cancelButton); + buttonPanel.add(okButton); + buttonPanel.add(cancelButton); //} getRootPane().setDefaultButton(okButton); @@ -189,13 +186,16 @@ private CollapsiblePanel createCategoryPanel(TrackConfigGroup configGroup) { int maxWidth = 0; List selectionBoxes = new ArrayList<>(); boolean isSelected = false; + if (allSelectionBoxes.get(configGroup.label) == null) { + allSelectionBoxes.put(configGroup.label, new ArrayList<>()); + } for (TrackConfig trackConfig : configGroup.tracks) { isSelected = isSelected || trackConfig.getVisible(); SelectionBox p = new SelectionBox(trackConfig); selectionBoxes.add(p); - allSelectionBoxes.add(p); + allSelectionBoxes.get(configGroup.label).add(p); maxWidth = Math.max(maxWidth, p.getPreferredSize().width); @@ -215,8 +215,8 @@ private CollapsiblePanel createCategoryPanel(TrackConfigGroup configGroup) { // Add a search button for categories with large numbers of records - if (configGroup.tracks.size() > 2) { - final JButton searchButton = createSearchButton("Search " + configGroup.label, selectionBoxes); + if (configGroup.tracks.size() > 50) { + final JButton searchButton = createSearchButton("Search " + configGroup.label, Map.of(configGroup.name, selectionBoxes)); searchButton.addActionListener(e -> collapsiblePanel.expand()); collapsiblePanel.addSearchButton(searchButton); } @@ -225,41 +225,55 @@ private CollapsiblePanel createCategoryPanel(TrackConfigGroup configGroup) { return collapsiblePanel; } - private JButton createSearchButton(String label, List selectionBoxes) { + private JButton createSearchButton(String label, Map> selectionBoxes) { JButton searchButton = new JButton("Search"); searchButton.addActionListener(e -> { Set attributeNames = new LinkedHashSet<>(); - attributeNames.add("Name"); - attributeNames.add("Description"); - attributeNames.add("Format"); + if(selectionBoxes.size() > 1) { + attributeNames.add("Group"); + attributeNames.add("Name"); + attributeNames.add("Description"); + attributeNames.add("Format"); + } Map recordSelectionBoxMap = new HashMap<>(); List records = new ArrayList<>(); - for (SelectionBox selectionBox : selectionBoxes) { - TrackConfig trackConfig = selectionBox.getTrackConfig(); - final Map trackConfigAttributes = trackConfig.getAttributes(); - Map attributes = trackConfigAttributes; - if (attributes == null) { - attributes = new LinkedHashMap<>(); - } - attributes.put("Name", trackConfig.getName()); - attributes.put("Description", trackConfig.getDescription()); - attributes.put("Format", trackConfig.getFormat()); + for(Map.Entry> entry : selectionBoxes.entrySet()) { - if (trackConfigAttributes != null) { - attributes.putAll(trackConfigAttributes); - attributeNames.addAll(trackConfigAttributes.keySet()); - } + String group = entry.getKey(); + List boxes = entry.getValue(); - final FileRecord record = new FileRecord(trackConfig.getUrl(), attributes); - record.setSelected(trackConfig.getVisible()); - records.add(record); - recordSelectionBoxMap.put(record, selectionBox); + for(SelectionBox selectionBox : boxes) { + + TrackConfig trackConfig = selectionBox.getTrackConfig(); + final Map trackConfigAttributes = trackConfig.getAttributes(); + Map attributes = trackConfigAttributes; + if (attributes == null) { + attributes = new LinkedHashMap<>(); + } + if(selectionBoxes.size() > 1) { + attributes.put("Group", group); + } + attributes.put("Name", trackConfig.getName()); + attributes.put("Description", trackConfig.getDescription()); + attributes.put("Format", trackConfig.getFormat()); + + if (trackConfigAttributes != null) { + attributes.putAll(trackConfigAttributes); + attributeNames.addAll(trackConfigAttributes.keySet()); + } + + final FileRecord record = new FileRecord(trackConfig.getUrl(), attributes); + record.getAttributes().put("Group", group); + record.setSelected(trackConfig.getVisible()); + records.add(record); + recordSelectionBoxMap.put(record, selectionBox); + } } List headings = new ArrayList<>(attributeNames); From 20dea34e7924f61760a694e5ae548400921fb18a Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Wed, 19 Feb 2025 07:44:38 -0800 Subject: [PATCH 116/130] bugs --- .../org/broad/igv/encode/TrackChooser.java | 16 +++++++++++++++- .../igv/ucsc/hub/TrackHubSelectionDialog.java | 19 ++++++++++++------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/broad/igv/encode/TrackChooser.java b/src/main/java/org/broad/igv/encode/TrackChooser.java index f396991589..43a4128c66 100644 --- a/src/main/java/org/broad/igv/encode/TrackChooser.java +++ b/src/main/java/org/broad/igv/encode/TrackChooser.java @@ -39,6 +39,7 @@ import javax.swing.border.EmptyBorder; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; +import javax.swing.table.JTableHeader; import javax.swing.table.TableColumn; import javax.swing.text.NumberFormatter; import java.awt.*; @@ -254,10 +255,23 @@ public String getToolTipText(MouseEvent e) { int colIndex = columnAtPoint(p); int realColumnIndex = convertColumnIndexToModel(colIndex); if (realColumnIndex > 0) { - return getModel().getValueAt(rowIndex, realColumnIndex).toString(); + Object value = table.getValueAt(rowIndex, realColumnIndex); + return value == null ? "" : value.toString(); } return null; } + + @Override + protected JTableHeader createDefaultTableHeader() { + return new JTableHeader(columnModel) { + public String getToolTipText(MouseEvent e) { + java.awt.Point p = e.getPoint(); + int index = columnModel.getColumnIndexAtX(p.x); + int realIndex = columnModel.getColumn(index).getModelIndex(); + return table.getColumnName(realIndex); + } + }; + } }; //======== outer content pane ======== diff --git a/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java b/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java index 5c706d8e4f..bf705dc2fd 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java +++ b/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java @@ -232,23 +232,24 @@ private JButton createSearchButton(String label, Map> searchButton.addActionListener(e -> { Set attributeNames = new LinkedHashSet<>(); - if(selectionBoxes.size() > 1) { + if (selectionBoxes.size() > 1) { attributeNames.add("Group"); - attributeNames.add("Name"); - attributeNames.add("Description"); - attributeNames.add("Format"); } + attributeNames.add("Name"); + attributeNames.add("Description"); + attributeNames.add("Format"); + Map recordSelectionBoxMap = new HashMap<>(); List records = new ArrayList<>(); - for(Map.Entry> entry : selectionBoxes.entrySet()) { + for (Map.Entry> entry : selectionBoxes.entrySet()) { String group = entry.getKey(); List boxes = entry.getValue(); - for(SelectionBox selectionBox : boxes) { + for (SelectionBox selectionBox : boxes) { TrackConfig trackConfig = selectionBox.getTrackConfig(); final Map trackConfigAttributes = trackConfig.getAttributes(); @@ -256,7 +257,7 @@ private JButton createSearchButton(String label, Map> if (attributes == null) { attributes = new LinkedHashMap<>(); } - if(selectionBoxes.size() > 1) { + if (selectionBoxes.size() > 1) { attributes.put("Group", group); } attributes.put("Name", trackConfig.getName()); @@ -277,6 +278,10 @@ private JButton createSearchButton(String label, Map> } List headings = new ArrayList<>(attributeNames); + // Limit # of columns + if(headings.size() > 15) { + headings = headings.subList(0, 15); + } Frame owner = IGV.hasInstance() ? IGV.getInstance().getMainFrame() : null; From 9d0f2469abea23d2b90674897f309c67e7533758 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Wed, 19 Feb 2025 22:45:22 -0800 Subject: [PATCH 117/130] export all packages. Add implied index for vcfTabix file types --- src/main/java/module-info.java | 97 +++++++++++++++++-- .../org/broad/igv/ucsc/hub/TrackDbHub.java | 26 ++--- 2 files changed, 103 insertions(+), 20 deletions(-) diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 8efafeb4e5..6be8932424 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,18 +1,99 @@ module org.igv { + exports biz.source_code.base64Coder; exports org.broad.igv; - exports org.broad.igv.tools; + exports org.broad.igv.ucsc; + exports org.broad.igv.ucsc.twobit; + exports org.broad.igv.ucsc.bb; + exports org.broad.igv.ucsc.bb.codecs; + exports org.broad.igv.ucsc.hub; + exports org.broad.igv.prefs; exports org.broad.igv.ui; + exports org.broad.igv.ui.svg; + exports org.broad.igv.ui.legend; + exports org.broad.igv.ui.supdiagram; + exports org.broad.igv.ui.filefilters; + exports org.broad.igv.ui.panel; + exports org.broad.igv.ui.util; + exports org.broad.igv.ui.util.download; + exports org.broad.igv.ui.dnd; + exports org.broad.igv.ui.color; + exports org.broad.igv.ui.action; + exports org.broad.igv.ui.commandbar; + exports org.broad.igv.ui.table; + exports org.broad.igv.maf; + exports org.broad.igv.renderer; + exports org.broad.igv.tools; + exports org.broad.igv.tools.experimental; + exports org.broad.igv.tools.parsers; + exports org.broad.igv.tools.sort; + exports org.broad.igv.tools.converters; + exports org.broad.igv.tools.motiffinder; + exports org.broad.igv.track; + exports org.broad.igv.oauth; + exports org.broad.igv.sam; + exports org.broad.igv.sam.reader; + exports org.broad.igv.sam.cram; + exports org.broad.igv.sam.mods; + exports org.broad.igv.sam.smrt; + exports org.broad.igv.util; + exports org.broad.igv.util.ftp; + exports org.broad.igv.util.blat; + exports org.broad.igv.util.extview; + exports org.broad.igv.util.liftover; + exports org.broad.igv.util.stream; + exports org.broad.igv.util.converters; + exports org.broad.igv.util.index; + exports org.broad.igv.util.collections; + exports org.broad.igv.util.stats; + exports org.broad.igv.lists; + exports org.broad.igv.repeats; + exports org.broad.igv.bedpe; + exports org.broad.igv.gwas; + exports org.broad.igv.charts; + exports org.broad.igv.sashimi; + exports org.broad.igv.encode; + exports org.broad.igv.tdf; + exports org.broad.igv.exceptions; + exports org.broad.igv.annotations; + exports org.broad.igv.batch; + exports org.broad.igv.ultima; + exports org.broad.igv.ultima.render; + exports org.broad.igv.ultima.annotate; + exports org.broad.igv.variant; + exports org.broad.igv.variant.util; + exports org.broad.igv.variant.vcf; + exports org.broad.igv.variant.New; + exports org.broad.igv.blast; + exports org.broad.igv.aws; + exports org.broad.igv.feature; + exports org.broad.igv.feature.cyto; + exports org.broad.igv.feature.basepair; + exports org.broad.igv.feature.tribble; + exports org.broad.igv.feature.tribble.reader; + exports org.broad.igv.feature.dsi; + exports org.broad.igv.feature.gff; + exports org.broad.igv.feature.bionano; + exports org.broad.igv.feature.sprite; + exports org.broad.igv.feature.aa; + exports org.broad.igv.feature.dranger; + exports org.broad.igv.feature.genome; + exports org.broad.igv.feature.genome.fasta; + exports org.broad.igv.feature.genome.load; + exports org.broad.igv.htsget; + exports org.broad.igv.data; + exports org.broad.igv.data.cufflinks; + exports org.broad.igv.data.seg; + exports org.broad.igv.data.expression; exports org.broad.igv.event; - exports org.broad.igv.jbrowse; exports org.broad.igv.logging; - exports org.broad.igv.util.liftover; - exports org.broad.igv.sam.smrt; - exports org.broad.igv.ui.supdiagram; - exports org.broad.igv.feature.genome.load to com.google.gson; + exports org.broad.igv.jbrowse; + exports org.broad.igv.session; + exports org.broad.igv.session.autosave; + exports slider; + exports com.sanityinc.jargs; + opens org.broad.igv.feature.genome.load to com.google.gson; opens org.broad.igv.feature to com.google.gson; - exports org.broad.igv.ucsc; - exports org.broad.igv.ucsc.hub; requires com.google.common; requires org.apache.commons.compress; diff --git a/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java b/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java index fdd1c2767e..e0bc4c917f 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java +++ b/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java @@ -142,7 +142,7 @@ private TrackConfig getTrackConfig(Stanza t) { // A rather complex workaround for some composite tracks String longLabel = t.getOwnProperty("longLabel"); - if(longLabel == null) { + if (longLabel == null) { String inheritedLongLabel = t.getProperty("longLabel"); longLabel = inheritedLongLabel.length() > config.getName().length() ? inheritedLongLabel : config.getName(); } @@ -153,13 +153,15 @@ private TrackConfig getTrackConfig(Stanza t) { config.setUrl(t.getProperty("bigDataUrl")); - // Expanded display mode does not work well in IGV desktop for some tracks - //config.displayMode = t.displayMode(); - - if(t.hasProperty("bigDataIndex")) { + if (t.hasProperty("bigDataIndex")) { config.setIndexURL(t.getProperty("bigDataIndex")); + } else if (t.format().equals("vcfTabix")) { + config.setIndexURL(t.getProperty("bigDataUrl") + ".tbi"); } + // Expanded display mode does not work well in IGV desktop for some tracks + //config.displayMode = t.displayMode(); + if (t.hasProperty("longLabel")) { config.setDescription(t.getProperty("longLabel")); } @@ -174,7 +176,7 @@ private TrackConfig getTrackConfig(Stanza t) { config.setDisplayMode(vizModeMap.get(vizProperty)); } - if(t.hasProperty("maxWindowToDraw")) { + if (t.hasProperty("maxWindowToDraw")) { long maxWindow = Long.parseLong(t.getProperty("maxWindowToDraw")); int vizWindow = Math.min(Integer.MAX_VALUE, (int) maxWindow); config.setVisibilityWindow(vizWindow); @@ -231,7 +233,7 @@ private TrackConfig getTrackConfig(Stanza t) { if (t.hasProperty("group")) { config.setGroup(t.getProperty("group")); } - if(t.hasProperty("metadata")) { + if (t.hasProperty("metadata")) { Map metadata = parseMetadata(t.getProperty("metadata")); config.setAttributes(metadata); } @@ -246,10 +248,10 @@ private TrackConfig getTrackConfig(Stanza t) { //metadata differentiation="10 hour" treatment=X donor=A lab="List Meta Lab" data_set_id=ucscTest1 access=group assay=long-RNA-seq enriched_in=exon life_stage=postpartum species="Homo sapiens" ucsc_db=hg38 static Map parseMetadata(String metadata) { - Map attrs = new HashMap(); - while(metadata.length() > 0) { + Map attrs = new HashMap(); + while (metadata.length() > 0) { int idx = metadata.indexOf("="); - if(idx == -1) { + if (idx == -1) { break; } int idx2; @@ -263,13 +265,13 @@ static Map parseMetadata(String metadata) { idx2++; } else { idx2 = metadata.indexOf(" "); - if(idx2 == -1) { + if (idx2 == -1) { idx2 = metadata.length(); } value = metadata.substring(idx + 1, idx2); } attrs.put(key, value); - if(idx2 == metadata.length()) { + if (idx2 == metadata.length()) { break; } metadata = metadata.substring(idx2 + 1); From 563642caa850f4de0942ce80d6ae38372259093f Mon Sep 17 00:00:00 2001 From: Jim Robinson <933148+jrobinso@users.noreply.github.com> Date: Tue, 25 Feb 2025 10:25:41 -0800 Subject: [PATCH 118/130] Large track hubs (#1660) * Support for large hubs (e.g. epignome roadmap) * Add count of total & selected tracks to hub category label --- .../org/broad/igv/feature/genome/Genome.java | 2 +- .../feature/genome/load/HubGenomeLoader.java | 8 +- .../broad/igv/ucsc/hub/CollapsiblePanel.java | 28 +- src/main/java/org/broad/igv/ucsc/hub/Hub.java | 4 +- .../org/broad/igv/ucsc/hub/HubParser.java | 20 +- .../java/org/broad/igv/ucsc/hub/Stanza.java | 39 ++- .../igv/ucsc/hub/TrackConfigContainer.java | 77 +++++ .../broad/igv/ucsc/hub/TrackConfigGroup.java | 27 -- .../org/broad/igv/ucsc/hub/TrackDbHub.java | 205 +++++++------ .../igv/ucsc/hub/TrackHubSelectionDialog.java | 278 +++++++++++------- .../java/org/broad/igv/ui/IGVMenuBar.java | 4 +- .../igv/ui/action/BrowseEncodeAction.java | 2 - .../igv/ui/action/LoadFromURLMenuAction.java | 2 +- .../igv/ui/action/SelectHubTracksAction.java | 19 +- .../broad/igv/ui/util/HyperlinkFactory.java | 4 +- .../org/broad/igv/ui/util/IconFactory.java | 6 +- .../java/org/broad/igv/util/StringUtils.java | 2 +- .../java/org/broad/igv/ucsc/hub/HubTest.java | 6 +- .../broad/igv/ucsc/hub/TrackDbHubTest.java | 56 ++++ test/data/hubs/groups_trackDb.txt | 47 +++ test/data/hubs/multiwig_trackDB.txt | 56 ++++ test/data/hubs/subgroups_trackDb.txt | 46 +++ test/data/hubs/supertrack_trackDb.txt | 67 +++++ test/data/hubs/view_trackDb.txt | 68 +++++ 24 files changed, 786 insertions(+), 287 deletions(-) create mode 100644 src/main/java/org/broad/igv/ucsc/hub/TrackConfigContainer.java delete mode 100644 src/main/java/org/broad/igv/ucsc/hub/TrackConfigGroup.java create mode 100644 test/data/hubs/groups_trackDb.txt create mode 100644 test/data/hubs/multiwig_trackDB.txt create mode 100644 test/data/hubs/subgroups_trackDb.txt create mode 100644 test/data/hubs/supertrack_trackDb.txt create mode 100644 test/data/hubs/view_trackDb.txt diff --git a/src/main/java/org/broad/igv/feature/genome/Genome.java b/src/main/java/org/broad/igv/feature/genome/Genome.java index 0a3fe8f83a..ac9bc79382 100644 --- a/src/main/java/org/broad/igv/feature/genome/Genome.java +++ b/src/main/java/org/broad/igv/feature/genome/Genome.java @@ -233,7 +233,7 @@ public Genome(GenomeConfig config) throws IOException { for(String hubUrl : config.getHubs()) { try { trackHubs.add(HubParser.loadHub(hubUrl, getUCSCId())); - } catch (IOException e) { + } catch (Exception e) { log.error("Error loading hub", e); } } diff --git a/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java b/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java index ac39159649..9c1793e2d7 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java +++ b/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java @@ -7,7 +7,7 @@ import org.broad.igv.prefs.PreferencesManager; import org.broad.igv.ucsc.hub.Hub; import org.broad.igv.ucsc.hub.HubParser; -import org.broad.igv.ucsc.hub.TrackConfigGroup; +import org.broad.igv.ucsc.hub.TrackConfigContainer; import org.broad.igv.ui.IGV; import org.broad.igv.ucsc.hub.TrackHubSelectionDialog; @@ -57,7 +57,7 @@ public Genome loadGenome() throws IOException { // Check previous selections for this hub first // TODO -- Maintain track order? String key = "hub:" + this.hubURL; - final List groupedTrackConfigurations = hub.getGroupedTrackConfigurations(); + final List groupedTrackConfigurations = hub.getGroupedTrackConfigurations(); if (PreferencesManager.getPreferences().hasExplicitValue(key)) { Set selectedTrackNames = new HashSet<>(Arrays.asList(PreferencesManager.getPreferences().get(key).split(","))); @@ -72,12 +72,12 @@ public Genome loadGenome() throws IOException { else if (IGV.hasInstance() && !Globals.isBatch() && !Globals.isHeadless() && !Globals.isTesting()) { int count = 0; - for (TrackConfigGroup g : groupedTrackConfigurations) { + for (TrackConfigContainer g : groupedTrackConfigurations) { count += g.tracks.size(); } // If the total # of tracks is >= 20 filter to "Gene" groups, usually a single group - List filteredGroups = count < 20 ? + List filteredGroups = count < 20 ? groupedTrackConfigurations : groupedTrackConfigurations.stream().filter(g -> g.label.startsWith("Gene")).collect(Collectors.toList()); diff --git a/src/main/java/org/broad/igv/ucsc/hub/CollapsiblePanel.java b/src/main/java/org/broad/igv/ucsc/hub/CollapsiblePanel.java index cedb3ae2f3..49748c785c 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/CollapsiblePanel.java +++ b/src/main/java/org/broad/igv/ucsc/hub/CollapsiblePanel.java @@ -10,16 +10,18 @@ public class CollapsiblePanel extends JPanel { public static final Color HEADER_BG = new Color(180, 204, 226); - - - private JButton collapseButton; - private JComponent content; - private JPanel header; + public static final Color HEADER_BG2 = new Color(204, 204, 204); + private final JLabel jlabel; + private JButton collapseButton; + private JComponent content; + private JPanel header; private ImageIcon openIcon; private ImageIcon closeIcon; public CollapsiblePanel(String label, JComponent content, boolean isOpen) { + Color backgroundColor = HEADER_BG; + setLayout(new BorderLayout()); this.content = content; this.openIcon = IconFactory.getInstance().getIcon(IconFactory.IconID.MINUS); @@ -30,7 +32,7 @@ public CollapsiblePanel(String label, JComponent content, boolean isOpen) { header = new JPanel(); header.setLayout(new BorderLayout()); - header.setBackground(HEADER_BG); + header.setBackground(backgroundColor); this.collapseButton = new JButton(); collapseButton.setIcon(isOpen ? openIcon : closeIcon); @@ -40,19 +42,23 @@ public CollapsiblePanel(String label, JComponent content, boolean isOpen) { collapseButton.addActionListener(e -> { collapseButton.setIcon(content.isVisible() ? closeIcon : openIcon); content.setVisible(!content.isVisible()); - this.getParent().revalidate(); + //this.getParent().revalidate(); }); header.add(collapseButton, BorderLayout.WEST); - final JLabel jLabel = new JLabel(label); - jLabel.setFont(FontManager.getFont(14)); - jLabel.setHorizontalAlignment(SwingConstants.CENTER); - header.add(jLabel, BorderLayout.CENTER); + jlabel = new JLabel(label); + jlabel.setFont(FontManager.getFont(14)); + jlabel.setHorizontalAlignment(SwingConstants.CENTER); + header.add(jlabel, BorderLayout.CENTER); this.add(header, BorderLayout.NORTH); } + public void updateLabel(String label) { + jlabel.setText(label); + } + public void addSearchButton(JComponent searchButton) { header.add(searchButton, BorderLayout.EAST); revalidate(); diff --git a/src/main/java/org/broad/igv/ucsc/hub/Hub.java b/src/main/java/org/broad/igv/ucsc/hub/Hub.java index 6aaadc188f..753620e397 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/Hub.java +++ b/src/main/java/org/broad/igv/ucsc/hub/Hub.java @@ -171,7 +171,7 @@ shortLabel Chromosome Band (Ideogram) return config; } - public List getGroupedTrackConfigurations() { + public List getGroupedTrackConfigurations() { if (this.trackHub == null) { try { List trackStanzas = HubParser.loadStanzas(this.trackDbURL); @@ -180,7 +180,7 @@ public List getGroupedTrackConfigurations() { throw new RuntimeException("Error loading track configurations: " + e.getMessage(), e); } } - return trackHub.getGroupedTrackConfigurations(); + return trackHub.getGroupedTrackConfigurations(this.getLongLabel()); } public String getUrl() { diff --git a/src/main/java/org/broad/igv/ucsc/hub/HubParser.java b/src/main/java/org/broad/igv/ucsc/hub/HubParser.java index e37ef69170..f3a6037451 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/HubParser.java +++ b/src/main/java/org/broad/igv/ucsc/hub/HubParser.java @@ -15,9 +15,9 @@ public class HubParser { private static Logger log = LogManager.getLogger(HubParser.class); - private static Set urlProperties = new HashSet<>(Arrays.asList("descriptionUrl","desriptionUrl", + private static Set urlProperties = new HashSet<>(Arrays.asList("descriptionUrl", "desriptionUrl", "twoBitPath", "blat", "chromAliasBb", "twoBitBptURL", "twoBitBptUrl", "htmlPath", "bigDataUrl", - "genomesFile","trackDb","groups", "include", "html", "searchTrix")); + "genomesFile", "trackDb", "groups", "include", "html", "searchTrix")); public static Hub loadAssemblyHub(String url) throws IOException { return loadHub(url, null); @@ -74,7 +74,7 @@ public static Hub loadHub(String url, String genomeId) throws IOException { } // Assembly hub validation - if(assemblyHub) { + if (assemblyHub) { if (!genomeStanza.hasProperty("twoBitPath")) { throw new RuntimeException("Assembly hubs must specify 'twoBitPath'"); } @@ -83,7 +83,6 @@ public static Hub loadHub(String url, String genomeId) throws IOException { } } - if (genomeStanza.hasProperty("groups")) { if (genomeStanza.hasProperty("groups")) { String groupsTxtURL = genomeStanza.getProperty("groups"); @@ -134,7 +133,7 @@ static List loadStanzas(String url) throws IOException { String line; while ((line = br.readLine()) != null) { - if(line.startsWith("#")) { + if (line.startsWith("#")) { continue; } @@ -146,9 +145,14 @@ static List loadStanzas(String url) throws IOException { } else { String key = line.substring(indent, i); if (key.startsWith("#")) continue; - String value = line.substring(i + 1); - if(urlProperties.contains(key) || value.endsWith("URL") || value.endsWith("Url")) { + String value = line.substring(i + 1).trim(); + if (!("shortLabel".equals(key) || "longLabel".equals(key) || "metadata".equals(key))) { + String[] tokens = Globals.whitespacePattern.split(value); + value = tokens[0]; + } + + if (urlProperties.contains(key) || value.endsWith("URL") || value.endsWith("Url")) { value = getDataURL(value, host, baseURL); } @@ -160,7 +164,7 @@ static List loadStanzas(String url) throws IOException { currentNode = newNode; startNewNode = false; } - currentNode.setProperty(key, value); + currentNode.properties.put(key, value); } } } diff --git a/src/main/java/org/broad/igv/ucsc/hub/Stanza.java b/src/main/java/org/broad/igv/ucsc/hub/Stanza.java index dcff273bd6..d1023475ec 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/Stanza.java +++ b/src/main/java/org/broad/igv/ucsc/hub/Stanza.java @@ -7,6 +7,11 @@ class Stanza { private static Set parentOverrideProperties = new HashSet<>(Arrays.asList("visibility", "priority", "group")); + private static Set inheritableProperties = new HashSet<>(Arrays.asList("group", "priority", "color", + "altColor", "autoscale", "viewLimits", "negativeValues", "maxWindowToQuery", "transformFun", + "windowingFunction", "yLineMark", "yLineOnOff", "graphTypeDefault", "interactUp", "interactMultiRegion", + "endsVisible", "maxHeightPixels", "scoreMin", "scoreFilter", "scoreFilterLimits", + "minAliQual", "bamColorTag", "bamColorMode", "bamGrayMode", "colorByStrand", "itemRgb")); final String type; final String name; Stanza parent; @@ -32,21 +37,23 @@ public String getName() { return name; } - void setProperty(String key, String value) { - this.properties.put(key, value); - } - String getOwnProperty(String key) { return this.properties.get(key); } + boolean hasOwnProperty(String key) { + return getProperty(key) != null; + } + String getProperty(String key) { - if (parentOverrideProperties.contains(key) && this.parent != null && this.parent.hasProperty(key)) { + if (this.properties.containsKey("noInherit")) { + return this.properties.get(key); + } else if (parentOverrideProperties.contains(key) && this.parent != null && this.parent.hasProperty(key)) { return this.parent.getProperty(key); } else if (this.properties.containsKey(key)) { return this.properties.get(key); - } else if (this.parent != null) { + } else if (this.parent != null && this.inheritableProperties.contains(key)) { return this.parent.getProperty(key); } else { return null; @@ -54,17 +61,11 @@ String getProperty(String key) { } boolean hasProperty(String key) { - if (this.properties.containsKey(key)) { - return true; - } else if (this.parent != null) { - return this.parent.hasProperty(key); - } else { - return false; - } + return getProperty(key) != null; } String format() { - String type = this.getProperty("type"); + String type = this.getOwnProperty("type"); if (type != null) { // Trim extra bed qualifiers (e.g. bigBed + 4) return firstWord(type); @@ -79,4 +80,14 @@ public Stanza getParent() { public void setParent(Stanza parent) { this.parent = parent; } + + public Stanza getAncestor() { + if (parent != null) { + return parent.getAncestor(); + } else { + return this; + } + } + + } diff --git a/src/main/java/org/broad/igv/ucsc/hub/TrackConfigContainer.java b/src/main/java/org/broad/igv/ucsc/hub/TrackConfigContainer.java new file mode 100644 index 0000000000..033b36a3b4 --- /dev/null +++ b/src/main/java/org/broad/igv/ucsc/hub/TrackConfigContainer.java @@ -0,0 +1,77 @@ +package org.broad.igv.ucsc.hub; + +import org.broad.igv.feature.genome.load.TrackConfig; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +public class TrackConfigContainer { + + public int priority; + public String name; + public String label; + public boolean defaultOpen; + public List tracks; + public List children; + + public TrackConfigContainer(String name, String label, int priority, boolean defaultOpen) { + this.name = name; + this.priority = priority; + this.label = label; + this.defaultOpen = defaultOpen; + this.tracks = new ArrayList<>(); + this.children = new ArrayList<>(); + } + + public boolean isEmpty() { + return tracks.isEmpty() && (children == null || children.isEmpty() || children.stream().allMatch(TrackConfigContainer::isEmpty)); + } + + public void map(Function f) { + for (TrackConfig config : tracks) { + f.apply(config); + } + for (TrackConfigContainer g : children) { + g.map(f); + } + } + + public void findSelectedConfigs(List selectedConfigs) { + for (TrackConfig trackConfig : selectedConfigs) { + if (trackConfig.getVisible() == true) { + selectedConfigs.add(trackConfig); + } + } + for (TrackConfigContainer container : children) { + container.findSelectedConfigs(selectedConfigs); + } + } + + public int countSelectedConfigs() { + int count = 0; + for (TrackConfig trackConfig : tracks) { + if (trackConfig.getVisible() == true) { + count++; + } + } + for (TrackConfigContainer container : children) { + count += container.countSelectedConfigs(); + } + return count; + } + + public void trim() { + children.stream().filter(c -> !c.isEmpty()).forEach(c -> c.trim()); + } + + public void setTrackVisibility(Set loadedTrackPaths) { + for (TrackConfig trackConfig : tracks) { + trackConfig.setVisible(loadedTrackPaths.contains(trackConfig.getUrl())); + } + for (TrackConfigContainer container : children) { + container.setTrackVisibility(loadedTrackPaths); + } + } +} diff --git a/src/main/java/org/broad/igv/ucsc/hub/TrackConfigGroup.java b/src/main/java/org/broad/igv/ucsc/hub/TrackConfigGroup.java deleted file mode 100644 index 72fe9202b8..0000000000 --- a/src/main/java/org/broad/igv/ucsc/hub/TrackConfigGroup.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.broad.igv.ucsc.hub; - -import org.broad.igv.feature.genome.load.TrackConfig; - -import java.util.ArrayList; -import java.util.List; - -public class TrackConfigGroup { - - public int priority; - public String name; - public String label; - public boolean defaultOpen; - public List tracks; - - public TrackConfigGroup(String name, String label, int priority, boolean defaultOpen) { - this.name = name; - this.priority = priority; - this.label = label; - this.defaultOpen = defaultOpen; - this.tracks = new ArrayList<>(); - } - - public boolean isEmpty() { - return tracks.isEmpty(); - } -} diff --git a/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java b/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java index e0bc4c917f..d6d13b442c 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java +++ b/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java @@ -1,9 +1,8 @@ package org.broad.igv.ucsc.hub; -import com.google.gson.Gson; -import org.broad.igv.Globals; import org.broad.igv.feature.genome.load.TrackConfig; import org.broad.igv.ui.IGV; +import org.broad.igv.util.StringUtils; import java.util.*; import java.util.stream.Collectors; @@ -12,8 +11,6 @@ public class TrackDbHub { - List trackStanzas; - List groupStanzas; static Set supportedTypes = new HashSet(Arrays.asList("bigBed", "bigWig", "bigGenePred", "vcfTabix", "refgene")); @@ -26,105 +23,102 @@ public class TrackDbHub { "squish", "SQUISHED", "dense", "COLLAPSED"); + List trackStanzas; + List groupStanzas; + List groupTrackConfigs; + public TrackDbHub(List trackStanzas, List groupStanzas) { - this.groupStanzas = groupStanzas; this.trackStanzas = trackStanzas; } - public List getGroupedTrackConfigurations() { - - // Build map of group objects - Map groupMap = new HashMap<>(); - // Create a group for tracks without a group - groupMap.put("", new TrackConfigGroup("", "", 0, true)); - - if (this.groupStanzas != null) { - for (Stanza groupStanza : this.groupStanzas) { - String name = groupStanza.getProperty("name"); - boolean defaultOpen = "0".equals(groupStanza.getProperty("defaultIsClosed")); - int priority = groupStanza.hasProperty("priority") ? getPriority(groupStanza.getProperty("priority")) : Integer.MAX_VALUE - 1; - groupMap.put(name, new TrackConfigGroup(name, groupStanza.getProperty("label"), priority, defaultOpen)); + public List getGroupedTrackConfigurations(String hubName) { + + if (groupTrackConfigs == null) { + // Build map of group objects + groupTrackConfigs = new ArrayList<>(); + Map trackContainers = new HashMap<>(); + + // Create a group for tracks without a group + TrackConfigContainer nullContainer = new TrackConfigContainer(hubName, hubName, 0, true); + groupTrackConfigs.add(nullContainer); + + if (this.groupStanzas != null) { + for (Stanza groupStanza : this.groupStanzas) { + String name = groupStanza.getProperty("name"); + boolean defaultOpen = "0".equals(groupStanza.getProperty("defaultIsClosed")); + int priority = groupStanza.hasProperty("priority") ? getPriority(groupStanza.getProperty("priority")) : Integer.MAX_VALUE - 1; + final TrackConfigContainer container = new TrackConfigContainer(name, groupStanza.getProperty("label"), priority, defaultOpen); + trackContainers.put(name, container); + groupTrackConfigs.add(container); + } } - } - - // Build map of stanzas to resolve parents - Map trackStanzaMap = new HashMap<>(); - for (Stanza s : this.trackStanzas) { - trackStanzaMap.put(s.getProperty("track"), s); - } - // Initialized cache of track containers. Use linked hashmap to maintain insertion order - LinkedHashMap parentCache = new LinkedHashMap<>(); + for (Stanza s : trackStanzas) { + + boolean isContainer = s.hasOwnProperty("superTrack") || + s.hasOwnProperty("compositeTrack") || + s.hasOwnProperty("view") || + (s.hasOwnProperty("container") && s.getOwnProperty("container").equals("multiWig")); + + // Find parent, if any. "group" containers can be implicit, all other types should be explicitly + // defined before their children + TrackConfigContainer parent = null; + if (s.hasProperty("group")) { + String groupName = s.getProperty("group"); + if (trackContainers.containsKey(groupName)) { + parent = trackContainers.get(groupName); + } else { + // Group not found, just append to name + } + } - // Tracks - final List trackConfigs = this.getTrackConfigs(stanza -> !filterTracks.contains(stanza.name)); - for (TrackConfig c : trackConfigs) { - String groupName = c.getGroup() != null ? c.getGroup() : ""; + if (parent == null && s.hasOwnProperty("parent")) { + parent = trackContainers.get(s.getOwnProperty("parent")); + } - // Some heads (at least one, the washu epigenomics hub) reference groups that are not defined. - if (!groupMap.containsKey(groupName)) { - groupMap.put(groupName, new TrackConfigGroup(groupName, groupName, Integer.MAX_VALUE, false)); - } - final TrackConfigGroup trackConfigGroup = groupMap.get(groupName); - int priority = trackConfigGroup.priority; - trackConfigGroup.tracks.add(c); - - String parentName = c.getStanzaParent(); - if (parentName != null && trackStanzaMap.containsKey(parentName)) { - // Create a contingent container, will be used if a sufficient # of tracks belong to this container - TrackConfigGroup container = parentCache.get(parentName); - if (container == null) { - Stanza s = trackStanzaMap.get(parentName); - String label = trackConfigGroup.label.equals("") ? - s.getProperty("shortLabel") : - trackConfigGroup.label + " - " + s.getProperty("shortLabel"); - int p = s.hasProperty("priority") ? getPriority(s.getProperty("priority")) : priority + 1; - container = new TrackConfigGroup(parentName, label, p, false); - parentCache.put(parentName, container); + if (isContainer) { + + String name = s.getProperty("track"); + int priority = s.hasProperty("priority") ? getPriority(s.getProperty("priority")) : Integer.MAX_VALUE - 1; + final TrackConfigContainer container = new TrackConfigContainer(name, s.getProperty("shortLabel"), priority, false); + if (trackContainers.containsKey(name)) { + throw new RuntimeException("Duplicate track container: " + name); + } + trackContainers.put(name, container); + + if (parent == null || s.hasOwnProperty("superTrack")) { + // No parent or a superTrack => promote to top level + groupTrackConfigs.add(container); + } else { + parent.children.add(container); + } + + } else if (!filterTracks.contains(s.name) && + s.hasProperty("bigDataUrl") && + supportedTypes.contains(s.format())) { + + final TrackConfig trackConfig = getTrackConfig(s); + if (parent != null) { + parent.tracks.add(trackConfig); + } else { + nullContainer.tracks.add(trackConfig); + } } - container.tracks.add(c); } - } - // Flatten the track groups into a list. Remove empty groups. - List groupedTrackConfigurations = groupMap.values().stream() - .filter(g -> !g.isEmpty()).collect(Collectors.toList()); - - // Promote contingent embedded track groups (e.g. composite tracks) to top level group if # of tracks > threshold - // Member tracks must also be removed from existing top level categories - List tmp = new ArrayList<>(); - Set toRemove = new HashSet<>(); - for (TrackConfigGroup parent : parentCache.values()) { - if (parent.tracks.size() > 5) { - tmp.add(parent); - toRemove.addAll(parent.tracks); + // Filter empty groups and sort + for (TrackConfigContainer c : groupTrackConfigs) { + c.trim(); } - } - if (toRemove.size() > 0) { - for (TrackConfigGroup g : groupedTrackConfigurations) { - g.tracks = g.tracks.stream().filter(t -> !toRemove.contains(t)).collect(Collectors.toList()); - } - } + groupTrackConfigs = groupTrackConfigs.stream().filter(t -> !t.isEmpty()).collect(Collectors.toList()); - // Filter empty groups - groupedTrackConfigurations = groupedTrackConfigurations.stream().filter(t -> t.tracks.size() > 0).collect(Collectors.toList()); - groupedTrackConfigurations.addAll(tmp); - Collections.sort(groupedTrackConfigurations, Comparator.comparingInt(o -> o.priority)); - return groupedTrackConfigurations; - } - /** - * Return an array of igv track config objects that satisfy the filter - */ - List getTrackConfigs(java.util.function.Function filter) { - return this.trackStanzas.stream().filter(t -> { - return supportedTypes.contains(t.format()) && t.hasProperty("bigDataUrl") && (filter == null || filter.apply(t)); - }) - .map(t -> this.getTrackConfig(t)) - .toList(); + Collections.sort(groupTrackConfigs, Comparator.comparingInt(o -> o.priority)); + } + return groupTrackConfigs; } @@ -144,7 +138,7 @@ private TrackConfig getTrackConfig(Stanza t) { String longLabel = t.getOwnProperty("longLabel"); if (longLabel == null) { String inheritedLongLabel = t.getProperty("longLabel"); - longLabel = inheritedLongLabel.length() > config.getName().length() ? inheritedLongLabel : config.getName(); + longLabel = inheritedLongLabel != null && inheritedLongLabel.length() > config.getName().length() ? inheritedLongLabel : config.getName(); } config.setLongLabel(longLabel); @@ -171,10 +165,10 @@ private TrackConfig getTrackConfig(Stanza t) { } String vizProperty = t.getProperty("visibility"); - if (vizProperty != null && vizModeMap.containsKey(vizProperty)) { config.setDisplayMode(vizModeMap.get(vizProperty)); } + config.setVisible(vizProperty != null && !("hide".equals(vizProperty))); if (t.hasProperty("maxWindowToDraw")) { long maxWindow = Long.parseLong(t.getProperty("maxWindowToDraw")); @@ -182,11 +176,6 @@ private TrackConfig getTrackConfig(Stanza t) { config.setVisibilityWindow(vizWindow); } - boolean visibility = t.hasProperty("compositeTrack") ? - "on".equals(t.getProperty("compositeTrack")) : - !("hide".equals(vizProperty)); - - config.setVisible(visibility); if (t.hasProperty("autoScale")) { config.setAutoscale(t.getProperty("autoScale").toLowerCase().equals("on")); @@ -239,7 +228,7 @@ private TrackConfig getTrackConfig(Stanza t) { } if (t.parent != null) { - config.setStanzaParent(t.parent.name); + config.setStanzaParent(t.getAncestor().name); } return config; @@ -250,18 +239,19 @@ static Map parseMetadata(String metadata) { Map attrs = new HashMap(); while (metadata.length() > 0) { + int idx = metadata.indexOf("="); if (idx == -1) { break; } int idx2; - String key = capitalize(metadata.substring(0, idx)); + String key = StringUtils.stripQuotes(capitalize(metadata.substring(0, idx))); String value; if ('"' == metadata.charAt(idx + 1)) { idx++; - idx2 = metadata.indexOf('"', idx + 1); - value = metadata.substring(idx + 1, idx2); + idx2 = metadata.indexOf("\" ", idx + 1); + value = idx2 > 0 ? metadata.substring(idx + 1, idx2) : metadata.substring(idx + 1); idx2++; } else { idx2 = metadata.indexOf(" "); @@ -270,11 +260,18 @@ static Map parseMetadata(String metadata) { } value = metadata.substring(idx + 1, idx2); } + value = StringUtils.stripQuotes(value); + if (value.endsWith("\"")) { + value = value.substring(0, value.length() - 1); + } + if (value.startsWith("<") && value.endsWith(">")) { + value = htmlText(value); + } attrs.put(key, value); if (idx2 == metadata.length()) { break; } - metadata = metadata.substring(idx2 + 1); + metadata = idx2 > 0 ? metadata.substring(idx2 + 1) : ""; } return attrs; } @@ -282,4 +279,16 @@ static Map parseMetadata(String metadata) { private static String capitalize(final String line) { return Character.toUpperCase(line.charAt(0)) + line.substring(1); } + + + static String htmlText(String html) { + // Assumes a pattern like Digestive + int idx1 = html.indexOf('>'); + int idx2 = html.indexOf('<', idx1); + if (idx1 > 0 && idx2 > idx1) { + return html.substring(idx1 + 1, idx2); + } else { + return html; + } + } } diff --git a/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java b/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java index bf705dc2fd..e1c43f7ded 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java +++ b/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java @@ -1,6 +1,5 @@ package org.broad.igv.ucsc.hub; -import org.broad.igv.Globals; import org.broad.igv.encode.FileRecord; import org.broad.igv.encode.TrackChooser; import org.broad.igv.feature.genome.load.TrackConfig; @@ -12,6 +11,8 @@ import javax.swing.*; import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.IOException; @@ -20,35 +21,36 @@ import java.util.*; import java.util.List; +import java.util.function.Function; import java.util.stream.Collectors; /** * Dialog to enable selection of tracks defined by track hubs. Modifies the "visible" property of - * supplied track configurations in place. + * supplied track configurations. */ public class TrackHubSelectionDialog extends JDialog { private static Logger log = LogManager.getLogger(TrackHubSelectionDialog.class); - private final List trackConfigGroups; + private final List trackConfigContainers; Hub hub; private ArrayList categoryPanels; - Map> allSelectionBoxes; + List allSelectionBoxes; boolean canceled = false; - public TrackHubSelectionDialog(Hub hub, List trackConfigGroups, Frame owner) { + public TrackHubSelectionDialog(Hub hub, List trackConfigContainers, Frame owner) { super(owner); setModal(true); this.hub = hub; - this.trackConfigGroups = trackConfigGroups; - init(trackConfigGroups); + this.trackConfigContainers = trackConfigContainers; + init(trackConfigContainers); setLocationRelativeTo(owner); } - void init(List trackConfigGroups) { + void init(List trackConfigContainers) { setTitle(this.hub.getLongLabel()); @@ -56,7 +58,7 @@ void init(List trackConfigGroups) { setSize(new Dimension(Math.min(ownerBounds.width, 1200), Math.min(ownerBounds.height, 1000))); categoryPanels = new ArrayList<>(); - allSelectionBoxes = new LinkedHashMap<>(); + allSelectionBoxes = new ArrayList<>(); JPanel mainPanel = new JPanel(); mainPanel.setLayout(new BorderLayout()); @@ -99,31 +101,19 @@ void init(List trackConfigGroups) { JPanel categoryContainer = new JPanel(); categoryContainer.setLayout(new BoxLayout(categoryContainer, BoxLayout.Y_AXIS)); JScrollPane scrollPane = new JScrollPane(categoryContainer); - //scrollPane.setBorder(BorderFactory.createLineBorder(Color.gray)); mainPanel.add(scrollPane, BorderLayout.CENTER); // Loop through track groups - for (TrackConfigGroup configGroup : trackConfigGroups) { + for (TrackConfigContainer configGroup : trackConfigContainers) { categoryContainer.add(Box.createVerticalStrut(10)); CollapsiblePanel categoryPanel = createCategoryPanel(configGroup); categoryContainer.add(categoryPanel); categoryPanels.add(categoryPanel); } - - // If total # of tracks is small expand all - if (allSelectionBoxes.size() < 50) { - for (CollapsiblePanel panel : categoryPanels) { - panel.expand(); - } - } else if (categoryPanels.size() == 1) { - categoryPanels.get(0).expand(); - } - - // Search button. - JButton searchButton = createSearchButton("Search " + hub.getShortLabel(), allSelectionBoxes); - topButtonPanel.add(searchButton, BorderLayout.EAST); + //JButton searchButton = createSearchButton("Search " + hub.getShortLabel(), allSelectionBoxes); + //topButtonPanel.add(searchButton, BorderLayout.EAST); JPanel buttonPanel = new JPanel(); ((FlowLayout) buttonPanel.getLayout()).setAlignment(FlowLayout.RIGHT); @@ -152,7 +142,7 @@ void init(List trackConfigGroups) { mainPanel.add(buttonPanel, BorderLayout.SOUTH); - mainPanel.validate(); + // mainPanel.validate(); } @@ -176,65 +166,108 @@ public boolean isCanceled() { * @param configGroup * @return */ - private CollapsiblePanel createCategoryPanel(TrackConfigGroup configGroup) { + private CollapsiblePanel createCategoryPanel(TrackConfigContainer configGroup) { JPanel trackContainer = new JPanel(); - final WrapLayout wrapLayout = new WrapLayout(); - wrapLayout.setAlignment(FlowLayout.LEFT); - trackContainer.setLayout(wrapLayout); - - int maxWidth = 0; - List selectionBoxes = new ArrayList<>(); - boolean isSelected = false; - if (allSelectionBoxes.get(configGroup.label) == null) { - allSelectionBoxes.put(configGroup.label, new ArrayList<>()); - } - for (TrackConfig trackConfig : configGroup.tracks) { - - isSelected = isSelected || trackConfig.getVisible(); + trackContainer.setLayout(new BoxLayout(trackContainer, BoxLayout.Y_AXIS)); - SelectionBox p = new SelectionBox(trackConfig); - selectionBoxes.add(p); - allSelectionBoxes.get(configGroup.label).add(p); + List selectionBoxes = addSelectionBoxes(null, configGroup, trackContainer); - maxWidth = Math.max(maxWidth, p.getPreferredSize().width); - - String longLabel = trackConfig.getLongLabel(); - if (longLabel != null) { - p.setToolTipText(longLabel); + boolean isSelected = false; + int maxWidth = 0; + int selectionCount = 0; + for (SelectionBox selectionBox : selectionBoxes) { + if (selectionBox.checkbox.isSelected()) { + selectionCount++; + isSelected = true; } - - trackContainer.add(p); + maxWidth = Math.max(maxWidth, selectionBox.getPreferredSize().width); + allSelectionBoxes.add(selectionBox); } for (SelectionBox selectionBox : selectionBoxes) { selectionBox.setPreferredWidth(maxWidth); } - final CollapsiblePanel collapsiblePanel = new CollapsiblePanel(configGroup.label, trackContainer, isSelected || configGroup.defaultOpen); + String label = configGroup.label + " (" + selectionBoxes.size() + " tracks, " + selectionCount + " selected)"; + + final CollapsiblePanel collapsiblePanel = new CollapsiblePanel(label, trackContainer, isSelected || configGroup.defaultOpen); // Add a search button for categories with large numbers of records + final JButton searchButton = createSearchButton("Search " + configGroup.label, selectionBoxes, + (selectedCount) -> { + String l = configGroup.label + " (" + selectionBoxes.size() + " tracks, " + selectedCount + " selected)"; + collapsiblePanel.updateLabel(l); + return null; + }); - if (configGroup.tracks.size() > 50) { - final JButton searchButton = createSearchButton("Search " + configGroup.label, Map.of(configGroup.name, selectionBoxes)); - searchButton.addActionListener(e -> collapsiblePanel.expand()); - collapsiblePanel.addSearchButton(searchButton); - } + collapsiblePanel.addSearchButton(searchButton); + for(SelectionBox selectionBox : selectionBoxes) { + selectionBox.setCallback( b -> { + int selected = configGroup.countSelectedConfigs(); + String l = configGroup.label + " (" + selectionBoxes.size() + " tracks, " + selected + " selected)"; + collapsiblePanel.updateLabel(l); + return null; + + }); + } return collapsiblePanel; } - private JButton createSearchButton(String label, Map> selectionBoxes) { + /** + * Add selection boxes for the container (a group, superTrack, compositeTrack, or view). Return true if any + * tracks are selected. + * + * @param labelPrefix + * @param container + * @param panel + * @return + */ + private List addSelectionBoxes(String labelPrefix, TrackConfigContainer container, JPanel panel) { + + String title = labelPrefix == null ? "" : + labelPrefix + (labelPrefix.length() > 0 ? " - " : "") + container.label; + + List selectionBoxes = new ArrayList<>(); + + if (container.tracks.size() > 0) { + + JPanel trackPanel = new JPanel(); + if (labelPrefix != null) { + trackPanel.setBorder(BorderFactory.createTitledBorder(title)); + } + final WrapLayout wrapLayout = new WrapLayout(); + wrapLayout.setAlignment(FlowLayout.LEFT); + trackPanel.setLayout(wrapLayout); + + for (TrackConfig trackConfig : container.tracks) { + SelectionBox p = new SelectionBox(trackConfig); + trackPanel.add(p); + selectionBoxes.add(p); + } + + panel.add(Box.createVerticalStrut(5)); + panel.add(trackPanel); + + // final CollapsiblePanel collapsiblePanel = new CollapsiblePanel(title, trackPanel, false, CollapsiblePanel.HEADER_BG2); + // trackContainer.add(collapsiblePanel); + } + + for (TrackConfigContainer childChild : container.children) { + selectionBoxes.addAll(addSelectionBoxes(title, childChild, panel)); + } + return selectionBoxes; + } + + private JButton createSearchButton(String label, List selectionBoxes, Function callback) { JButton searchButton = new JButton("Search"); searchButton.addActionListener(e -> { Set attributeNames = new LinkedHashSet<>(); - if (selectionBoxes.size() > 1) { - attributeNames.add("Group"); - } attributeNames.add("Name"); attributeNames.add("Description"); attributeNames.add("Format"); @@ -244,42 +277,34 @@ private JButton createSearchButton(String label, Map> List records = new ArrayList<>(); - for (Map.Entry> entry : selectionBoxes.entrySet()) { - - String group = entry.getKey(); - List boxes = entry.getValue(); + for (SelectionBox selectionBox : selectionBoxes) { - for (SelectionBox selectionBox : boxes) { - - TrackConfig trackConfig = selectionBox.getTrackConfig(); - final Map trackConfigAttributes = trackConfig.getAttributes(); - Map attributes = trackConfigAttributes; - if (attributes == null) { - attributes = new LinkedHashMap<>(); - } - if (selectionBoxes.size() > 1) { - attributes.put("Group", group); - } - attributes.put("Name", trackConfig.getName()); - attributes.put("Description", trackConfig.getDescription()); - attributes.put("Format", trackConfig.getFormat()); + TrackConfig trackConfig = selectionBox.getTrackConfig(); + final Map trackConfigAttributes = trackConfig.getAttributes(); + Map attributes = trackConfigAttributes; + if (attributes == null) { + attributes = new LinkedHashMap<>(); + } - if (trackConfigAttributes != null) { - attributes.putAll(trackConfigAttributes); - attributeNames.addAll(trackConfigAttributes.keySet()); - } + attributes.put("Name", trackConfig.getName()); + attributes.put("Description", trackConfig.getDescription()); + attributes.put("Format", trackConfig.getFormat()); - final FileRecord record = new FileRecord(trackConfig.getUrl(), attributes); - record.getAttributes().put("Group", group); - record.setSelected(trackConfig.getVisible()); - records.add(record); - recordSelectionBoxMap.put(record, selectionBox); + if (trackConfigAttributes != null) { + attributes.putAll(trackConfigAttributes); + attributeNames.addAll(trackConfigAttributes.keySet()); } + + final FileRecord record = new FileRecord(trackConfig.getUrl(), attributes); + record.setSelected(trackConfig.getVisible()); + records.add(record); + recordSelectionBoxMap.put(record, selectionBox); } + List headings = new ArrayList<>(attributeNames); // Limit # of columns - if(headings.size() > 15) { + if (headings.size() > 15) { headings = headings.subList(0, 15); } @@ -299,6 +324,7 @@ private JButton createSearchButton(String label, Map> for (Map.Entry entry : recordSelectionBoxMap.entrySet()) { entry.getValue().setSelected(selectedRecords.contains(entry.getKey())); } + callback.apply(selectedRecords.size()); } }); return searchButton; @@ -310,29 +336,39 @@ private JButton createSearchButton(String label, Map> * @return */ public List getSelectedConfigs() { - return trackConfigGroups.stream() - .flatMap(group -> group.tracks.stream()) - .filter(trackConfig -> trackConfig.getVisible()) - .collect(Collectors.toList()); + + List selectedConfigs = new ArrayList<>(); + for (TrackConfigContainer container : trackConfigContainers) { + container.findSelectedConfigs(selectedConfigs); + } + return selectedConfigs; } static class SelectionBox extends JPanel { TrackConfig trackConfig; - private JCheckBox checkbox; + private CheckBox checkbox; int preferredWidth = -1; private int minWidth; - + Function callback; public SelectionBox(TrackConfig trackConfig) { - this.setLayout(new BorderLayout()); + this.setLayout(new BorderLayout(5, 0)); this.trackConfig = trackConfig; - this.checkbox = new JCheckBox(); + String longLabel = trackConfig.getLongLabel(); + if (longLabel != null) { + this.setToolTipText(longLabel); + } + + this.checkbox = new CheckBox(); checkbox.setSelected(trackConfig.getVisible()); - checkbox.addActionListener(e -> { + checkbox.setActionListener(e -> { trackConfig.setVisible(checkbox.isSelected()); + if (callback != null) { + callback.apply(checkbox.isSelected() ? 1 : 0); + } }); JLabel label = new JLabel(trackConfig.getName()); @@ -393,15 +429,54 @@ public void setSelected(boolean selected) { trackConfig.setVisible(selected); } - public boolean isSelected() { - return checkbox.isSelected(); - } - public TrackConfig getTrackConfig() { return trackConfig; } + + public void setCallback(Function callback) { + this.callback = callback; + } } + static class CheckBox extends JLabel { + + boolean selected = false; + Icon checkedIcon; + Icon uncheckedIcon; + private ActionListener actionListener; + + public CheckBox() { + + this.checkedIcon = IconFactory.getInstance().getIcon(IconFactory.IconID.CHECKBOX); + this.uncheckedIcon = IconFactory.getInstance().getIcon(IconFactory.IconID.CHECKBOX_UNCHECKED); + setIcon(selected ? checkedIcon : uncheckedIcon); + this.setVerticalAlignment(SwingConstants.BOTTOM); + setSize(new Dimension(16, 16)); + + addMouseListener(new MouseAdapter() { + @Override + public void mouseReleased(MouseEvent e) { + setSelected(!selected); + if (actionListener != null) { + actionListener.actionPerformed(new ActionEvent(this, 0, "")); + } + } + }); + } + + public void setSelected(boolean selected) { + this.selected = selected; + setIcon(selected ? checkedIcon : uncheckedIcon); + } + + public boolean isSelected() { + return selected; + } + + public void setActionListener(ActionListener l) { + this.actionListener = l; + } + } /** * main for testing and development @@ -417,9 +492,10 @@ public static void main(String[] args) throws InterruptedException, InvocationTa Hub hub = HubParser.loadHub(hubFile, "hs1"); - List groupedTrackConfigurations = hub.getGroupedTrackConfigurations(); + List groupedTrackConfigurations = hub.getGroupedTrackConfigurations(); final TrackHubSelectionDialog dlf = new TrackHubSelectionDialog(hub, groupedTrackConfigurations, null); + dlf.setSize(new Dimension(800, 600)); dlf.setVisible(true); for (TrackConfig config : dlf.getSelectedConfigs()) { diff --git a/src/main/java/org/broad/igv/ui/IGVMenuBar.java b/src/main/java/org/broad/igv/ui/IGVMenuBar.java index 3c6982550c..0fafd30be6 100644 --- a/src/main/java/org/broad/igv/ui/IGVMenuBar.java +++ b/src/main/java/org/broad/igv/ui/IGVMenuBar.java @@ -285,7 +285,7 @@ JMenu createFileMenu(Genome genome) { menuItems.add(new JSeparator()); // Load menu items - menuAction = new LoadFilesMenuAction("Load Tracks from File...", KeyEvent.VK_L, igv); + menuAction = new LoadFilesMenuAction("Load from File...", KeyEvent.VK_L, igv); menuAction.setToolTipText(UIConstants.LOAD_TRACKS_TOOLTIP); menuItems.add(MenuAndToolbarUtils.createMenuItem(menuAction)); @@ -296,7 +296,7 @@ JMenu createFileMenu(Genome genome) { if (genomeId != null && genome.getTrackHubs().isEmpty() && LoadFromServerAction.getNodeURLs(genomeId) != null) { - menuAction = new LoadFromServerAction("Load Tracks From IGV Server...", KeyEvent.VK_S, igv); + menuAction = new LoadFromServerAction("Load from Server...", KeyEvent.VK_S, igv); menuAction.setToolTipText(UIConstants.LOAD_SERVER_DATA_TOOLTIP); JMenuItem loadTracksFromServerMenuItem = MenuAndToolbarUtils.createMenuItem(menuAction); menuItems.add(loadTracksFromServerMenuItem); diff --git a/src/main/java/org/broad/igv/ui/action/BrowseEncodeAction.java b/src/main/java/org/broad/igv/ui/action/BrowseEncodeAction.java index 3afda18867..6f5f380bab 100644 --- a/src/main/java/org/broad/igv/ui/action/BrowseEncodeAction.java +++ b/src/main/java/org/broad/igv/ui/action/BrowseEncodeAction.java @@ -27,12 +27,10 @@ import org.broad.igv.encode.TrackChooser; import org.broad.igv.feature.genome.GenomeManager; -import org.broad.igv.feature.genome.load.TrackConfig; import org.broad.igv.logging.*; import org.broad.igv.feature.genome.Genome; import org.broad.igv.track.AttributeManager; import org.broad.igv.track.Track; -import org.broad.igv.ucsc.hub.TrackConfigGroup; import org.broad.igv.ui.IGV; import org.broad.igv.ui.WaitCursorManager; import org.broad.igv.ui.util.MessageUtils; diff --git a/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java b/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java index 5bdda10fca..1049ca999f 100644 --- a/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java +++ b/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java @@ -54,7 +54,7 @@ */ public class LoadFromURLMenuAction extends MenuAction { - public static final String LOAD_FROM_URL = "Load Track from URL..."; + public static final String LOAD_FROM_URL = "Load from URL..."; public static final String LOAD_GENOME_FROM_URL = "Load Genome from URL..."; public static final String LOAD_FROM_HTSGET = "Load from htsget Server..."; public static final String LOAD_TRACKHUB = "Load Track Hub..."; diff --git a/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java b/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java index 3d828a9e27..24150c2061 100644 --- a/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java +++ b/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java @@ -37,7 +37,7 @@ import org.broad.igv.prefs.PreferencesManager; import org.broad.igv.track.Track; import org.broad.igv.ucsc.hub.Hub; -import org.broad.igv.ucsc.hub.TrackConfigGroup; +import org.broad.igv.ucsc.hub.TrackConfigContainer; import org.broad.igv.ucsc.hub.TrackHubSelectionDialog; import org.broad.igv.ui.IGV; import org.broad.igv.ui.WaitCursorManager; @@ -95,11 +95,11 @@ protected Object doInBackground() throws Exception { final List loadedTracks = IGV.getInstance().getAllTracks().stream().filter(t -> t.getResourceLocator() != null).toList(); Set loadedTrackPaths = new HashSet<>(loadedTracks.stream().map(t -> t.getResourceLocator().getPath()).toList()); - List groups = hub.getGroupedTrackConfigurations(); - for (TrackConfigGroup g : groups) { - for (TrackConfig config : g.tracks) { - config.setVisible(loadedTrackPaths.contains(config.getUrl())); - } + List groups = hub.getGroupedTrackConfigurations(); + + // Overide visibility -- track is vis + for (TrackConfigContainer g : groups) { + g.setTrackVisibility(loadedTrackPaths); } TrackHubSelectionDialog dlg = new TrackHubSelectionDialog(hub, groups, IGV.getInstance().getMainFrame()); @@ -113,8 +113,8 @@ protected Object doInBackground() throws Exception { Set trackPathsToRemove = new HashSet<>(); List tracksToLoad = new ArrayList<>(); List selected = new ArrayList<>(); - for (TrackConfigGroup g : groups) { - for (TrackConfig config : g.tracks) { + for (TrackConfigContainer g : groups) { + g.map(config -> { if (config.getVisible()) { selected.add(config); if (!loadedTrackPaths.contains(config.getUrl())) { @@ -123,7 +123,8 @@ protected Object doInBackground() throws Exception { } else { trackPathsToRemove.add(config.getUrl()); } - } + return null; + }); } List tracksToRemove = loadedTracks.stream().filter(t -> trackPathsToRemove.contains(t.getResourceLocator().getPath())).toList(); diff --git a/src/main/java/org/broad/igv/ui/util/HyperlinkFactory.java b/src/main/java/org/broad/igv/ui/util/HyperlinkFactory.java index 8175454436..9ee9236d7e 100644 --- a/src/main/java/org/broad/igv/ui/util/HyperlinkFactory.java +++ b/src/main/java/org/broad/igv/ui/util/HyperlinkFactory.java @@ -40,8 +40,8 @@ public void mouseClicked(MouseEvent e) { return hyperLink; } catch (URISyntaxException e) { - log.error("Error creating hyperlink for: " + link, e); - return null; + // Not a url + return new JLabel(link); } } } diff --git a/src/main/java/org/broad/igv/ui/util/IconFactory.java b/src/main/java/org/broad/igv/ui/util/IconFactory.java index 4931b31e39..827cbb649d 100644 --- a/src/main/java/org/broad/igv/ui/util/IconFactory.java +++ b/src/main/java/org/broad/igv/ui/util/IconFactory.java @@ -54,7 +54,9 @@ public enum IconID { CLOSE, PLUS, MINUS, - INFO + INFO, + CHECKBOX, + CHECKBOX_UNCHECKED, } private Map icons; @@ -71,6 +73,8 @@ public static IconFactory getInstance() { private IconFactory() { icons = new HashMap(); + icons.put(IconID.CHECKBOX, createImageIcon("/images/checkbox16.png", "checked")); + icons.put(IconID.CHECKBOX_UNCHECKED, createImageIcon("/images/checkboxUn16.png", "un-checked")); icons.put(IconID.DRAG_AND_DROP, createImageIcon("/images/dragNdrop.png", "drag and drop")); icons.put(IconID.ZOOM, diff --git a/src/main/java/org/broad/igv/util/StringUtils.java b/src/main/java/org/broad/igv/util/StringUtils.java index 6c679bb2f0..970b112286 100644 --- a/src/main/java/org/broad/igv/util/StringUtils.java +++ b/src/main/java/org/broad/igv/util/StringUtils.java @@ -317,7 +317,7 @@ public static int countChar(String string, char c) { * @return */ public static String stripQuotes(String fileString) { - if (isQuoted(fileString)) { + if (fileString.length() > 2 && isQuoted(fileString)) { fileString = fileString.substring(1, fileString.length() - 1); } return fileString; diff --git a/src/test/java/org/broad/igv/ucsc/hub/HubTest.java b/src/test/java/org/broad/igv/ucsc/hub/HubTest.java index b3f0b6b733..cd4dcb58e3 100644 --- a/src/test/java/org/broad/igv/ucsc/hub/HubTest.java +++ b/src/test/java/org/broad/igv/ucsc/hub/HubTest.java @@ -23,7 +23,7 @@ public void testGetGenomeConfig() throws IOException { GenomeConfig genomeConfig = hub.getGenomeConfig(); assertNotNull(genomeConfig); assertEquals("GCF_000186305.1", genomeConfig.getId()); - assertEquals("Python bivittatus (GCF_000186305.1)", genomeConfig.getName()); + assertEquals("Python (GCF_000186305.1)", genomeConfig.getName()); assertNotNull(genomeConfig.getTwoBitBptURL()); assertNotNull(genomeConfig.getTwoBitURL()); assertNotNull(genomeConfig.getChromAliasBbURL()); @@ -34,7 +34,7 @@ public void testGetGenomeConfig() throws IOException { public void testGetGroupedTrackConfigurations() throws IOException { String hubFile = TestUtils.DATA_DIR + "hubs/hub.txt"; Hub hub = HubParser.loadAssemblyHub(hubFile); - List groupedTrackConfigurations = hub.getGroupedTrackConfigurations(); + List groupedTrackConfigurations = hub.getGroupedTrackConfigurations(); assertEquals(5, groupedTrackConfigurations.size()); } @@ -43,7 +43,7 @@ public void testNCBIHostedHub() throws IOException { String hubFile = "https://ftp.ncbi.nlm.nih.gov/snp/population_frequency/TrackHub/latest/hub.txt"; Hub hub = HubParser.loadHub(hubFile, "hg38"); - List groupedTrackConfigurations = hub.getGroupedTrackConfigurations(); + List groupedTrackConfigurations = hub.getGroupedTrackConfigurations(); assertEquals(1, groupedTrackConfigurations.size()); assertEquals(12, groupedTrackConfigurations.get(0).tracks.size()); diff --git a/src/test/java/org/broad/igv/ucsc/hub/TrackDbHubTest.java b/src/test/java/org/broad/igv/ucsc/hub/TrackDbHubTest.java index 5ec76400ab..cac10f99c5 100644 --- a/src/test/java/org/broad/igv/ucsc/hub/TrackDbHubTest.java +++ b/src/test/java/org/broad/igv/ucsc/hub/TrackDbHubTest.java @@ -1,7 +1,11 @@ package org.broad.igv.ucsc.hub; +import org.broad.igv.feature.genome.load.TrackConfig; +import org.broad.igv.util.TestUtils; import org.junit.Test; +import java.io.IOException; +import java.util.List; import java.util.Map; import static org.junit.Assert.*; @@ -26,4 +30,56 @@ public void parseMetadata() { assertEquals("long-RNA-seq", metadataMap.get("Assay")); assertNotNull(metadataMap); } + + // metadata "Epigenome_Mnemonic"="GI.STMC.GAST" "Standardized_Epigenome_name"="Gastric" "EDACC_Epigenome_name"="Gastric" "Group"="Digestive" "Age"="34Y" "Lab"="UCSD" "Sex"="Male" "Anatomy"="GI_STOMACH" "EID"="E094" "Type"="PrimaryTissue" "Order"="100" "Ethnicity"="Caucasian" + + @Test + public void parseMetadata2() { + + String metadata = "\"Epigenome_Mnemonic\"=\"GI.STMC.GAST\" \"Standardized_Epigenome_name\"=\"Gastric\" \"EDACC_Epigenome_name\"=\"Gastric\" \"Group\"=\"Digestive\" \"Age\"=\"34Y\" \"Lab\"=\"UCSD\" \"Sex\"=\"Male\" \"Anatomy\"=\"GI_STOMACH\" \"EID\"=\"E094\" \"Type\"=\"PrimaryTissue\" \"Order\"=\"100\" \"Ethnicity\"=\"Caucasian\""; + Map metadataMap = TrackDbHub.parseMetadata(metadata); + assertEquals("GI.STMC.GAST", metadataMap.get("Epigenome_Mnemonic")); + assertEquals("Digestive", metadataMap.get("Group")); + assertEquals("Caucasian", metadataMap.get("Ethnicity")); + + assertNotNull(metadataMap); + } + + /** + * group roadmap + * .. track CompRoadmapbySample + * .... track CompRoadmap_HUES64 + * ...... track CompRoadmap_HUES64ViewCOV + * ........ track ENCFF002FAZ_HUES64_ChIP-seq_bySample + * ........ track ENCFF021OWW_HUES64_ChIP-seq_bySample + * + * @throws IOException + */ + @Test + public void testSuperTrack() throws IOException { + String bbFile = TestUtils.DATA_DIR + "hubs/supertrack_trackDb.txt"; + List stanzas = HubParser.loadStanzas(bbFile); + + TrackDbHub trackDbHub = new TrackDbHub(stanzas, null); + + List containers = trackDbHub.getGroupedTrackConfigurations("test"); + assertEquals(1, containers.size()); + + TrackConfigContainer superTrack = containers.get(0); + assertEquals("CompRoadmapbySample", superTrack.name); + assertEquals(1, superTrack.children.size()); + + TrackConfigContainer compositeTrack = superTrack.children.get(0); + assertEquals("HUES64", compositeTrack.label); + assertEquals(1, compositeTrack.children.size()); + + TrackConfigContainer view = compositeTrack.children.get(0); + assertEquals("Coverage", view.label); + assertEquals(0, view.children.size()); + assertEquals(2, view.tracks.size()); + + TrackConfig track = view.tracks.get(0); + Map meta = track.getAttributes(); + assertEquals(4, meta.size()); + } } \ No newline at end of file diff --git a/test/data/hubs/groups_trackDb.txt b/test/data/hubs/groups_trackDb.txt new file mode 100644 index 0000000000..d2ddb944ba --- /dev/null +++ b/test/data/hubs/groups_trackDb.txt @@ -0,0 +1,47 @@ +track cactus +type bigBed 3 +html cactus.html +visibility hide +subGroup1 view Track_Type Snake=Alignments +shortLabel Cactus Alignment +subGroup2 orgs Organisms hs1=hs1 hg38=hg38 +group compGeno +compositeTrack on +dimensions dimensionX=view dimensionY=orgs +dragAndDrop subTracks +priority 2 +longLabel Cactus Alignment +centerLabelsDense on +noInherit on + +track cactusAlignments +type bigBed 3 +visibility full +shortLabel Alignments +view Alignments +parent cactus +longLabel Cactus Alignment + +track snakeHg38 +type halSnake +visibility full +shortLabel hg38 +group compGeno +priority 1 +subGroups view=Snake orgs=hg38 +bigDataUrl /gbdb/hs1/hgCactus/t2tChm13.v2.0.hal +parent cactusAlignments on +longLabel Cactus GRCh37/hg38 +otherSpecies hg38 + +track snakehs1 +type halSnake +visibility hide +shortLabel CHM13/hs1 +group compGeno +priority 2 +subGroups view=Snake orgs=hs1 +bigDataUrl /gbdb/hs1/hgCactus/t2tChm13.v2.0.hal +parent cactusAlignments off +longLabel Cactus CHM13/hs1 +otherSpecies hs1 diff --git a/test/data/hubs/multiwig_trackDB.txt b/test/data/hubs/multiwig_trackDB.txt new file mode 100644 index 0000000000..6d355c3d95 --- /dev/null +++ b/test/data/hubs/multiwig_trackDB.txt @@ -0,0 +1,56 @@ +track RoadmapbySample +superTrack on +shortLabel Sample Summary +longLabel Sample Summary +group our_roadmap +priority 14 +visibility dense + + + track CRoadmap_adipocyte + container multiWig + parent RoadmapbySample + shortLabel adipocyte Summary + longLabel Roadmap Epigenome adipocyte Summary + type bigWig 0 30 + configurable on + visibility dense + maxHeightPixels 150:30:11 + aggregate transparentOverlay + showSubtrackColorOnUi on + autoScale on + windowingFunction mean + priority 1 + viewLimits 0.0:30.0 + + + track ENCFF017DQP_H3K36me3_sampleSummary + bigDataUrl https://www.encodeproject.org/files/ENCFF017DQP/@@download/ENCFF017DQP.bigWig + shortLabel ENCFF017DQP_H3K36me3 + longLabel ENCFF017DQP H3K36me3 + parent CRoadmap_adipocyte off + subGroups view=COV sampleType=adipocyteassayType=adipocyte + type bigWig 0 30 + noInherit on + maxHeightPixels 64:32:16 + color 33,97,11 + autoScale on + priority 1 + metadata Sample=adipocyte OutputType=fold_change_over_control Assay=ChIP-seq genome=hg38 + viewLimits 0.0:30.0 + + + track ENCFF020CKV_H3K9ac_sampleSummary + bigDataUrl https://www.encodeproject.org/files/ENCFF020CKV/@@download/ENCFF020CKV.bigWig + shortLabel ENCFF020CKV_H3K9ac + longLabel ENCFF020CKV H3K9ac + parent CRoadmap_adipocyte off + subGroups view=COV sampleType=adipocyteassayType=adipocyte + type bigWig 0 30 + noInherit on + maxHeightPixels 64:32:16 + color 46,92,0 + autoScale on + priority 1 + metadata Sample=adipocyte OutputType=signal_p-value Assay=ChIP-seq genome=hg38 + viewLimits 0.0:30.0 diff --git a/test/data/hubs/subgroups_trackDb.txt b/test/data/hubs/subgroups_trackDb.txt new file mode 100644 index 0000000000..fc2e448d0d --- /dev/null +++ b/test/data/hubs/subgroups_trackDb.txt @@ -0,0 +1,46 @@ +track RoadmapH3K4me2 +compositeTrack on +parent RoadmapChIP-seq off +shortLabel H3K4me2 +longLabel Roadmap H3K4me2 +group roadmap +subGroup1 view Views ALN=Alignments COV=Coverage +subGroup2 sampleType Sample_Type H1=H1 mesendoderm=mesendoderm H9=H9 skeletal_muscle_satellite_cell=skeletal_muscle_satellite_cell neuronal_stem_cell=neuronal_stem_cell IMR-90=IMR-90 trophoblast_cell=trophoblast_cell mesenchymal_stem_cell=mesenchymal_stem_cell +subGroup3 outputType Output_Type peaks=peaks replicated_peaks=replicated_peaks pseudoreplicated_peaks=pseudoreplicated_peaks signal_p-value=signal_p-value fold_change_over_control=fold_change_over_control +dimensions dimensionY=sampleType dimensionX=outputType +sortOrder sampleType=+ +dragAndDrop on, priority 1 +visibility dense +type bed 3 + + track ENCFF230SMK_H1_ChIP-seq + shortLabel ENCFF230SMK + longLabel H1 ChIP-seq peaks + type bigBed + color 0,82,41 + bigDataUrl https://www.encodeproject.org/files/ENCFF230SMK/@@download/ENCFF230SMK.bigBed + visibility dense + noInherit on + maxHeightPixels 64:32:16 + metadata Sample=H1 Type=peaks Assay=ChIP-seq genome=hg38 + subGroups view=COV sampleType=H1 outputType=peaks + parent RoadmapH3K4me2 off + autoScale on + yLineOnOff on + priority 1 + + track ENCFF029XEN_H1_ChIP-seq + shortLabel ENCFF029XEN + longLabel H1 ChIP-seq peaks + type bigBed + color 0,82,41 + bigDataUrl https://www.encodeproject.org/files/ENCFF029XEN/@@download/ENCFF029XEN.bigBed + visibility dense + noInherit on + maxHeightPixels 64:32:16 + metadata Sample=H1 Type=peaks Assay=ChIP-seq genome=hg38 + subGroups view=COV sampleType=H1 outputType=peaks + parent RoadmapH3K4me2 off + autoScale on + yLineOnOff on + priority 1 diff --git a/test/data/hubs/supertrack_trackDb.txt b/test/data/hubs/supertrack_trackDb.txt new file mode 100644 index 0000000000..11b65077b7 --- /dev/null +++ b/test/data/hubs/supertrack_trackDb.txt @@ -0,0 +1,67 @@ +hub test +shortLabel Supertrack test +longLabel Supertrack test +useOneFile on + +genome hg38 + +track CompRoadmapbySample +superTrack on +shortLabel By Sample +longLabel Roadmap data by sample +group roadmap +visibility dense +priority 12 + +track CompRoadmap_HUES64 +compositeTrack on +parent CompRoadmapbySample +shortLabel HUES64 +longLabel HUES64 tracks +group roadmap +subGroup1 view Views ALN=Alignments COV=Coverage +subGroup2 assayType Assay_Type ChIP-seq=ChIP-seq polyA_plus_RNA-seq=polyA_plus_RNA-seq WGBS=WGBS +dimensions dimensionX=assayType +sortOrder assayType=+ +dividers assayType +dragAndDrop on +priority 1 +visibility dense +type bed 3 + + track CompRoadmap_HUES64ViewCOV + shortLabel Coverage + view COV + maxHeightPixels 64:32:16 + parent CompRoadmap_HUES64 + visibility dense + + track ENCFF002FAZ_HUES64_ChIP-seq_bySample + bigDataUrl https://www.encodeproject.org/files/ENCFF002FAZ/@@download/ENCFF002FAZ.bigBed + shortLabel ENCFF002FAZ + longLabel ENCFF002FAZ HUES64 ChIP-seq + parent CompRoadmap_HUES64ViewCOV off + subGroups view=COV assayType=ChIP-seq + type bigBed + noInherit on + maxHeightPixels 64:32:16 + color 33,97,11 + autoScale on + priority 1 + metadata Sample=HUES64 OutputType=peaks Assay=ChIP-seq genome=hg38 + viewLimits 0.0:30.0 + + track ENCFF021OWW_HUES64_ChIP-seq_bySample + bigDataUrl https://www.encodeproject.org/files/ENCFF021OWW/@@download/ENCFF021OWW.bigWig + shortLabel ENCFF021OWW + longLabel ENCFF021OWW HUES64 ChIP-seq + parent CompRoadmap_HUES64ViewCOV off + subGroups view=COV assayType=ChIP-seq + type bigWig + noInherit on + maxHeightPixels 64:32:16 + color 180,0,0 + autoScale on + priority 1 + metadata Sample=HUES64 OutputType=fold_change_over_control Assay=ChIP-seq genome=hg38 + viewLimits 0.0:30.0 diff --git a/test/data/hubs/view_trackDb.txt b/test/data/hubs/view_trackDb.txt new file mode 100644 index 0000000000..8e174ae037 --- /dev/null +++ b/test/data/hubs/view_trackDb.txt @@ -0,0 +1,68 @@ +track RoadmapConsolidatedHMM +compositeTrack on +shortLabel chromHMM +longLabel chromHMM tracks from Roadmap +subGroup1 view Views AuxiliaryHMM=AuxiliaryHMM PrimaryHMM=PrimaryHMM ImputedHMM=ImputedHMM +subGroup2 sampleType Sample_Type s13048=HepG2 s13300=CD4+_CD25-_IL17-_PMA-Ionomycin_stimulated_MACS_purified_Th_Primary_Cells s13301=Lung s13302=Ovary s13303=Thymus s13049=HMEC s13025=CD4_Naive_Primary_cells s13149=CD4+_CD25-_CD45RA+_Naive_Primary_Cells s13021=CD8_Naive_Primary_cells s13020=CD4_Memory_Primary_cells s13023=Mobilized_CD34_Primary_cells s13022=CD3_Primary_cells s13146=Stomach_Mucosa s13028=CD19_Primary_cells s13144=Penis_Foreskin_Fibroblast_Primary_Cells s13145=Penis_Foreskin_Melanocyte_Primary_Cells s11104=H9 s11106=HUES48 s11107=HUES64 s11101=H1 s11102=hES-I3 s11103=WA-7 s13099=CD14_primary_cells s13052=HUVEC s14044=Dnd41 s13010=Adult_Liver s13011=Bone_Marrow_Derived_Mesenchymal_Stem_cell s13013=Adipose_Nuclei s13014=Colonic_Mucosa s13015=Rectal_Mucosa s13151=CD4+_CD25-_IL17+_PMA-Ionomcyin_stimulated_Th17_Primary_Cells s13150=CD4+_CD25-_CD45RO+_Memory_Primary_Cells s13153=CD8_Memory_Primary_Cells s13152=CD4+_CD25int_CD127+_Tmem_Primary_Cells s13155=Brain_Germinal_Matrix s11304=H9_Derived_Neuronal_Progenitor_Cultured_Cells s13157=CD4+_CD25-_Th_Primary_Cells s13216=Esophagus s13211=Right_Atrium s13160=Brain_Angular_Gyrus s13162=Duodenum_Smooth_Muscle s13163=Duodenum_Mucosa s11213=iPS_DF_19.11 s11216=iPS_DF_6.9 s13007=Breast_vHMEC s13005=Breast_Myoepithelial_cells s13004=Rectal_Smooth_Muscle s13003=Skeletal_Muscle s13002=Stomach_Smooth_Muscle s13001=Muscle_Satellite_Cultured_Cells s13018=Peripheral_Blood_Mononuclear_Primary_cells s11205=hiPS-20b s13019=CD34_Primary_cells s13029=CD15_Primary_cells s13171=PanIslets s11206=hiPS-15b s13154=CD4+_CD25+_CD127-_Treg_Primary_Cells s12219=Fetal_Lung s12218=Fetal_Kidney s13068=Monocytes-CD14+ s11208=hiPS-18b s12107=Fetal_Placenta s12224=Fetal_Intestine_Small s12225=Fetal_Muscle_Trunk s12226=Fetal_Muscle_Leg s14001=HeLa-S3 s12222=Fetal_Adrenal_Gland s12223=Fetal_Intestine_Large s12228=Fetal_Thymus s13054=NH-A s13055=NHDF-Ad s13056=NHEK s13057=NHLF s13050=HSMM s13051=HSMMtube s13218=Psoas_Muscle s11309=H1_Derived_Neuronal_Progenitor_Cultured_Cells s15001=Placenta_Amnion s13217=Aorta s13214=Right_Ventricle s13215=Gastric s13058=Osteobl s13213=Sigmoid_Colon s13210=Left_Ventricle s13147=Colon_Smooth_Muscle s14011=A549 s14018=K562 s13043=Brain_Inferior_Temporal_Lobe s13042=Brain_Hippocampus_Middle s13041=Brain_Cingulate_Gyrus s13040=Brain_Anterior_Caudate s13047=GM12878 s13208=Pancreas s13045=Brain_Substantia_Nigra s13044=Brain_Mid_Frontal_Lobe s13205=Spleen s11314=Mesenchymal_Stem_Cell_Derived_Adipocyte_Cultured_Cells s13207=Small_Intestine s11316=Chondrocytes_from_Bone_Marrow_Derived_Mesenchymal_Stem_Cell_Cultured_Cells s11311=H1_BMP4_Derived_Mesendoderm_Cultured_Cells s11310=IMR90 s11313=H1_Derived_Mesenchymal_Stem_Cells s11312=H1_BMP4_Derived_Trophoblast_Cultured_Cells s11315=Adipose_Derived_Mesenchymal_Stem_Cell_Cultured_Cells s11120=HUES6 s11125=UCSF-4star s11319=H9_Derived_Neuron_Cultured_Cells s12003=Fetal_Brain s12006=Fetal_Heart s13046=CD34_cultured_cells s12227=Fetal_Stomach s11323=hESC_Derived_CD56+_Mesoderm_Cultured_Cells s11327=hESC_Derived_CD56+_Ectoderm_Cultured_Cells s13037=CD56_primary_cells s13035=Penis_Foreskin_Keratinocyte_Primary_Cells s13033=Neurospheres,_Cortex_Derived s13031=Neurospheres,_Ganglionic_Eminence_Derived s11317=hESC_Derived_CD184+_Endoderm_Cultured_Cells +subGroup3 dataType Data_Type Real=Real Imputed=Imputed +dimensions dimensionX=view dimensionY=sampleType dimA=dataType +sortOrder sampleType=+ view=+ +dividers sampleType +dragAndDrop on +visibility dense +type bed 3 + + track RoadmapConsolidatedHMMViewAuxiliaryHMM + shortLabel AuxiliaryHMM + view AuxiliaryHMM + maxHeightPixels 64:32:16 + parent RoadmapConsolidatedHMM + visibility dense + + track AuxiliaryHMM_Real_E071_a27004 + shortLabel BRN.HIPP.MID + longLabel E071 Brain Hippocampus Middle AuxiliaryHMM + type bigBed 9 . + itemRgb on + bigDataUrl https://egg2.wustl.edu/roadmap/data/byFileType/chromhmmSegmentations/ChmmModels/core_K27ac/jointModel/final/E071_18_core_K27ac_hg38lift_dense.bb + visibility dense + noInherit on + maxHeightPixels 64:32:16 + metadata "Epigenome_Mnemonic"="BRN.HIPP.MID" "Standardized_Epigenome_name"="Brain Hippocampus Middle" "EDACC_Epigenome_name"="Brain_Hippocampus_Middle" "Group"="Brain" "Age"="81Y, 73Y" "Lab"="BI" "Sex"="Male" "Anatomy"="BRAIN" "EID"="E071" "Type"="PrimaryTissue" "Order"="64" "Ethnicity"="Unknown" + subGroups view=AuxiliaryHMM sampleType=s13042 dataType=Real + parent RoadmapConsolidatedHMMViewAuxiliaryHMM off + + track RoadmapConsolidatedHMMViewPrimaryHMM + shortLabel PrimaryHMM + view PrimaryHMM + maxHeightPixels 64:32:16 + parent RoadmapConsolidatedHMM + visibility dense + + track PrimaryHMM_Real_E012_a27004 + shortLabel ESDR.CD56.ECTO + longLabel E012 hESC Derived CD56+ Ectoderm Cultured Cells PrimaryHMM + type bigBed 9 . + itemRgb on + bigDataUrl https://egg2.wustl.edu/roadmap/data/byFileType/chromhmmSegmentations/ChmmModels/coreMarks/jointModel/final/E012_15_coreMarks_hg38lift_dense.bb + visibility dense + noInherit on + maxHeightPixels 64:32:16 + metadata "Epigenome_Mnemonic"="ESDR.CD56.ECTO" "Standardized_Epigenome_name"="hESC Derived CD56+ Ectoderm Cultured Cells" "EDACC_Epigenome_name"="hESC_Derived_CD56+_Ectoderm_Cultured_Cells" "Group"="ES-deriv" "Age"="CL" "Lab"="BI" "Sex"="Male" "Anatomy"="ESC_DERIVED" "EID"="E012" "Type"="ESCDerived" "Order"="19" "Ethnicity"="NA" + subGroups view=PrimaryHMM sampleType=s11327 dataType=Real + parent RoadmapConsolidatedHMMViewPrimaryHMM off + + + + track PrimaryHMM_Real_E087_a27004 + shortLabel PANC.ISLT + longLabel E087 Pancreatic Islets PrimaryHMM + type bigBed 9 . + itemRgb on + bigDataUrl https://egg2.wustl.edu/roadmap/data/byFileType/chromhmmSegmentations/ChmmModels/coreMarks/jointModel/final/E087_15_coreMarks_hg38lift_dense.bb + visibility dense + noInherit on + maxHeightPixels 64:32:16 + metadata "Epigenome_Mnemonic"="PANC.ISLT" "Standardized_Epigenome_name"="Pancreatic Islets" "EDACC_Epigenome_name"="Pancreatic_Islets" "Group"="Other" "Age"="Unknown, Unknown, 45Y" "Lab"="BI;UCSF-UBC" "Sex"="Male" "Anatomy"="PANCREAS" "EID"="E087" "Type"="PrimaryTissue" "Order"="105" "Ethnicity"="Unknown, Unknown, Caucasian" + subGroups view=PrimaryHMM sampleType=s13171 dataType=Real + parent RoadmapConsolidatedHMMViewPrimaryHMM off From 25fc5ef63c7a70561ce1699d9ff719dd9984322c Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Tue, 25 Feb 2025 14:07:09 -0800 Subject: [PATCH 119/130] missing icons --- src/main/resources/images/checkbox16.png | Bin 0 -> 428 bytes src/main/resources/images/checkboxUn16.png | Bin 0 -> 221 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/main/resources/images/checkbox16.png create mode 100644 src/main/resources/images/checkboxUn16.png diff --git a/src/main/resources/images/checkbox16.png b/src/main/resources/images/checkbox16.png new file mode 100644 index 0000000000000000000000000000000000000000..65b5c58c08458c970d8f498c2b54e41dbbd54a80 GIT binary patch literal 428 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbMf`~aU2S0JsfuCAu0W()($%E|@? z20)&Xk&%*;lD@t^L`FqL#n8}DK|uk?1u|gDjTIFYfox4pO&|lp25JE+28sa1foyGU zZHR#oF3?zrUZA}YbKs@{odL96M@I*!S6*Hos7yn_=OWOFCM7|B!3bHn@!rZa`OVOOs7BfX#6Z8;!p-pf>Z4={%74OtzX-sr);?F^Nn53U+%(% zJpu87j(U3zHaqBjcW2a8G-jyS8G+wdWQJHW&!7Ud&Y#YxLmPNSiRT!$Ra=)Pm+KjSD%X7B;y&SfOaXQ|!}~ zRrd4u{9UDX=;0L=of7l;M|~tk%uXE5+9)iy-tXa7>5Ys Date: Tue, 25 Feb 2025 18:24:34 -0800 Subject: [PATCH 120/130] fix case issue --- src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java b/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java index d6d13b442c..3d4d9cefbf 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java +++ b/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java @@ -124,10 +124,13 @@ public List getGroupedTrackConfigurations(String hubName) private TrackConfig getTrackConfig(Stanza t) { - String format = t.format(); String url = t.getProperty("bigDataUrl"); TrackConfig config = new TrackConfig(url); - config.setFormat(format); + + String format = t.format(); + if(format != null) { + config.setFormat(format.toLowerCase()); + } config.setPanelName(IGV.DATA_PANEL_NAME); From 9c2f03ebef828e5977bd74399fc19819636a2111 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Tue, 25 Feb 2025 21:11:48 -0800 Subject: [PATCH 121/130] minor UI improvements --- .../igv/ucsc/hub/TrackConfigContainer.java | 12 +- .../igv/ucsc/hub/TrackHubSelectionDialog.java | 106 +++++++++++++++--- .../igv/ui/action/SelectHubTracksAction.java | 9 +- 3 files changed, 105 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/broad/igv/ucsc/hub/TrackConfigContainer.java b/src/main/java/org/broad/igv/ucsc/hub/TrackConfigContainer.java index 033b36a3b4..73bc9e3957 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/TrackConfigContainer.java +++ b/src/main/java/org/broad/igv/ucsc/hub/TrackConfigContainer.java @@ -49,7 +49,15 @@ public void findSelectedConfigs(List selectedConfigs) { } } - public int countSelectedConfigs() { + public int countTracks() { + int count = tracks.size(); + for (TrackConfigContainer container : children) { + count += container.countTracks(); + } + return count; + } + + public int countSelectedTracks() { int count = 0; for (TrackConfig trackConfig : tracks) { if (trackConfig.getVisible() == true) { @@ -57,7 +65,7 @@ public int countSelectedConfigs() { } } for (TrackConfigContainer container : children) { - count += container.countSelectedConfigs(); + count += container.countSelectedTracks(); } return count; } diff --git a/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java b/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java index e1c43f7ded..ee57a36bdf 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java +++ b/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java @@ -22,7 +22,6 @@ import java.util.List; import java.util.function.Function; -import java.util.stream.Collectors; /** @@ -33,12 +32,36 @@ public class TrackHubSelectionDialog extends JDialog { private static Logger log = LogManager.getLogger(TrackHubSelectionDialog.class); + private static Map hubSelectionDialogs = new HashMap(); + private final List trackConfigContainers; Hub hub; private ArrayList categoryPanels; List allSelectionBoxes; boolean canceled = false; + public static TrackHubSelectionDialog getTrackHubSelectionDialog(Hub hub, Set loadedTrackPaths) { + + if (hubSelectionDialogs.containsKey(hub)) { + TrackHubSelectionDialog dialog = hubSelectionDialogs.get(hub); + dialog.resetSelectionBoxes(loadedTrackPaths); + return dialog; + } else { + Frame owner = IGV.getInstance().getMainFrame(); + List groups = hub.getGroupedTrackConfigurations(); + + // Overide visibility + if (loadedTrackPaths != null) { + for (TrackConfigContainer g : groups) { + g.setTrackVisibility(loadedTrackPaths); + } + } + + TrackHubSelectionDialog dialog = new TrackHubSelectionDialog(hub, groups, owner); + hubSelectionDialogs.put(hub, dialog); + return dialog; + } + } public TrackHubSelectionDialog(Hub hub, List trackConfigContainers, Frame owner) { super(owner); @@ -49,6 +72,13 @@ public TrackHubSelectionDialog(Hub hub, List trackConfigCo setLocationRelativeTo(owner); } + private void resetSelectionBoxes(Set loadedTrackPaths) { + if (loadedTrackPaths != null) { + for (SelectionBox box : allSelectionBoxes) { + box.setSelected(loadedTrackPaths.contains(box.trackConfig.getUrl())); + } + } + } void init(List trackConfigContainers) { @@ -171,7 +201,11 @@ private CollapsiblePanel createCategoryPanel(TrackConfigContainer configGroup) { JPanel trackContainer = new JPanel(); trackContainer.setLayout(new BoxLayout(trackContainer, BoxLayout.Y_AXIS)); - List selectionBoxes = addSelectionBoxes(null, configGroup, trackContainer); + // There is a (so far) intractable bug if a large # of JCheckboxes are created for this widget. + int totalTrackCount = configGroup.countTracks(); + SelectionBox.CheckboxType checkboxType = totalTrackCount < 1000 ? SelectionBox.CheckboxType.SWING : SelectionBox.CheckboxType.CUSTOM; + + List selectionBoxes = addSelectionBoxes(null, configGroup, trackContainer, checkboxType); boolean isSelected = false; int maxWidth = 0; @@ -203,9 +237,9 @@ private CollapsiblePanel createCategoryPanel(TrackConfigContainer configGroup) { collapsiblePanel.addSearchButton(searchButton); - for(SelectionBox selectionBox : selectionBoxes) { - selectionBox.setCallback( b -> { - int selected = configGroup.countSelectedConfigs(); + for (SelectionBox selectionBox : selectionBoxes) { + selectionBox.setCallback(b -> { + int selected = configGroup.countSelectedTracks(); String l = configGroup.label + " (" + selectionBoxes.size() + " tracks, " + selected + " selected)"; collapsiblePanel.updateLabel(l); return null; @@ -223,9 +257,10 @@ private CollapsiblePanel createCategoryPanel(TrackConfigContainer configGroup) { * @param labelPrefix * @param container * @param panel + * @param checkboxType * @return */ - private List addSelectionBoxes(String labelPrefix, TrackConfigContainer container, JPanel panel) { + private List addSelectionBoxes(String labelPrefix, TrackConfigContainer container, JPanel panel, SelectionBox.CheckboxType checkboxType) { String title = labelPrefix == null ? "" : labelPrefix + (labelPrefix.length() > 0 ? " - " : "") + container.label; @@ -243,7 +278,7 @@ private List addSelectionBoxes(String labelPrefix, TrackConfigCont trackPanel.setLayout(wrapLayout); for (TrackConfig trackConfig : container.tracks) { - SelectionBox p = new SelectionBox(trackConfig); + SelectionBox p = new SelectionBox(trackConfig, checkboxType); trackPanel.add(p); selectionBoxes.add(p); } @@ -256,7 +291,7 @@ private List addSelectionBoxes(String labelPrefix, TrackConfigCont } for (TrackConfigContainer childChild : container.children) { - selectionBoxes.addAll(addSelectionBoxes(title, childChild, panel)); + selectionBoxes.addAll(addSelectionBoxes(title, childChild, panel, checkboxType)); } return selectionBoxes; } @@ -346,13 +381,15 @@ public List getSelectedConfigs() { static class SelectionBox extends JPanel { + enum CheckboxType {SWING, CUSTOM} + TrackConfig trackConfig; - private CheckBox checkbox; + private CheckBoxWrapper checkbox; int preferredWidth = -1; private int minWidth; Function callback; - public SelectionBox(TrackConfig trackConfig) { + public SelectionBox(TrackConfig trackConfig, CheckboxType checkboxType) { this.setLayout(new BorderLayout(5, 0)); this.trackConfig = trackConfig; @@ -362,7 +399,7 @@ public SelectionBox(TrackConfig trackConfig) { this.setToolTipText(longLabel); } - this.checkbox = new CheckBox(); + this.checkbox = new CheckBoxWrapper(checkboxType); checkbox.setSelected(trackConfig.getVisible()); checkbox.setActionListener(e -> { trackConfig.setVisible(checkbox.isSelected()); @@ -372,8 +409,8 @@ public SelectionBox(TrackConfig trackConfig) { }); JLabel label = new JLabel(trackConfig.getName()); - label.setLabelFor(checkbox); - add(checkbox, BorderLayout.WEST); + label.setLabelFor(checkbox.getComponent()); + add(checkbox.getComponent(), BorderLayout.WEST); String infoLink = trackConfig.getHtml(); @@ -438,6 +475,44 @@ public void setCallback(Function callback) { } } + static class CheckBoxWrapper { + + CheckBox checkBox; + JCheckBox jCheckBox; + + public CheckBoxWrapper(SelectionBox.CheckboxType checkboxType) { + if(checkboxType == SelectionBox.CheckboxType.SWING) { + jCheckBox = new JCheckBox(); + } else { + checkBox = new CheckBox(); + } + } + + public JComponent getComponent() { + return checkBox != null ? checkBox : jCheckBox; + } + + public void setSelected(boolean selected) { + if (checkBox != null) { + checkBox.setSelected(selected); + } else { + jCheckBox.setSelected(selected); + } + } + + public boolean isSelected() { + return checkBox != null ? checkBox.isSelected() : jCheckBox.isSelected(); + } + + public void setActionListener(ActionListener l) { + if(checkBox != null) { + checkBox.setActionListener(l); + } else { + jCheckBox.addActionListener(l); + } + } + } + static class CheckBox extends JLabel { boolean selected = false; @@ -476,6 +551,11 @@ public boolean isSelected() { public void setActionListener(ActionListener l) { this.actionListener = l; } + + @Override + public Dimension getPreferredSize() { + return new Dimension(16, 16); + } } /** diff --git a/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java b/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java index 24150c2061..ce044d7734 100644 --- a/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java +++ b/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java @@ -95,19 +95,14 @@ protected Object doInBackground() throws Exception { final List loadedTracks = IGV.getInstance().getAllTracks().stream().filter(t -> t.getResourceLocator() != null).toList(); Set loadedTrackPaths = new HashSet<>(loadedTracks.stream().map(t -> t.getResourceLocator().getPath()).toList()); - List groups = hub.getGroupedTrackConfigurations(); - // Overide visibility -- track is vis - for (TrackConfigContainer g : groups) { - g.setTrackVisibility(loadedTrackPaths); - } - - TrackHubSelectionDialog dlg = new TrackHubSelectionDialog(hub, groups, IGV.getInstance().getMainFrame()); + TrackHubSelectionDialog dlg = TrackHubSelectionDialog.getTrackHubSelectionDialog(hub, loadedTrackPaths); SwingUtilities.invokeAndWait(() -> dlg.setVisible(true)); if (!dlg.isCanceled()) { + List groups = hub.getGroupedTrackConfigurations(); // The dialog action will modify the visible state for each track config Set trackPathsToRemove = new HashSet<>(); From 98441e3c36e351b70e5b78b9f6489eff240f543f Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Thu, 27 Feb 2025 20:22:51 -0800 Subject: [PATCH 122/130] bug fix -- side effects from rendering VCF track name --- .../org/broad/igv/variant/VariantTrack.java | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/broad/igv/variant/VariantTrack.java b/src/main/java/org/broad/igv/variant/VariantTrack.java index b6a8e13db5..b1ee1dd2ba 100644 --- a/src/main/java/org/broad/igv/variant/VariantTrack.java +++ b/src/main/java/org/broad/igv/variant/VariantTrack.java @@ -662,36 +662,45 @@ private void renderBoundaryLines(Graphics2D g2D, Rectangle trackRectangle, Recta *

* NOTE: The sample names are actually drawn in the drawBackground method. * - * @param g2D + * @param g * @param trackRectangle * @param visibleRectangle */ @Override - public void renderName(Graphics2D g2D, Rectangle trackRectangle, Rectangle visibleRectangle) { + public void renderName(Graphics2D g, Rectangle trackRectangle, Rectangle visibleRectangle) { - top = trackRectangle.y; + Graphics2D g2D = null; - Rectangle rect = new Rectangle(trackRectangle); - g2D.setFont(FontManager.getFont(getFontSize())); - g2D.setColor(BAND2_COLOR); + try { + g2D = (Graphics2D) g.create(); + top = trackRectangle.y; + Rectangle rect = new Rectangle(trackRectangle); + g2D.setFont(FontManager.getFont(getFontSize())); + g2D.setColor(BAND2_COLOR); - g2D.setColor(Color.black); - rect.height = getVariantsHeight(); - if (rect.intersects(visibleRectangle)) { - Rectangle intersectedRect = rect.intersection(visibleRectangle); - GraphicUtils.drawWrappedText(getName(), intersectedRect, g2D, false); - } - rect.y += rect.height; - rect.height = getGenotypeBandHeight(); + g2D.setColor(Color.black); + rect.height = getVariantsHeight(); + if (rect.intersects(visibleRectangle)) { + Rectangle intersectedRect = rect.intersection(visibleRectangle); + GraphicUtils.drawWrappedText(getName(), intersectedRect, g2D, false); + } - // The sample bounds list will get reset when the names are drawn. - sampleBounds.clear(); - drawBackground(g2D, rect, visibleRectangle, BackgroundType.NAME); + rect.y += rect.height; + rect.height = getGenotypeBandHeight(); + // The sample bounds list will get reset when the names are drawn. + sampleBounds.clear(); + drawBackground(g2D, rect, visibleRectangle, BackgroundType.NAME); - renderBoundaryLines(g2D, trackRectangle, visibleRectangle); + + renderBoundaryLines(g2D, trackRectangle, visibleRectangle); + } finally { + if(g2D != null) { + g2D.dispose(); + } + } } From d19aba814b2a06c0a11fa64550979218dd7c3f6d Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Tue, 4 Mar 2025 09:52:58 -0800 Subject: [PATCH 123/130] track load and genome definition refinements. --- .../org/broad/igv/feature/genome/Genome.java | 6 - .../feature/genome/GenomeDownloadUtils.java | 8 +- .../feature/genome/load/HubGenomeLoader.java | 99 ++++++++---- .../igv/ucsc/hub/TrackConfigContainer.java | 18 ++- .../igv/ucsc/hub/TrackHubSelectionDialog.java | 151 +++++++++++------- .../java/org/broad/igv/ui/IGVMenuBar.java | 23 ++- .../igv/ui/action/LoadFromURLMenuAction.java | 22 +-- .../igv/ui/action/SelectHubTracksAction.java | 34 +--- .../broad/igv/ui/action/UCSCGenArkAction.java | 2 +- .../igv/ui/commandbar/GenomeListManager.java | 29 ++-- 10 files changed, 226 insertions(+), 166 deletions(-) diff --git a/src/main/java/org/broad/igv/feature/genome/Genome.java b/src/main/java/org/broad/igv/feature/genome/Genome.java index ac9bc79382..9a7ac4a887 100644 --- a/src/main/java/org/broad/igv/feature/genome/Genome.java +++ b/src/main/java/org/broad/igv/feature/genome/Genome.java @@ -97,7 +97,6 @@ public class Genome { private String homeChromosome; private String defaultPos; private String nameSet; - private Hub genomeHub; private List trackHubs; public Genome(GenomeConfig config) throws IOException { @@ -809,12 +808,7 @@ public List computeLongChromosomeNames() { } - public Hub getGenomeHub() { - return genomeHub; - } - public void setGenomeHub(Hub genomeHub) { - this.genomeHub = genomeHub; // A genome hub is by definition also a track hub this.trackHubs.add(genomeHub); } diff --git a/src/main/java/org/broad/igv/feature/genome/GenomeDownloadUtils.java b/src/main/java/org/broad/igv/feature/genome/GenomeDownloadUtils.java index 8c20632aef..681e271ae8 100644 --- a/src/main/java/org/broad/igv/feature/genome/GenomeDownloadUtils.java +++ b/src/main/java/org/broad/igv/feature/genome/GenomeDownloadUtils.java @@ -140,7 +140,13 @@ private static void downloadAndUpdateConfig(boolean downloadData, String[] field } } - private static void saveLocalGenome(GenomeConfig genomeConfig, File localFile) throws IOException { + public static File saveLocalGenome(GenomeConfig config) throws IOException { + File f = new File(DirectoryManager.getGenomeCacheDirectory(), config.getId() + ".json"); + saveLocalGenome(config, f); + return f; + } + + public static void saveLocalGenome(GenomeConfig genomeConfig, File localFile) throws IOException { log.info("Saving " + localFile.getAbsolutePath()); try (BufferedWriter writer = new BufferedWriter(new FileWriter(localFile))) { Gson gson = new GsonBuilder().setPrettyPrinting().create(); diff --git a/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java b/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java index 9c1793e2d7..571465ae16 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java +++ b/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java @@ -1,15 +1,16 @@ package org.broad.igv.feature.genome.load; -import org.apache.commons.io.FileUtils; -import org.broad.igv.DirectoryManager; import org.broad.igv.Globals; import org.broad.igv.feature.genome.Genome; +import org.broad.igv.feature.genome.GenomeDownloadUtils; +import org.broad.igv.feature.genome.GenomeManager; import org.broad.igv.prefs.PreferencesManager; import org.broad.igv.ucsc.hub.Hub; import org.broad.igv.ucsc.hub.HubParser; import org.broad.igv.ucsc.hub.TrackConfigContainer; import org.broad.igv.ui.IGV; import org.broad.igv.ucsc.hub.TrackHubSelectionDialog; +import org.broad.igv.ui.commandbar.GenomeListManager; import java.io.File; import java.io.IOException; @@ -21,12 +22,6 @@ */ public class HubGenomeLoader extends GenomeLoader { - String hubURL; - - public HubGenomeLoader(String hubURL) { - this.hubURL = hubURL; - } - public static boolean isHubURL(String obj) { return obj.endsWith("/hub.txt"); } @@ -45,10 +40,21 @@ public static String convertToHubURL(String accension) { } } - @Override - public Genome loadGenome() throws IOException { + /** + * Load and parse a hub.txt file. Creates and stores a genome json definition for future loads. + * + * @param hubURL + * @return + * @throws IOException + */ + public static Genome loadGenome(String hubURL) throws IOException { + Hub hub = HubParser.loadAssemblyHub(hubURL); + final GenomeConfig config = getGenomeConfig(hub); + File genomeFile = GenomeDownloadUtils.saveLocalGenome(config); + return GenomeManager.getInstance().loadGenome(genomeFile.getAbsolutePath()); + } - Hub hub = HubParser.loadAssemblyHub(this.hubURL); + private static GenomeConfig getGenomeConfig(Hub hub) throws IOException { GenomeConfig config = hub.getGenomeConfig(); @@ -56,7 +62,7 @@ public Genome loadGenome() throws IOException { // Check previous selections for this hub first // TODO -- Maintain track order? - String key = "hub:" + this.hubURL; + String key = "hub:" + hub.getUrl(); final List groupedTrackConfigurations = hub.getGroupedTrackConfigurations(); if (PreferencesManager.getPreferences().hasExplicitValue(key)) { @@ -76,36 +82,75 @@ else if (IGV.hasInstance() && !Globals.isBatch() && !Globals.isHeadless() && !Gl count += g.tracks.size(); } - // If the total # of tracks is >= 20 filter to "Gene" groups, usually a single group - List filteredGroups = count < 20 ? - groupedTrackConfigurations : - groupedTrackConfigurations.stream().filter(g -> g.label.startsWith("Gene")).collect(Collectors.toList()); - - - TrackHubSelectionDialog dlg = new TrackHubSelectionDialog(hub, filteredGroups, IGV.getInstance().getMainFrame()); + TrackHubSelectionDialog dlg = TrackHubSelectionDialog.getTrackHubSelectionDialog(hub, null); dlg.setVisible(true); - if(!dlg.isCanceled()) { + if (!dlg.isCanceled()) { List selectedTracks = dlg.getSelectedConfigs(); config.setTracks(selectedTracks); // Remember selections in user preferences - List names = selectedTracks.stream().map((trackConfig) -> trackConfig.getName()).toList(); - PreferencesManager.getPreferences().put(key, String.join(",", names)); + // List names = selectedTracks.stream().map((trackConfig) -> trackConfig.getName()).toList(); + // PreferencesManager.getPreferences().put(key, String.join(",", names)); + // TODO -- read these for backward compatibility? } } - + config.setHubs(Arrays.asList(hub.getUrl())); Genome genome = new Genome(config); genome.setGenomeHub(hub); + return config; + } + /*********************************************/ + // GenomeLoad interface follows -- provided for backward compatibility (IGV versions up to 2.19.2). + // The current IGV version uses the static loadGenome(hubURL) method. - String genomeJson = config.toJson(); - File dir = DirectoryManager.getGenomeCacheDirectory(); - FileUtils.write(new File(dir, config.getId() + ".json"), genomeJson); + String hubURL; + public HubGenomeLoader(String hubURL) { + this.hubURL = hubURL; + } - return genome; + /** + * + * @return + * @throws IOException + */ + @Override + public Genome loadGenome() throws IOException { + Hub hub = HubParser.loadAssemblyHub(hubURL); + GenomeConfig config = getGenomeConfig(hub); + + // Search for list of tracks for this hub in the preferences. This was used prior to version 2.19.2, current + // versions of IGV store track information in a genome json file. + String key = "hub:" + this.hubURL; + if (PreferencesManager.getPreferences().hasExplicitValue(key)) { + List selectedTracks = new ArrayList<>(); + Set selectedTrackNames = new HashSet<>(Arrays.asList(PreferencesManager.getPreferences().get(key).split(","))); + List trackConfigGroups = hub.getGroupedTrackConfigurations(); + for (TrackConfigContainer group : trackConfigGroups) { + List trackConfigs = group.findTracks(trackConfig -> selectedTrackNames.contains(trackConfig.getName())); + for (TrackConfig trackConfig : trackConfigs) { + if (selectedTrackNames.contains(trackConfig.getName())) { + selectedTracks.add(trackConfig); + } + } + } + config.setTracks(selectedTracks); + } + + // Save genome json for future loads and remove preferences and remote reference (Genome is now defined by + // local genome json, preferences and remote reference not needed) + GenomeDownloadUtils.saveLocalGenome(config); + PreferencesManager.getPreferences().remove(key); + GenomeListManager.getInstance().removeRemoteItem(config.getId()); + + + Genome genome = new Genome(config); + genome.setGenomeHub(hub); + return genome; } + } diff --git a/src/main/java/org/broad/igv/ucsc/hub/TrackConfigContainer.java b/src/main/java/org/broad/igv/ucsc/hub/TrackConfigContainer.java index 73bc9e3957..b2e933bc4b 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/TrackConfigContainer.java +++ b/src/main/java/org/broad/igv/ucsc/hub/TrackConfigContainer.java @@ -38,14 +38,20 @@ public void map(Function f) { } } - public void findSelectedConfigs(List selectedConfigs) { - for (TrackConfig trackConfig : selectedConfigs) { - if (trackConfig.getVisible() == true) { - selectedConfigs.add(trackConfig); + public List findTracks(Function filter) { + List found = new ArrayList<>(); + find(found, filter); + return found; + } + + private void find(List found, Function filter) { + for (TrackConfig config : tracks) { + if (filter.apply(config)) { + found.add(config); } } - for (TrackConfigContainer container : children) { - container.findSelectedConfigs(selectedConfigs); + for (TrackConfigContainer g : children) { + g.find(found, filter); } } diff --git a/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java b/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java index ee57a36bdf..44e631c55e 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java +++ b/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java @@ -5,6 +5,7 @@ import org.broad.igv.feature.genome.load.TrackConfig; import org.broad.igv.logging.LogManager; import org.broad.igv.logging.Logger; +import org.broad.igv.track.Track; import org.broad.igv.ui.IGV; import org.broad.igv.ui.util.HyperlinkFactory; import org.broad.igv.ui.util.IconFactory; @@ -32,12 +33,11 @@ public class TrackHubSelectionDialog extends JDialog { private static Logger log = LogManager.getLogger(TrackHubSelectionDialog.class); - private static Map hubSelectionDialogs = new HashMap(); + private static Map hubSelectionDialogs = new HashMap<>(); - private final List trackConfigContainers; - Hub hub; + private Hub hub; private ArrayList categoryPanels; - List allSelectionBoxes; + private List allSelectionBoxes; boolean canceled = false; public static TrackHubSelectionDialog getTrackHubSelectionDialog(Hub hub, Set loadedTrackPaths) { @@ -49,25 +49,16 @@ public static TrackHubSelectionDialog getTrackHubSelectionDialog(Hub hub, Set groups = hub.getGroupedTrackConfigurations(); - - // Overide visibility - if (loadedTrackPaths != null) { - for (TrackConfigContainer g : groups) { - g.setTrackVisibility(loadedTrackPaths); - } - } - TrackHubSelectionDialog dialog = new TrackHubSelectionDialog(hub, groups, owner); hubSelectionDialogs.put(hub, dialog); return dialog; } } - public TrackHubSelectionDialog(Hub hub, List trackConfigContainers, Frame owner) { + private TrackHubSelectionDialog(Hub hub, List trackConfigContainers, Frame owner) { super(owner); setModal(true); this.hub = hub; - this.trackConfigContainers = trackConfigContainers; init(trackConfigContainers); setLocationRelativeTo(owner); } @@ -75,7 +66,9 @@ public TrackHubSelectionDialog(Hub hub, List trackConfigCo private void resetSelectionBoxes(Set loadedTrackPaths) { if (loadedTrackPaths != null) { for (SelectionBox box : allSelectionBoxes) { - box.setSelected(loadedTrackPaths.contains(box.trackConfig.getUrl())); + final boolean isLoaded = loadedTrackPaths.contains(box.trackConfig.getUrl()); + box.setSelected(isLoaded); + box.setEnabled(!isLoaded); } } } @@ -108,7 +101,7 @@ void init(List trackConfigContainers) { topButtonPanel.setLayout(new BorderLayout()); JPanel expandButtonPanel = new JPanel(); - expandButtonPanel.setLayout(new FlowLayout(FlowLayout.LEFT)); + expandButtonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT)); JButton expandAllButton = new JButton("Expand All"); expandAllButton.addActionListener(e -> { categoryPanels.forEach(cp -> cp.expand()); @@ -134,9 +127,12 @@ void init(List trackConfigContainers) { mainPanel.add(scrollPane, BorderLayout.CENTER); // Loop through track groups + final List loadedTracks = IGV.getInstance().getAllTracks().stream().filter(t -> t.getResourceLocator() != null).toList(); + Set loadedTrackPaths = new HashSet<>(loadedTracks.stream().map(t -> t.getResourceLocator().getPath()).toList()); + for (TrackConfigContainer configGroup : trackConfigContainers) { categoryContainer.add(Box.createVerticalStrut(10)); - CollapsiblePanel categoryPanel = createCategoryPanel(configGroup); + CollapsiblePanel categoryPanel = createCategoryPanel(configGroup, loadedTrackPaths); categoryContainer.add(categoryPanel); categoryPanels.add(categoryPanel); } @@ -154,7 +150,7 @@ void init(List trackConfigContainers) { setVisible(false); }); - JButton okButton = new JButton("OK"); + JButton okButton = new JButton("Load"); okButton.addActionListener(e -> { canceled = false; setVisible(false); @@ -172,7 +168,6 @@ void init(List trackConfigContainers) { mainPanel.add(buttonPanel, BorderLayout.SOUTH); - // mainPanel.validate(); } @@ -196,16 +191,16 @@ public boolean isCanceled() { * @param configGroup * @return */ - private CollapsiblePanel createCategoryPanel(TrackConfigContainer configGroup) { + private CollapsiblePanel createCategoryPanel(TrackConfigContainer configGroup, Set loadedTrackPaths) { - JPanel trackContainer = new JPanel(); - trackContainer.setLayout(new BoxLayout(trackContainer, BoxLayout.Y_AXIS)); + JPanel trackPanel = new JPanel(); + trackPanel.setLayout(new BoxLayout(trackPanel, BoxLayout.Y_AXIS)); // There is a (so far) intractable bug if a large # of JCheckboxes are created for this widget. int totalTrackCount = configGroup.countTracks(); SelectionBox.CheckboxType checkboxType = totalTrackCount < 1000 ? SelectionBox.CheckboxType.SWING : SelectionBox.CheckboxType.CUSTOM; - List selectionBoxes = addSelectionBoxes(null, configGroup, trackContainer, checkboxType); + List selectionBoxes = addSelectionBoxes(null, configGroup, trackPanel, loadedTrackPaths, checkboxType); boolean isSelected = false; int maxWidth = 0; @@ -225,7 +220,7 @@ private CollapsiblePanel createCategoryPanel(TrackConfigContainer configGroup) { String label = configGroup.label + " (" + selectionBoxes.size() + " tracks, " + selectionCount + " selected)"; - final CollapsiblePanel collapsiblePanel = new CollapsiblePanel(label, trackContainer, isSelected || configGroup.defaultOpen); + final CollapsiblePanel collapsiblePanel = new CollapsiblePanel(label, trackPanel, isSelected || configGroup.defaultOpen); // Add a search button for categories with large numbers of records final JButton searchButton = createSearchButton("Search " + configGroup.label, selectionBoxes, @@ -260,7 +255,11 @@ private CollapsiblePanel createCategoryPanel(TrackConfigContainer configGroup) { * @param checkboxType * @return */ - private List addSelectionBoxes(String labelPrefix, TrackConfigContainer container, JPanel panel, SelectionBox.CheckboxType checkboxType) { + private List addSelectionBoxes(String labelPrefix, + TrackConfigContainer container, + JPanel panel, + Set loadedTrackPaths, + SelectionBox.CheckboxType checkboxType) { String title = labelPrefix == null ? "" : labelPrefix + (labelPrefix.length() > 0 ? " - " : "") + container.label; @@ -278,9 +277,13 @@ private List addSelectionBoxes(String labelPrefix, TrackConfigCont trackPanel.setLayout(wrapLayout); for (TrackConfig trackConfig : container.tracks) { - SelectionBox p = new SelectionBox(trackConfig, checkboxType); - trackPanel.add(p); - selectionBoxes.add(p); + + SelectionBox selectionBox = new SelectionBox(trackConfig, checkboxType); + final boolean isLoaded = loadedTrackPaths.contains(trackConfig.getUrl()); + selectionBox.setSelected(isLoaded); + selectionBox.setEnabled(!isLoaded); + trackPanel.add(selectionBox); + selectionBoxes.add(selectionBox); } panel.add(Box.createVerticalStrut(5)); @@ -291,7 +294,7 @@ private List addSelectionBoxes(String labelPrefix, TrackConfigCont } for (TrackConfigContainer childChild : container.children) { - selectionBoxes.addAll(addSelectionBoxes(title, childChild, panel, checkboxType)); + selectionBoxes.addAll(addSelectionBoxes(title, childChild, panel, loadedTrackPaths, checkboxType)); } return selectionBoxes; } @@ -307,33 +310,35 @@ private JButton createSearchButton(String label, List selectionBox attributeNames.add("Description"); attributeNames.add("Format"); - Map recordSelectionBoxMap = new HashMap<>(); List records = new ArrayList<>(); for (SelectionBox selectionBox : selectionBoxes) { - TrackConfig trackConfig = selectionBox.getTrackConfig(); - final Map trackConfigAttributes = trackConfig.getAttributes(); - Map attributes = trackConfigAttributes; - if (attributes == null) { - attributes = new LinkedHashMap<>(); - } + if (selectionBox.isEnabled()) { - attributes.put("Name", trackConfig.getName()); - attributes.put("Description", trackConfig.getDescription()); - attributes.put("Format", trackConfig.getFormat()); + TrackConfig trackConfig = selectionBox.getTrackConfig(); + final Map trackConfigAttributes = trackConfig.getAttributes(); + Map attributes = trackConfigAttributes; + if (attributes == null) { + attributes = new LinkedHashMap<>(); + } - if (trackConfigAttributes != null) { - attributes.putAll(trackConfigAttributes); - attributeNames.addAll(trackConfigAttributes.keySet()); - } + attributes.put("Name", trackConfig.getName()); + attributes.put("Description", trackConfig.getDescription()); + attributes.put("Format", trackConfig.getFormat()); + + if (trackConfigAttributes != null) { + attributes.putAll(trackConfigAttributes); + attributeNames.addAll(trackConfigAttributes.keySet()); + } - final FileRecord record = new FileRecord(trackConfig.getUrl(), attributes); - record.setSelected(trackConfig.getVisible()); - records.add(record); - recordSelectionBoxMap.put(record, selectionBox); + final FileRecord record = new FileRecord(trackConfig.getUrl(), attributes); + record.setSelected(trackConfig.getVisible()); + records.add(record); + recordSelectionBoxMap.put(record, selectionBox); + } } @@ -371,25 +376,23 @@ private JButton createSearchButton(String label, List selectionBox * @return */ public List getSelectedConfigs() { - - List selectedConfigs = new ArrayList<>(); - for (TrackConfigContainer container : trackConfigContainers) { - container.findSelectedConfigs(selectedConfigs); - } - return selectedConfigs; + return allSelectionBoxes.stream() + .filter(b -> b.isEnabled() && b.isSelected()) + .map(SelectionBox::getTrackConfig).toList(); } static class SelectionBox extends JPanel { enum CheckboxType {SWING, CUSTOM} - TrackConfig trackConfig; + private final JLabel label; + private TrackConfig trackConfig; private CheckBoxWrapper checkbox; - int preferredWidth = -1; + private int preferredWidth = -1; private int minWidth; - Function callback; + private Function callback; - public SelectionBox(TrackConfig trackConfig, CheckboxType checkboxType) { + public SelectionBox(TrackConfig trackConfig, CheckboxType checkboxType) { this.setLayout(new BorderLayout(5, 0)); this.trackConfig = trackConfig; @@ -400,7 +403,7 @@ public SelectionBox(TrackConfig trackConfig, CheckboxType checkboxType) { } this.checkbox = new CheckBoxWrapper(checkboxType); - checkbox.setSelected(trackConfig.getVisible()); + checkbox.setActionListener(e -> { trackConfig.setVisible(checkbox.isSelected()); if (callback != null) { @@ -408,7 +411,7 @@ public SelectionBox(TrackConfig trackConfig, CheckboxType checkboxType) { } }); - JLabel label = new JLabel(trackConfig.getName()); + label = new JLabel(trackConfig.getName()); label.setLabelFor(checkbox.getComponent()); add(checkbox.getComponent(), BorderLayout.WEST); @@ -466,6 +469,18 @@ public void setSelected(boolean selected) { trackConfig.setVisible(selected); } + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + checkbox.setEnabled(enabled); + label.setEnabled(enabled); + } + + + public boolean isSelected() { + return checkbox.isSelected(); + } + public TrackConfig getTrackConfig() { return trackConfig; } @@ -481,7 +496,7 @@ static class CheckBoxWrapper { JCheckBox jCheckBox; public CheckBoxWrapper(SelectionBox.CheckboxType checkboxType) { - if(checkboxType == SelectionBox.CheckboxType.SWING) { + if (checkboxType == SelectionBox.CheckboxType.SWING) { jCheckBox = new JCheckBox(); } else { checkBox = new CheckBox(); @@ -501,16 +516,28 @@ public void setSelected(boolean selected) { } public boolean isSelected() { - return checkBox != null ? checkBox.isSelected() : jCheckBox.isSelected(); + return checkBox != null ? checkBox.isSelected() : jCheckBox.isSelected(); } public void setActionListener(ActionListener l) { - if(checkBox != null) { + if (checkBox != null) { checkBox.setActionListener(l); } else { jCheckBox.addActionListener(l); } } + + public void setEnabled(boolean enabled) { + if (checkBox != null) { + checkBox.setEnabled(enabled); + } else { + jCheckBox.setEnabled(enabled); + } + } + + public boolean isEnabled() { + return checkBox != null ? checkBox.isEnabled() : jCheckBox.isEnabled(); + } } static class CheckBox extends JLabel { diff --git a/src/main/java/org/broad/igv/ui/IGVMenuBar.java b/src/main/java/org/broad/igv/ui/IGVMenuBar.java index 0fafd30be6..14920f4c95 100644 --- a/src/main/java/org/broad/igv/ui/IGVMenuBar.java +++ b/src/main/java/org/broad/igv/ui/IGVMenuBar.java @@ -109,7 +109,6 @@ public class IGVMenuBar extends JMenuBar implements IGVEventObserver { * we can't access genome server list */ private JMenuItem loadGenomeFromServerMenuItem; - private JMenuItem selectGenomeAnnotationsItem; private JMenuItem reloadSessionItem; private JMenuItem recentFilesMenu; @@ -480,16 +479,16 @@ private JMenu createGenomesMenu() { menu.add(new JSeparator()); MenuAction genArkAction = new UCSCGenArkAction("Load Genome from UCSC GenArk...", 0, igv); menu.add(MenuAndToolbarUtils.createMenuItem(genArkAction)); - - MenuAction menuAction = new SelectHubTracksAction("Select GenArk Genome Tracks...", igv, null); - selectGenomeAnnotationsItem = MenuAndToolbarUtils.createMenuItem(menuAction); - Genome newGenome = GenomeManager.getInstance().getCurrentGenome(); - selectGenomeAnnotationsItem.setEnabled(newGenome != null && newGenome.getGenomeHub() != null); - menu.add(selectGenomeAnnotationsItem); +// +// MenuAction menuAction = new SelectHubTracksAction("Select GenArk Genome Tracks...", igv, null); +// selectGenomeAnnotationsItem = MenuAndToolbarUtils.createMenuItem(menuAction); +// Genome newGenome = GenomeManager.getInstance().getCurrentGenome(); +// selectGenomeAnnotationsItem.setEnabled(newGenome != null && newGenome.getGenomeHub() != null); +// menu.add(selectGenomeAnnotationsItem); menu.add(new JSeparator()); // Add genome to combo box from server - menuAction = new MenuAction("Remove Genomes...", null) { + MenuAction menuAction = new MenuAction("Remove Genomes...", null) { @Override public void actionPerformed(ActionEvent event) { RemoveGenomesDialog dialog2 = new RemoveGenomesDialog(igv.getMainFrame()); @@ -499,10 +498,10 @@ public void actionPerformed(ActionEvent event) { menuAction.setToolTipText("Remove genomes which appear in the dropdown list"); menu.add(MenuAndToolbarUtils.createMenuItem(menuAction)); - menu.addMenuListener((MenuSelectedListener) e -> { - Genome genome1 = GenomeManager.getInstance().getCurrentGenome(); - selectGenomeAnnotationsItem.setEnabled(genome1 != null && genome1.getGenomeHub() != null); - }); +// menu.addMenuListener((MenuSelectedListener) e -> { +// Genome genome1 = GenomeManager.getInstance().getCurrentGenome(); +// selectGenomeAnnotationsItem.setEnabled(genome1 != null && genome1.getGenomeHub() != null); +// }); return menu; diff --git a/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java b/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java index 1049ca999f..f58818d28a 100644 --- a/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java +++ b/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java @@ -84,7 +84,7 @@ public void actionPerformed(ActionEvent e) { } } else if ((command.equalsIgnoreCase(LOAD_GENOME_FROM_URL))) { - String url = JOptionPane.showInputDialog(IGV.getInstance().getMainFrame(), ta, "Enter URL to .genome or FASTA file", + String url = JOptionPane.showInputDialog(IGV.getInstance().getMainFrame(), ta, "Enter URL to .json, hub.txt, or FASTA file", JOptionPane.QUESTION_MESSAGE); loadGenomeFromUrl(url); @@ -95,7 +95,7 @@ public void actionPerformed(ActionEvent e) { } private void loadUrls(List inputs, List indexes, boolean isHtsGet) { - checkURLs(inputs); + if (inputs.size() == 1 && HubGenomeLoader.isHubURL(inputs.getFirst())) { LongRunningTask.submit(() -> { try { @@ -119,7 +119,7 @@ private void loadUrls(List inputs, List indexes, boolean isHtsGe if (!indexes.isEmpty() && indexes.size() != inputs.size()) { throw new RuntimeException("The number of Index URLs must equal the number of File URLs"); } - checkURLs(indexes); + List locators = getResourceLocators(inputs, indexes, isHtsGet); igv.addToRecentUrls(locators); igv.loadTracks(locators); @@ -150,8 +150,11 @@ private static void loadGenomeFromUrl(String url) { if (url != null && !url.isBlank()) { url = url.trim(); try { - checkURLs(List.of(url)); - GenomeManager.getInstance().loadGenome(url); + if(isHubURL(url)) { + HubGenomeLoader.loadGenome(url); + } else { + GenomeManager.getInstance().loadGenome(url); + } } catch (Exception e) { MessageUtils.showMessage("Error loading genome: " + e.getMessage()); } @@ -183,14 +186,5 @@ private static boolean isHubURL(String input) { return input.endsWith("/hub.txt"); } - private static void checkURLs(List urls) { - for (String url : urls) { - if (url.startsWith("ftp://")) { - MessageUtils.showMessage("FTP protocol is not supported"); - } - } - } - - } diff --git a/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java b/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java index ce044d7734..c16b1bdce2 100644 --- a/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java +++ b/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java @@ -49,9 +49,7 @@ import java.util.*; /** - * Select tracks from a track hub. This action is used in 2 modes, (1) load tracks from a specific hub, and (2) select - * default annotation tracks for the currently loaded genome. Tracks are loaded in both modes, but in the - * second selected tracks are also added to the genome definition. + * Select tracks from a track hub. * * @author jrobinso */ @@ -62,15 +60,10 @@ public class SelectHubTracksAction extends MenuAction { private Hub hub; IGV mainFrame; - // Keep track of authorization failures so user isn't constantly harranged - static HashSet failedURLs = new HashSet(); - - boolean updateGenome; public SelectHubTracksAction(String label, IGV mainFrame, Hub hub) { super(label, null); this.mainFrame = mainFrame; - this.updateGenome = hub == null; this.hub = hub; } @@ -85,13 +78,6 @@ public void actionPerformed(ActionEvent evt) { protected Object doInBackground() throws Exception { try { Genome genome = GenomeManager.getInstance().getCurrentGenome(); - if (hub == null) { - hub = genome.getGenomeHub(); - if (hub == null) { - // This should not happen - MessageUtils.showMessage("No tracks available for current genome."); - } - } final List loadedTracks = IGV.getInstance().getAllTracks().stream().filter(t -> t.getResourceLocator() != null).toList(); Set loadedTrackPaths = new HashSet<>(loadedTracks.stream().map(t -> t.getResourceLocator().getPath()).toList()); @@ -105,7 +91,6 @@ protected Object doInBackground() throws Exception { List groups = hub.getGroupedTrackConfigurations(); // The dialog action will modify the visible state for each track config - Set trackPathsToRemove = new HashSet<>(); List tracksToLoad = new ArrayList<>(); List selected = new ArrayList<>(); for (TrackConfigContainer g : groups) { @@ -115,26 +100,21 @@ protected Object doInBackground() throws Exception { if (!loadedTrackPaths.contains(config.getUrl())) { tracksToLoad.add(config); } - } else { - trackPathsToRemove.add(config.getUrl()); } return null; }); } - List tracksToRemove = loadedTracks.stream().filter(t -> trackPathsToRemove.contains(t.getResourceLocator().getPath())).toList(); - IGV.getInstance().deleteTracks(tracksToRemove); - List locators = tracksToLoad.stream().map(t -> ResourceLocator.fromTrackConfig(t)).toList(); IGV.getInstance().loadTracks(locators); // Update genome - if (updateGenome) { - genome.setAnnotationResources(locators); - // Update preferences - String key = "hub:" + hub.getUrl(); - PreferencesManager.getPreferences().put(key, String.join(",", selected.stream().map(c -> c.getName()).toList())); - } +// if (updateGenome) { +// genome.setAnnotationResources(locators); +// // Update preferences +// String key = "hub:" + hub.getUrl(); +// PreferencesManager.getPreferences().put(key, String.join(",", selected.stream().map(c -> c.getName()).toList())); +// } } } catch (Exception e) { log.error("Error loading track configurations", e); diff --git a/src/main/java/org/broad/igv/ui/action/UCSCGenArkAction.java b/src/main/java/org/broad/igv/ui/action/UCSCGenArkAction.java index bdb06b20f9..28e5dd67bc 100644 --- a/src/main/java/org/broad/igv/ui/action/UCSCGenArkAction.java +++ b/src/main/java/org/broad/igv/ui/action/UCSCGenArkAction.java @@ -95,7 +95,7 @@ public void actionPerformed(ActionEvent event) { if (!dlg.isCanceled() && rec != null) { String accession = rec.getAttributeValue("accession"); String url = HubGenomeLoader.convertToHubURL(accession); - GenomeManager.getInstance().loadGenome(url); + HubGenomeLoader.loadGenome(url); } } catch (IOException e) { log.error(e); diff --git a/src/main/java/org/broad/igv/ui/commandbar/GenomeListManager.java b/src/main/java/org/broad/igv/ui/commandbar/GenomeListManager.java index b883de0dfc..954774f33b 100644 --- a/src/main/java/org/broad/igv/ui/commandbar/GenomeListManager.java +++ b/src/main/java/org/broad/igv/ui/commandbar/GenomeListManager.java @@ -101,15 +101,15 @@ public List getGenomeListItems() { */ public void addGenomeItem(GenomeListItem genomeListItem) { - if(genomeItemMap.values().stream().anyMatch(item -> genomeListItem.equals(item))) { + if (genomeItemMap.values().stream().anyMatch(item -> genomeListItem.equals(item))) { return; } genomeItemMap.put(genomeListItem.getId(), genomeListItem); - if(FileUtils.isRemote(genomeListItem.getPath()) || - !DirectoryManager.getGenomeCacheDirectory().getAbsoluteFile(). - equals(new File(genomeListItem.getPath()).getParentFile().getAbsolutePath())) { + if (FileUtils.isRemote(genomeListItem.getPath()) || + !DirectoryManager.getGenomeCacheDirectory(). + equals(new File(genomeListItem.getPath()).getParentFile())) { if (remoteGenomesMap == null) { remoteGenomesMap = new HashMap<>(); } @@ -135,7 +135,7 @@ public GenomeListItem getGenomeListItem(String genomeId) { if (matchingItem == null) { // If genome archive was not found, search hosted genomes - matchingItem = getHostedGenomesMap().get(genomeId); + matchingItem = getHostedGenomesMap().get(genomeId); if (matchingItem != null) { return matchingItem; } @@ -178,9 +178,9 @@ private void rebuildGenomeItemMaps() throws IOException { * @throws IOException * @see GenomeListItem */ - public Map getDownloadedGenomeMap() { + public Map getDownloadedGenomeMap() { - if(downloadedGenomesMap == null) { + if (downloadedGenomesMap == null) { downloadedGenomesMap = new HashMap<>(); if (!DirectoryManager.getGenomeCacheDirectory().exists()) { @@ -311,7 +311,14 @@ public void removeItems(List removedValuesList) { } } - List getHostedGenomeList() { + public void removeRemoteItem(String id) { + if (remoteGenomesMap.containsKey(id)) { + remoteGenomesMap.remove(id); + exportRemoteGenomesList(); + } + } + + List getHostedGenomeList() { List items = new ArrayList<>(getHostedGenomesMap().values()); items.sort(sorter); return items; @@ -397,7 +404,9 @@ public Map getHostedGenomesMap() { } /** - * Return the list of remote genomes for the dropdown + * Return the list of remote genomes for the dropdown. + * + * Remote genomes include all genomes not store locally in the igv/genomes folder. * * @return LinkedHashSet * @throws IOException @@ -413,7 +422,7 @@ public Map getRemoteGenomesMap() { File listFile = new File(DirectoryManager.getGenomeCacheDirectory(), getRemoteGenomesFilename()); - if(!listFile.exists()) { + if (!listFile.exists()) { // Try old filename listFile = new File(DirectoryManager.getGenomeCacheDirectory(), "user-defined-genomes.txt"); } From 12b00df7287c1b12200a27b3b875497ead44af28 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Tue, 4 Mar 2025 15:58:51 -0800 Subject: [PATCH 124/130] Add wait cursor for genark hub loads. Some can take time to load. --- .../feature/genome/load/HubGenomeLoader.java | 65 +++++++++++++++---- .../igv/ucsc/hub/TrackHubSelectionDialog.java | 24 ++++--- .../igv/ui/action/SelectHubTracksAction.java | 2 +- 3 files changed, 70 insertions(+), 21 deletions(-) diff --git a/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java b/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java index 571465ae16..2be47c8b19 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java +++ b/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java @@ -4,16 +4,22 @@ import org.broad.igv.feature.genome.Genome; import org.broad.igv.feature.genome.GenomeDownloadUtils; import org.broad.igv.feature.genome.GenomeManager; +import org.broad.igv.logging.LogManager; +import org.broad.igv.logging.Logger; import org.broad.igv.prefs.PreferencesManager; import org.broad.igv.ucsc.hub.Hub; import org.broad.igv.ucsc.hub.HubParser; import org.broad.igv.ucsc.hub.TrackConfigContainer; import org.broad.igv.ui.IGV; import org.broad.igv.ucsc.hub.TrackHubSelectionDialog; +import org.broad.igv.ui.WaitCursorManager; import org.broad.igv.ui.commandbar.GenomeListManager; +import org.broad.igv.ui.util.MessageUtils; +import javax.swing.*; import java.io.File; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; import java.util.*; import java.util.stream.Collectors; @@ -22,6 +28,8 @@ */ public class HubGenomeLoader extends GenomeLoader { + static Logger log = LogManager.getLogger(HubGenomeLoader.class); + public static boolean isHubURL(String obj) { return obj.endsWith("/hub.txt"); } @@ -47,11 +55,42 @@ public static String convertToHubURL(String accension) { * @return * @throws IOException */ - public static Genome loadGenome(String hubURL) throws IOException { - Hub hub = HubParser.loadAssemblyHub(hubURL); - final GenomeConfig config = getGenomeConfig(hub); - File genomeFile = GenomeDownloadUtils.saveLocalGenome(config); - return GenomeManager.getInstance().loadGenome(genomeFile.getAbsolutePath()); + public static void loadGenome(String hubURL) throws IOException { + + final WaitCursorManager.CursorToken token = WaitCursorManager.showWaitCursor(); + + SwingWorker worker = new SwingWorker<>() { + @Override + protected File doInBackground() throws Exception { + File genomeFile = null; + try { + Hub hub = HubParser.loadAssemblyHub(hubURL); + final GenomeConfig config = getGenomeConfig(hub); + genomeFile = GenomeDownloadUtils.saveLocalGenome(config); + } catch (Exception e) { + log.error("Error loading hub: " + e.getMessage()); + MessageUtils.showMessage("Error loading hub: " + e.getMessage()); + } finally { + WaitCursorManager.removeWaitCursor(token); + } + return genomeFile; + } + + @Override + protected void done() { + try { + File genomeFile = get(); + WaitCursorManager.removeWaitCursor(token); + GenomeManager.getInstance().loadGenome(genomeFile.getAbsolutePath()); + } catch (Exception e) { + log.error("Error loading hub: " + e.getMessage()); + MessageUtils.showMessage("Error loading hub: " + e.getMessage()); + } + } + }; + + worker.execute(); + } private static GenomeConfig getGenomeConfig(Hub hub) throws IOException { @@ -77,15 +116,18 @@ private static GenomeConfig getGenomeConfig(Hub hub) throws IOException { // If running in interactive mode opend dialog to set tracks. else if (IGV.hasInstance() && !Globals.isBatch() && !Globals.isHeadless() && !Globals.isTesting()) { - int count = 0; - for (TrackConfigContainer g : groupedTrackConfigurations) { - count += g.tracks.size(); + TrackHubSelectionDialog dlg = TrackHubSelectionDialog.getTrackHubSelectionDialog(hub, null, true); + + boolean dlgSuccess = true; + try { + SwingUtilities.invokeAndWait(() -> dlg.setVisible(true)); + } catch (Exception e) { + dlgSuccess = false; + log.error("Error opening or using TrackHubSelectionDialog: " + e.getMessage()); } - TrackHubSelectionDialog dlg = TrackHubSelectionDialog.getTrackHubSelectionDialog(hub, null); - dlg.setVisible(true); - if (!dlg.isCanceled()) { + if (!dlg.isCanceled() && dlgSuccess) { List selectedTracks = dlg.getSelectedConfigs(); config.setTracks(selectedTracks); @@ -107,6 +149,7 @@ else if (IGV.hasInstance() && !Globals.isBatch() && !Globals.isHeadless() && !Gl // The current IGV version uses the static loadGenome(hubURL) method. String hubURL; + public HubGenomeLoader(String hubURL) { this.hubURL = hubURL; } diff --git a/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java b/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java index 44e631c55e..76385a8dfa 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java +++ b/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java @@ -34,35 +34,43 @@ public class TrackHubSelectionDialog extends JDialog { private static Logger log = LogManager.getLogger(TrackHubSelectionDialog.class); private static Map hubSelectionDialogs = new HashMap<>(); - private Hub hub; + private boolean autoselectDefaults; private ArrayList categoryPanels; private List allSelectionBoxes; boolean canceled = false; - public static TrackHubSelectionDialog getTrackHubSelectionDialog(Hub hub, Set loadedTrackPaths) { + public static TrackHubSelectionDialog getTrackHubSelectionDialog(Hub hub, Set loadedTrackPaths, boolean autoselectDefaults) { if (hubSelectionDialogs.containsKey(hub)) { TrackHubSelectionDialog dialog = hubSelectionDialogs.get(hub); dialog.resetSelectionBoxes(loadedTrackPaths); + dialog.autoselectDefaults = autoselectDefaults; return dialog; } else { Frame owner = IGV.getInstance().getMainFrame(); List groups = hub.getGroupedTrackConfigurations(); - TrackHubSelectionDialog dialog = new TrackHubSelectionDialog(hub, groups, owner); + TrackHubSelectionDialog dialog = new TrackHubSelectionDialog(hub, groups, autoselectDefaults, owner); hubSelectionDialogs.put(hub, dialog); return dialog; } } - private TrackHubSelectionDialog(Hub hub, List trackConfigContainers, Frame owner) { + private TrackHubSelectionDialog(Hub hub, List trackConfigContainers, boolean autoselectDefaults, Frame owner) { super(owner); setModal(true); + this.autoselectDefaults = autoselectDefaults; this.hub = hub; init(trackConfigContainers); setLocationRelativeTo(owner); } + /** + * Called when hub dialog is reused. Update the selection box state to reflect currently loaded tracks, which + * could have changed since last invocation. + * + * @param loadedTrackPaths + */ private void resetSelectionBoxes(Set loadedTrackPaths) { if (loadedTrackPaths != null) { for (SelectionBox box : allSelectionBoxes) { @@ -167,8 +175,6 @@ void init(List trackConfigContainers) { getRootPane().setDefaultButton(okButton); mainPanel.add(buttonPanel, BorderLayout.SOUTH); - - } private JPanel getLabeledHyperlink(String label, String url) { @@ -280,7 +286,7 @@ private List addSelectionBoxes(String labelPrefix, SelectionBox selectionBox = new SelectionBox(trackConfig, checkboxType); final boolean isLoaded = loadedTrackPaths.contains(trackConfig.getUrl()); - selectionBox.setSelected(isLoaded); + selectionBox.setSelected(isLoaded || (autoselectDefaults && trackConfig.getVisible() == true)); selectionBox.setEnabled(!isLoaded); trackPanel.add(selectionBox); selectionBoxes.add(selectionBox); @@ -392,7 +398,7 @@ enum CheckboxType {SWING, CUSTOM} private int minWidth; private Function callback; - public SelectionBox(TrackConfig trackConfig, CheckboxType checkboxType) { + public SelectionBox(TrackConfig trackConfig, CheckboxType checkboxType) { this.setLayout(new BorderLayout(5, 0)); this.trackConfig = trackConfig; @@ -601,7 +607,7 @@ public static void main(String[] args) throws InterruptedException, InvocationTa List groupedTrackConfigurations = hub.getGroupedTrackConfigurations(); - final TrackHubSelectionDialog dlf = new TrackHubSelectionDialog(hub, groupedTrackConfigurations, null); + final TrackHubSelectionDialog dlf = new TrackHubSelectionDialog(hub, groupedTrackConfigurations, true, null); dlf.setSize(new Dimension(800, 600)); dlf.setVisible(true); diff --git a/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java b/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java index c16b1bdce2..d9842a3311 100644 --- a/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java +++ b/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java @@ -82,7 +82,7 @@ protected Object doInBackground() throws Exception { final List loadedTracks = IGV.getInstance().getAllTracks().stream().filter(t -> t.getResourceLocator() != null).toList(); Set loadedTrackPaths = new HashSet<>(loadedTracks.stream().map(t -> t.getResourceLocator().getPath()).toList()); - TrackHubSelectionDialog dlg = TrackHubSelectionDialog.getTrackHubSelectionDialog(hub, loadedTrackPaths); + TrackHubSelectionDialog dlg = TrackHubSelectionDialog.getTrackHubSelectionDialog(hub, loadedTrackPaths, false); SwingUtilities.invokeAndWait(() -> dlg.setVisible(true)); From 47942cae10a6989d9532ee74d0e4776b5bba5cda Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Wed, 5 Mar 2025 10:41:59 -0800 Subject: [PATCH 125/130] spinner and track selection refinements --- .../feature/genome/load/HubGenomeLoader.java | 4 +- .../igv/ucsc/hub/TrackHubSelectionDialog.java | 55 +++++++++++-------- .../java/org/broad/igv/ui/IGVMenuBar.java | 2 +- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java b/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java index 2be47c8b19..fd462edd68 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java +++ b/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java @@ -70,8 +70,6 @@ protected File doInBackground() throws Exception { } catch (Exception e) { log.error("Error loading hub: " + e.getMessage()); MessageUtils.showMessage("Error loading hub: " + e.getMessage()); - } finally { - WaitCursorManager.removeWaitCursor(token); } return genomeFile; } @@ -85,6 +83,8 @@ protected void done() { } catch (Exception e) { log.error("Error loading hub: " + e.getMessage()); MessageUtils.showMessage("Error loading hub: " + e.getMessage()); + } finally { + WaitCursorManager.removeWaitCursor(token); } } }; diff --git a/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java b/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java index 76385a8dfa..98b94ba945 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java +++ b/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java @@ -40,20 +40,24 @@ public class TrackHubSelectionDialog extends JDialog { private List allSelectionBoxes; boolean canceled = false; - public static TrackHubSelectionDialog getTrackHubSelectionDialog(Hub hub, Set loadedTrackPaths, boolean autoselectDefaults) { + public static TrackHubSelectionDialog getTrackHubSelectionDialog( + Hub hub, + Set loadedTrackPaths, + boolean autoselectDefaults) { + + TrackHubSelectionDialog dialog; + if (hubSelectionDialogs.containsKey(hub) && !autoselectDefaults) { + dialog = hubSelectionDialogs.get(hub); - if (hubSelectionDialogs.containsKey(hub)) { - TrackHubSelectionDialog dialog = hubSelectionDialogs.get(hub); - dialog.resetSelectionBoxes(loadedTrackPaths); dialog.autoselectDefaults = autoselectDefaults; - return dialog; } else { Frame owner = IGV.getInstance().getMainFrame(); List groups = hub.getGroupedTrackConfigurations(); - TrackHubSelectionDialog dialog = new TrackHubSelectionDialog(hub, groups, autoselectDefaults, owner); + dialog = new TrackHubSelectionDialog(hub, groups, autoselectDefaults, owner); hubSelectionDialogs.put(hub, dialog); - return dialog; } + dialog.resetSelectionBoxes(loadedTrackPaths); + return dialog; } private TrackHubSelectionDialog(Hub hub, List trackConfigContainers, boolean autoselectDefaults, Frame owner) { @@ -72,12 +76,10 @@ private TrackHubSelectionDialog(Hub hub, List trackConfigC * @param loadedTrackPaths */ private void resetSelectionBoxes(Set loadedTrackPaths) { - if (loadedTrackPaths != null) { - for (SelectionBox box : allSelectionBoxes) { - final boolean isLoaded = loadedTrackPaths.contains(box.trackConfig.getUrl()); - box.setSelected(isLoaded); - box.setEnabled(!isLoaded); - } + for (SelectionBox box : allSelectionBoxes) { + final boolean isLoaded = loadedTrackPaths != null && loadedTrackPaths.contains(box.trackConfig.getUrl()); + box.setSelected(isLoaded || (autoselectDefaults && box.trackConfig.getVisible() == true)); + box.setEnabled(!isLoaded); } } @@ -125,6 +127,19 @@ void init(List trackConfigContainers) { expandButtonPanel.add(collapseAllButton); topButtonPanel.add(expandButtonPanel, BorderLayout.WEST); + JPanel clearAllPanel = new JPanel(); + clearAllPanel.setLayout(new FlowLayout(FlowLayout.RIGHT)); + JButton clearAllButton = new JButton("Clear All"); + clearAllPanel.add(clearAllButton); + clearAllButton.addActionListener(e -> { + for (SelectionBox box : allSelectionBoxes) { + if (box.isEnabled()) { + box.setSelected(false); + } + } + }); + topButtonPanel.add(clearAllPanel, BorderLayout.EAST); + topPanel.add(topButtonPanel); mainPanel.add(topPanel, BorderLayout.NORTH); @@ -140,7 +155,7 @@ void init(List trackConfigContainers) { for (TrackConfigContainer configGroup : trackConfigContainers) { categoryContainer.add(Box.createVerticalStrut(10)); - CollapsiblePanel categoryPanel = createCategoryPanel(configGroup, loadedTrackPaths); + CollapsiblePanel categoryPanel = createCategoryPanel(configGroup); categoryContainer.add(categoryPanel); categoryPanels.add(categoryPanel); } @@ -197,7 +212,7 @@ public boolean isCanceled() { * @param configGroup * @return */ - private CollapsiblePanel createCategoryPanel(TrackConfigContainer configGroup, Set loadedTrackPaths) { + private CollapsiblePanel createCategoryPanel(TrackConfigContainer configGroup) { JPanel trackPanel = new JPanel(); trackPanel.setLayout(new BoxLayout(trackPanel, BoxLayout.Y_AXIS)); @@ -206,7 +221,7 @@ private CollapsiblePanel createCategoryPanel(TrackConfigContainer configGroup, S int totalTrackCount = configGroup.countTracks(); SelectionBox.CheckboxType checkboxType = totalTrackCount < 1000 ? SelectionBox.CheckboxType.SWING : SelectionBox.CheckboxType.CUSTOM; - List selectionBoxes = addSelectionBoxes(null, configGroup, trackPanel, loadedTrackPaths, checkboxType); + List selectionBoxes = addSelectionBoxes(null, configGroup, trackPanel, checkboxType); boolean isSelected = false; int maxWidth = 0; @@ -264,7 +279,6 @@ private CollapsiblePanel createCategoryPanel(TrackConfigContainer configGroup, S private List addSelectionBoxes(String labelPrefix, TrackConfigContainer container, JPanel panel, - Set loadedTrackPaths, SelectionBox.CheckboxType checkboxType) { String title = labelPrefix == null ? "" : @@ -285,9 +299,6 @@ private List addSelectionBoxes(String labelPrefix, for (TrackConfig trackConfig : container.tracks) { SelectionBox selectionBox = new SelectionBox(trackConfig, checkboxType); - final boolean isLoaded = loadedTrackPaths.contains(trackConfig.getUrl()); - selectionBox.setSelected(isLoaded || (autoselectDefaults && trackConfig.getVisible() == true)); - selectionBox.setEnabled(!isLoaded); trackPanel.add(selectionBox); selectionBoxes.add(selectionBox); } @@ -295,12 +306,10 @@ private List addSelectionBoxes(String labelPrefix, panel.add(Box.createVerticalStrut(5)); panel.add(trackPanel); - // final CollapsiblePanel collapsiblePanel = new CollapsiblePanel(title, trackPanel, false, CollapsiblePanel.HEADER_BG2); - // trackContainer.add(collapsiblePanel); } for (TrackConfigContainer childChild : container.children) { - selectionBoxes.addAll(addSelectionBoxes(title, childChild, panel, loadedTrackPaths, checkboxType)); + selectionBoxes.addAll(addSelectionBoxes(title, childChild, panel, checkboxType)); } return selectionBoxes; } diff --git a/src/main/java/org/broad/igv/ui/IGVMenuBar.java b/src/main/java/org/broad/igv/ui/IGVMenuBar.java index 14920f4c95..c8141032e5 100644 --- a/src/main/java/org/broad/igv/ui/IGVMenuBar.java +++ b/src/main/java/org/broad/igv/ui/IGVMenuBar.java @@ -308,7 +308,7 @@ JMenu createFileMenu(Genome genome) { if (genome.getTrackHubs().size() > 0) { menuItems.add(new JSeparator()); for (Hub trackHub : genome.getTrackHubs()) { - menuAction = new SelectHubTracksAction(trackHub.getShortLabel(), igv, trackHub); + menuAction = new SelectHubTracksAction("Hub: " + trackHub.getShortLabel(), igv, trackHub); menuAction.setToolTipText(trackHub.getLongLabel()); JMenuItem selectGenomeAnnotationsItem = MenuAndToolbarUtils.createMenuItem(menuAction); selectGenomeAnnotationsItem.setToolTipText(trackHub.getLongLabel()); From d161e0e13cafe8377fee55e1b36a86bef3cbd178 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Wed, 5 Mar 2025 20:19:42 -0800 Subject: [PATCH 126/130] Refactoring --- .../broad/igv/ucsc/hub/CollapsiblePanel.java | 338 +++++++++++++++++- .../igv/ucsc/hub/TrackHubSelectionDialog.java | 316 +--------------- 2 files changed, 344 insertions(+), 310 deletions(-) diff --git a/src/main/java/org/broad/igv/ucsc/hub/CollapsiblePanel.java b/src/main/java/org/broad/igv/ucsc/hub/CollapsiblePanel.java index 49748c785c..5d2a014a9d 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/CollapsiblePanel.java +++ b/src/main/java/org/broad/igv/ucsc/hub/CollapsiblePanel.java @@ -1,29 +1,78 @@ package org.broad.igv.ucsc.hub; +import org.broad.igv.encode.FileRecord; +import org.broad.igv.encode.TrackChooser; +import org.broad.igv.feature.genome.load.TrackConfig; +import org.broad.igv.logging.LogManager; +import org.broad.igv.logging.Logger; import org.broad.igv.ui.FontManager; +import org.broad.igv.ui.IGV; import org.broad.igv.ui.util.IconFactory; import javax.swing.*; import javax.swing.border.EmptyBorder; import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.net.URI; +import java.util.*; +import java.util.List; +import java.util.function.Function; public class CollapsiblePanel extends JPanel { + private static Logger log = LogManager.getLogger(CollapsiblePanel.class); public static final Color HEADER_BG = new Color(180, 204, 226); public static final Color HEADER_BG2 = new Color(204, 204, 204); private final JLabel jlabel; + private final List selectionBoxes; + private final boolean autoselectDefaults; + private final TrackConfigContainer configGroup; private JButton collapseButton; private JComponent content; private JPanel header; private ImageIcon openIcon; private ImageIcon closeIcon; - public CollapsiblePanel(String label, JComponent content, boolean isOpen) { + public CollapsiblePanel(TrackConfigContainer configGroup, boolean autoselectDefaults) { Color backgroundColor = HEADER_BG; + this.configGroup = configGroup; + this.autoselectDefaults = autoselectDefaults; + setLayout(new BorderLayout()); - this.content = content; + + JPanel trackPanel = new JPanel(); + trackPanel.setLayout(new BoxLayout(trackPanel, BoxLayout.Y_AXIS)); + + // There is a (so far) intractable bug if a large # of JCheckboxes are created for this widget. + int totalTrackCount = configGroup.countTracks(); + SelectionBox.CheckboxType checkboxType = totalTrackCount < 1000 ? SelectionBox.CheckboxType.SWING : SelectionBox.CheckboxType.CUSTOM; + + selectionBoxes = addSelectionBoxes(null, configGroup, trackPanel, checkboxType); + + boolean isSelected = false; + int maxWidth = 0; + int selectionCount = 0; + for (SelectionBox selectionBox : selectionBoxes) { + if (selectionBox.isSelected()) { + selectionCount++; + isSelected = true; + } + maxWidth = Math.max(maxWidth, selectionBox.getPreferredSize().width); + // allSelectionBoxes.add(selectionBox); + } + + for (SelectionBox selectionBox : selectionBoxes) { + selectionBox.setPreferredWidth(maxWidth); + } + + String label = configGroup.label + " (" + selectionBoxes.size() + " tracks, " + selectionCount + " selected)"; + + boolean isOpen = isSelected || configGroup.defaultOpen; + + this.content = trackPanel; this.openIcon = IconFactory.getInstance().getIcon(IconFactory.IconID.MINUS); this.closeIcon = IconFactory.getInstance().getIcon(IconFactory.IconID.PLUS); @@ -53,12 +102,59 @@ public CollapsiblePanel(String label, JComponent content, boolean isOpen) { this.add(header, BorderLayout.NORTH); + final JButton searchButton = createSearchButton("Search " + configGroup.label, selectionBoxes, + (selectedCount) -> { + this.updateLabel(); + return null; + }); + + this.addSearchButton(searchButton); + + for (SelectionBox selectionBox : selectionBoxes) { + selectionBox.setCallback(b -> { + this.updateLabel(); + return null; + }); + } + } + + public void resetSelectionBoxes(Set loadedTrackPaths) { + for (CollapsiblePanel.SelectionBox box : selectionBoxes) { + final boolean isLoaded = loadedTrackPaths != null && loadedTrackPaths.contains(box.trackConfig.getUrl()); + box.setSelected(isLoaded || (autoselectDefaults && box.trackConfig.getVisible() == true)); + box.setEnabled(!isLoaded); + updateLabel(); + } } - public void updateLabel(String label) { + public void updateLabel() { + int count = 0; + for (SelectionBox selectionBox : selectionBoxes) { + if (selectionBox.isEnabled() && selectionBox.isSelected()) { + count++; + } + } + String label = configGroup.label + " (" + selectionBoxes.size() + " tracks, " + count + " selected)"; jlabel.setText(label); } + public void clearSelections() { + for (CollapsiblePanel.SelectionBox box : selectionBoxes) { + if (box.isEnabled()) { + box.setSelected(false); + } + } + String label = configGroup.label + " (" + selectionBoxes.size() + " tracks, 0 selected)"; + jlabel.setText(label); + } + + public List getSelectedTracks() { + return selectionBoxes.stream() + .filter(b -> b.isEnabled() && b.isSelected()) + .map(CollapsiblePanel.SelectionBox::getTrackConfig).toList(); + } + + public void addSearchButton(JComponent searchButton) { header.add(searchButton, BorderLayout.EAST); revalidate(); @@ -92,15 +188,237 @@ public Dimension getMaximumSize() { } } - public static void main(String[] args) { + /** + * Add selection boxes for the container (a group, superTrack, compositeTrack, or view). Return true if any + * tracks are selected. + * + * @param labelPrefix + * @param container + * @param panel + * @param checkboxType + * @return + */ + private java.util.List addSelectionBoxes(String labelPrefix, + TrackConfigContainer container, + JPanel panel, + SelectionBox.CheckboxType checkboxType) { + + String title = labelPrefix == null ? "" : + labelPrefix + (labelPrefix.length() > 0 ? " - " : "") + container.label; + + java.util.List selectionBoxes = new ArrayList<>(); + + if (container.tracks.size() > 0) { + + JPanel trackPanel = new JPanel(); + if (labelPrefix != null) { + trackPanel.setBorder(BorderFactory.createTitledBorder(title)); + } + final WrapLayout wrapLayout = new WrapLayout(); + wrapLayout.setAlignment(FlowLayout.LEFT); + trackPanel.setLayout(wrapLayout); + + for (TrackConfig trackConfig : container.tracks) { + + SelectionBox selectionBox = new SelectionBox(trackConfig, checkboxType); + trackPanel.add(selectionBox); + selectionBoxes.add(selectionBox); + } + + panel.add(Box.createVerticalStrut(5)); + panel.add(trackPanel); + + } + + for (TrackConfigContainer childChild : container.children) { + selectionBoxes.addAll(addSelectionBoxes(title, childChild, panel, checkboxType)); + } + return selectionBoxes; + } + + private JButton createSearchButton(String label, java.util.List selectionBoxes, Function callback) { + + JButton searchButton = new JButton("Search"); + + searchButton.addActionListener(e -> { + + Set attributeNames = new LinkedHashSet<>(); + attributeNames.add("Name"); + attributeNames.add("Description"); + attributeNames.add("Format"); + + Map recordSelectionBoxMap = new HashMap<>(); + + java.util.List records = new ArrayList<>(); + + for (SelectionBox selectionBox : selectionBoxes) { + + if (selectionBox.isEnabled()) { + + TrackConfig trackConfig = selectionBox.getTrackConfig(); + final Map trackConfigAttributes = trackConfig.getAttributes(); + Map attributes = trackConfigAttributes; + if (attributes == null) { + attributes = new LinkedHashMap<>(); + } + + attributes.put("Name", trackConfig.getName()); + attributes.put("Description", trackConfig.getDescription()); + attributes.put("Format", trackConfig.getFormat()); + + if (trackConfigAttributes != null) { + attributes.putAll(trackConfigAttributes); + attributeNames.addAll(trackConfigAttributes.keySet()); + } + + final FileRecord record = new FileRecord(trackConfig.getUrl(), attributes); + record.setSelected(trackConfig.getVisible()); + records.add(record); + recordSelectionBoxMap.put(record, selectionBox); + } + } + + + List headings = new ArrayList<>(attributeNames); + // Limit # of columns + if (headings.size() > 15) { + headings = headings.subList(0, 15); + } + + Frame owner = IGV.hasInstance() ? IGV.getInstance().getMainFrame() : null; + + TrackChooser chooser = new TrackChooser( + owner, + headings, + records, + label); + + if(owner != null) { + Rectangle ownerBounds = owner.getBounds(); + chooser.setSize(new Dimension(Math.min(ownerBounds.width, 1200), Math.min(ownerBounds.height, 800))); + chooser.setLocationRelativeTo(owner); + } + chooser.setVisible(true); + + if (!chooser.isCanceled()) { + Set selectedRecords = new HashSet<>(chooser.getSelectedRecords()); + for (Map.Entry entry : recordSelectionBoxMap.entrySet()) { + entry.getValue().setSelected(selectedRecords.contains(entry.getKey())); + } + callback.apply(selectedRecords.size()); + } + }); + return searchButton; + } + + + static class SelectionBox extends JPanel { + + enum CheckboxType {SWING, CUSTOM} + + private final JLabel label; + private TrackConfig trackConfig; + private TrackHubSelectionDialog.CheckBoxWrapper checkbox; + private int preferredWidth = -1; + private int minWidth; + private Function callback; + + public SelectionBox(TrackConfig trackConfig, CheckboxType checkboxType) { + + this.setLayout(new BorderLayout(5, 0)); + this.trackConfig = trackConfig; + + String longLabel = trackConfig.getLongLabel(); + if (longLabel != null) { + this.setToolTipText(longLabel); + } + + this.checkbox = new TrackHubSelectionDialog.CheckBoxWrapper(checkboxType); - JComponent content = new JTextArea("alskdfjalskdjflsdkjfsaldkfjsladkfjsalkfj"); - CollapsiblePanel cp = new CollapsiblePanel("Expand/Collapse", content, false); + checkbox.setActionListener(e -> { + trackConfig.setVisible(checkbox.isSelected()); + if (callback != null) { + callback.apply(checkbox.isSelected() ? 1 : 0); + } + }); - JFrame f = new JFrame("test"); - f.setSize(500, 500); - f.setContentPane(cp); - f.setVisible(true); + label = new JLabel(trackConfig.getName()); + label.setLabelFor(checkbox.getComponent()); + add(checkbox.getComponent(), BorderLayout.WEST); + + String infoLink = trackConfig.getHtml(); + + if (infoLink == null || "".equals(infoLink.trim())) { + add(label, BorderLayout.CENTER); + } else { + ImageIcon icon = IconFactory.getInstance().getIcon(IconFactory.IconID.INFO); + JLabel iconLabel = new JLabel(icon); + iconLabel.setToolTipText(infoLink); + iconLabel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + add(iconLabel, BorderLayout.CENTER); + iconLabel.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + try { + Desktop.getDesktop().browse(new URI(infoLink)); + } catch (Exception ex) { + log.error("Error following hyperlink: " + infoLink, ex); + } + } + }); + + JPanel panel = new JPanel(); + panel.setLayout(new FlowLayout(FlowLayout.LEFT)); + panel.add(label); + panel.add(iconLabel); + add(panel, BorderLayout.CENTER); + } + } + + public void setPreferredWidth(int width) { + minWidth = 200; + this.preferredWidth = Math.max(minWidth, width); + } + + @Override + public Dimension getPreferredSize() { + return preferredWidth > 0 ? new Dimension(preferredWidth, 20) : super.getPreferredSize(); + } + + @Override + public Dimension getMinimumSize() { + return getPreferredSize(); + } + + @Override + public Dimension getMaximumSize() { + return getPreferredSize(); + } + + public void setSelected(boolean selected) { + checkbox.setSelected(selected); + trackConfig.setVisible(selected); + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + checkbox.setEnabled(enabled); + label.setEnabled(enabled); + } + + + public boolean isSelected() { + return checkbox.isSelected(); + } + + public TrackConfig getTrackConfig() { + return trackConfig; + } + + public void setCallback(Function callback) { + this.callback = callback; + } } } diff --git a/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java b/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java index 98b94ba945..9ee51df600 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java +++ b/src/main/java/org/broad/igv/ucsc/hub/TrackHubSelectionDialog.java @@ -1,7 +1,5 @@ package org.broad.igv.ucsc.hub; -import org.broad.igv.encode.FileRecord; -import org.broad.igv.encode.TrackChooser; import org.broad.igv.feature.genome.load.TrackConfig; import org.broad.igv.logging.LogManager; import org.broad.igv.logging.Logger; @@ -18,12 +16,9 @@ import java.awt.event.MouseEvent; import java.io.IOException; import java.lang.reflect.InvocationTargetException; -import java.net.URI; import java.util.*; import java.util.List; -import java.util.function.Function; - /** * Dialog to enable selection of tracks defined by track hubs. Modifies the "visible" property of @@ -34,11 +29,11 @@ public class TrackHubSelectionDialog extends JDialog { private static Logger log = LogManager.getLogger(TrackHubSelectionDialog.class); private static Map hubSelectionDialogs = new HashMap<>(); + private Hub hub; private boolean autoselectDefaults; private ArrayList categoryPanels; - private List allSelectionBoxes; - boolean canceled = false; + private boolean canceled = false; public static TrackHubSelectionDialog getTrackHubSelectionDialog( Hub hub, @@ -47,16 +42,16 @@ public static TrackHubSelectionDialog getTrackHubSelectionDialog( TrackHubSelectionDialog dialog; if (hubSelectionDialogs.containsKey(hub) && !autoselectDefaults) { - dialog = hubSelectionDialogs.get(hub); - + dialog = hubSelectionDialogs.get(hub); dialog.autoselectDefaults = autoselectDefaults; } else { Frame owner = IGV.getInstance().getMainFrame(); List groups = hub.getGroupedTrackConfigurations(); - dialog = new TrackHubSelectionDialog(hub, groups, autoselectDefaults, owner); + dialog = new TrackHubSelectionDialog(hub, groups, autoselectDefaults, owner); hubSelectionDialogs.put(hub, dialog); } dialog.resetSelectionBoxes(loadedTrackPaths); + return dialog; } @@ -76,10 +71,8 @@ private TrackHubSelectionDialog(Hub hub, List trackConfigC * @param loadedTrackPaths */ private void resetSelectionBoxes(Set loadedTrackPaths) { - for (SelectionBox box : allSelectionBoxes) { - final boolean isLoaded = loadedTrackPaths != null && loadedTrackPaths.contains(box.trackConfig.getUrl()); - box.setSelected(isLoaded || (autoselectDefaults && box.trackConfig.getVisible() == true)); - box.setEnabled(!isLoaded); + for (CollapsiblePanel collapsiblePanel : categoryPanels) { + collapsiblePanel.resetSelectionBoxes(loadedTrackPaths); } } @@ -91,7 +84,6 @@ void init(List trackConfigContainers) { setSize(new Dimension(Math.min(ownerBounds.width, 1200), Math.min(ownerBounds.height, 1000))); categoryPanels = new ArrayList<>(); - allSelectionBoxes = new ArrayList<>(); JPanel mainPanel = new JPanel(); mainPanel.setLayout(new BorderLayout()); @@ -132,10 +124,8 @@ void init(List trackConfigContainers) { JButton clearAllButton = new JButton("Clear All"); clearAllPanel.add(clearAllButton); clearAllButton.addActionListener(e -> { - for (SelectionBox box : allSelectionBoxes) { - if (box.isEnabled()) { - box.setSelected(false); - } + for (CollapsiblePanel collapsiblePanel : categoryPanels) { + collapsiblePanel.clearSelections(); } }); topButtonPanel.add(clearAllPanel, BorderLayout.EAST); @@ -213,177 +203,9 @@ public boolean isCanceled() { * @return */ private CollapsiblePanel createCategoryPanel(TrackConfigContainer configGroup) { - - JPanel trackPanel = new JPanel(); - trackPanel.setLayout(new BoxLayout(trackPanel, BoxLayout.Y_AXIS)); - - // There is a (so far) intractable bug if a large # of JCheckboxes are created for this widget. - int totalTrackCount = configGroup.countTracks(); - SelectionBox.CheckboxType checkboxType = totalTrackCount < 1000 ? SelectionBox.CheckboxType.SWING : SelectionBox.CheckboxType.CUSTOM; - - List selectionBoxes = addSelectionBoxes(null, configGroup, trackPanel, checkboxType); - - boolean isSelected = false; - int maxWidth = 0; - int selectionCount = 0; - for (SelectionBox selectionBox : selectionBoxes) { - if (selectionBox.checkbox.isSelected()) { - selectionCount++; - isSelected = true; - } - maxWidth = Math.max(maxWidth, selectionBox.getPreferredSize().width); - allSelectionBoxes.add(selectionBox); - } - - for (SelectionBox selectionBox : selectionBoxes) { - selectionBox.setPreferredWidth(maxWidth); - } - - String label = configGroup.label + " (" + selectionBoxes.size() + " tracks, " + selectionCount + " selected)"; - - final CollapsiblePanel collapsiblePanel = new CollapsiblePanel(label, trackPanel, isSelected || configGroup.defaultOpen); - - // Add a search button for categories with large numbers of records - final JButton searchButton = createSearchButton("Search " + configGroup.label, selectionBoxes, - (selectedCount) -> { - String l = configGroup.label + " (" + selectionBoxes.size() + " tracks, " + selectedCount + " selected)"; - collapsiblePanel.updateLabel(l); - return null; - }); - - collapsiblePanel.addSearchButton(searchButton); - - for (SelectionBox selectionBox : selectionBoxes) { - selectionBox.setCallback(b -> { - int selected = configGroup.countSelectedTracks(); - String l = configGroup.label + " (" + selectionBoxes.size() + " tracks, " + selected + " selected)"; - collapsiblePanel.updateLabel(l); - return null; - - }); - } - - return collapsiblePanel; + return new CollapsiblePanel(configGroup, this.autoselectDefaults); } - /** - * Add selection boxes for the container (a group, superTrack, compositeTrack, or view). Return true if any - * tracks are selected. - * - * @param labelPrefix - * @param container - * @param panel - * @param checkboxType - * @return - */ - private List addSelectionBoxes(String labelPrefix, - TrackConfigContainer container, - JPanel panel, - SelectionBox.CheckboxType checkboxType) { - - String title = labelPrefix == null ? "" : - labelPrefix + (labelPrefix.length() > 0 ? " - " : "") + container.label; - - List selectionBoxes = new ArrayList<>(); - - if (container.tracks.size() > 0) { - - JPanel trackPanel = new JPanel(); - if (labelPrefix != null) { - trackPanel.setBorder(BorderFactory.createTitledBorder(title)); - } - final WrapLayout wrapLayout = new WrapLayout(); - wrapLayout.setAlignment(FlowLayout.LEFT); - trackPanel.setLayout(wrapLayout); - - for (TrackConfig trackConfig : container.tracks) { - - SelectionBox selectionBox = new SelectionBox(trackConfig, checkboxType); - trackPanel.add(selectionBox); - selectionBoxes.add(selectionBox); - } - - panel.add(Box.createVerticalStrut(5)); - panel.add(trackPanel); - - } - - for (TrackConfigContainer childChild : container.children) { - selectionBoxes.addAll(addSelectionBoxes(title, childChild, panel, checkboxType)); - } - return selectionBoxes; - } - - private JButton createSearchButton(String label, List selectionBoxes, Function callback) { - - JButton searchButton = new JButton("Search"); - - searchButton.addActionListener(e -> { - - Set attributeNames = new LinkedHashSet<>(); - attributeNames.add("Name"); - attributeNames.add("Description"); - attributeNames.add("Format"); - - Map recordSelectionBoxMap = new HashMap<>(); - - List records = new ArrayList<>(); - - for (SelectionBox selectionBox : selectionBoxes) { - - if (selectionBox.isEnabled()) { - - TrackConfig trackConfig = selectionBox.getTrackConfig(); - final Map trackConfigAttributes = trackConfig.getAttributes(); - Map attributes = trackConfigAttributes; - if (attributes == null) { - attributes = new LinkedHashMap<>(); - } - - attributes.put("Name", trackConfig.getName()); - attributes.put("Description", trackConfig.getDescription()); - attributes.put("Format", trackConfig.getFormat()); - - if (trackConfigAttributes != null) { - attributes.putAll(trackConfigAttributes); - attributeNames.addAll(trackConfigAttributes.keySet()); - } - - final FileRecord record = new FileRecord(trackConfig.getUrl(), attributes); - record.setSelected(trackConfig.getVisible()); - records.add(record); - recordSelectionBoxMap.put(record, selectionBox); - } - } - - - List headings = new ArrayList<>(attributeNames); - // Limit # of columns - if (headings.size() > 15) { - headings = headings.subList(0, 15); - } - - Frame owner = IGV.hasInstance() ? IGV.getInstance().getMainFrame() : null; - - TrackChooser chooser = new TrackChooser( - owner, - headings, - records, - label); - chooser.setSize(this.getSize()); - chooser.setLocationRelativeTo(getOwner()); - chooser.setVisible(true); - - if (!chooser.isCanceled()) { - Set selectedRecords = new HashSet<>(chooser.getSelectedRecords()); - for (Map.Entry entry : recordSelectionBoxMap.entrySet()) { - entry.getValue().setSelected(selectedRecords.contains(entry.getKey())); - } - callback.apply(selectedRecords.size()); - } - }); - return searchButton; - } /** * Convenience method to extract and return selected track configurations. @@ -391,118 +213,12 @@ private JButton createSearchButton(String label, List selectionBox * @return */ public List getSelectedConfigs() { - return allSelectionBoxes.stream() - .filter(b -> b.isEnabled() && b.isSelected()) - .map(SelectionBox::getTrackConfig).toList(); - } - - static class SelectionBox extends JPanel { - - enum CheckboxType {SWING, CUSTOM} - - private final JLabel label; - private TrackConfig trackConfig; - private CheckBoxWrapper checkbox; - private int preferredWidth = -1; - private int minWidth; - private Function callback; - - public SelectionBox(TrackConfig trackConfig, CheckboxType checkboxType) { - - this.setLayout(new BorderLayout(5, 0)); - this.trackConfig = trackConfig; - - String longLabel = trackConfig.getLongLabel(); - if (longLabel != null) { - this.setToolTipText(longLabel); - } - - this.checkbox = new CheckBoxWrapper(checkboxType); - - checkbox.setActionListener(e -> { - trackConfig.setVisible(checkbox.isSelected()); - if (callback != null) { - callback.apply(checkbox.isSelected() ? 1 : 0); - } - }); - - label = new JLabel(trackConfig.getName()); - label.setLabelFor(checkbox.getComponent()); - add(checkbox.getComponent(), BorderLayout.WEST); - - String infoLink = trackConfig.getHtml(); - - if (infoLink == null || "".equals(infoLink.trim())) { - add(label, BorderLayout.CENTER); - } else { - ImageIcon icon = IconFactory.getInstance().getIcon(IconFactory.IconID.INFO); - JLabel iconLabel = new JLabel(icon); - iconLabel.setToolTipText(infoLink); - iconLabel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); - add(iconLabel, BorderLayout.CENTER); - iconLabel.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - try { - Desktop.getDesktop().browse(new URI(infoLink)); - } catch (Exception ex) { - log.error("Error following hyperlink: " + infoLink, ex); - } - } - }); - - JPanel panel = new JPanel(); - panel.setLayout(new FlowLayout(FlowLayout.LEFT)); - panel.add(label); - panel.add(iconLabel); - add(panel, BorderLayout.CENTER); - } - } - - public void setPreferredWidth(int width) { - minWidth = 200; - this.preferredWidth = Math.max(minWidth, width); - } - - @Override - public Dimension getPreferredSize() { - return preferredWidth > 0 ? new Dimension(preferredWidth, 20) : super.getPreferredSize(); - } - - @Override - public Dimension getMinimumSize() { - return getPreferredSize(); - } - - @Override - public Dimension getMaximumSize() { - return getPreferredSize(); - } - - public void setSelected(boolean selected) { - checkbox.setSelected(selected); - trackConfig.setVisible(selected); - } - - @Override - public void setEnabled(boolean enabled) { - super.setEnabled(enabled); - checkbox.setEnabled(enabled); - label.setEnabled(enabled); - } - - - public boolean isSelected() { - return checkbox.isSelected(); - } - - public TrackConfig getTrackConfig() { - return trackConfig; - } - public void setCallback(Function callback) { - this.callback = callback; + List selectedConfigs = new ArrayList<>(); + for (CollapsiblePanel collapsiblePanel : categoryPanels) { + selectedConfigs.addAll(collapsiblePanel.getSelectedTracks()); } + return selectedConfigs; } static class CheckBoxWrapper { @@ -510,8 +226,8 @@ static class CheckBoxWrapper { CheckBox checkBox; JCheckBox jCheckBox; - public CheckBoxWrapper(SelectionBox.CheckboxType checkboxType) { - if (checkboxType == SelectionBox.CheckboxType.SWING) { + public CheckBoxWrapper(CollapsiblePanel.SelectionBox.CheckboxType checkboxType) { + if (checkboxType == CollapsiblePanel.SelectionBox.CheckboxType.SWING) { jCheckBox = new JCheckBox(); } else { checkBox = new CheckBox(); From 913bbbf7ee5e71fb14091787fc7ad05aff3f3258 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Thu, 6 Mar 2025 13:34:48 -0800 Subject: [PATCH 127/130] Parsing fixes from more hub testing --- .../org/broad/igv/ucsc/hub/HubParser.java | 24 +++++-- .../org/broad/igv/ucsc/hub/TrackDbHub.java | 72 ++++++++++--------- .../igv/util/stream/SeekableSplitStream.java | 8 +-- 3 files changed, 59 insertions(+), 45 deletions(-) diff --git a/src/main/java/org/broad/igv/ucsc/hub/HubParser.java b/src/main/java/org/broad/igv/ucsc/hub/HubParser.java index f3a6037451..486e83f0c1 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/HubParser.java +++ b/src/main/java/org/broad/igv/ucsc/hub/HubParser.java @@ -90,13 +90,6 @@ public static Hub loadHub(String url, String genomeId) throws IOException { } } - // load includes. Nested includes (includes within includes) are not supported - List includes = stanzas.stream().filter(s -> "include".equals(s.type)).toList(); - for (Stanza s : includes) { - List includeStanzas = HubParser.loadStanzas(s.getProperty("include")); - trackStanzas.addAll(includeStanzas); - } - return new Hub(url, trackDbURL, hubStanza, genomeStanza, trackStanzas, groupStanzas); } @@ -137,6 +130,22 @@ static List loadStanzas(String url) throws IOException { continue; } + while (line.endsWith("\\")) { + String continuation = br.readLine(); + if (continuation == null) { + break; + } else { + line = line.substring(0, line.length() - 1) + " " + continuation.trim(); + } + } + + if(line.startsWith("include")) { + String relativeURL = line.substring(8).trim(); + String includeURL = getDataURL(relativeURL, host, baseURL); + List includeStanzas = HubParser.loadStanzas(includeURL); + nodes.addAll(includeStanzas); + } + int indent = indentLevel(line); int i = line.indexOf(" ", indent); if (i < 0) { @@ -147,6 +156,7 @@ static List loadStanzas(String url) throws IOException { if (key.startsWith("#")) continue; String value = line.substring(i + 1).trim(); + if (!("shortLabel".equals(key) || "longLabel".equals(key) || "metadata".equals(key))) { String[] tokens = Globals.whitespacePattern.split(value); value = tokens[0]; diff --git a/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java b/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java index 3d4d9cefbf..a7ae2a7c5b 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java +++ b/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java @@ -12,7 +12,7 @@ public class TrackDbHub { - static Set supportedTypes = new HashSet(Arrays.asList("bigBed", "bigWig", "bigGenePred", "vcfTabix", "refgene")); + static Set supportedTypes = new HashSet(Arrays.asList("bigBed", "bigWig", "bigGenePred", "vcfTabix", "refgene", "bam", "sampleInfo", "vcf.list")); static Set filterTracks = new HashSet(Arrays.asList("cytoBandIdeo", "assembly", "gap", "gapOverlap", "allGaps", "cpgIslandExtUnmasked", "windowMasker")); @@ -83,7 +83,8 @@ public List getGroupedTrackConfigurations(String hubName) String name = s.getProperty("track"); int priority = s.hasProperty("priority") ? getPriority(s.getProperty("priority")) : Integer.MAX_VALUE - 1; - final TrackConfigContainer container = new TrackConfigContainer(name, s.getProperty("shortLabel"), priority, false); + boolean defaultOpen = "0".equals(s.getProperty("defaultIsClosed")); + final TrackConfigContainer container = new TrackConfigContainer(name, s.getProperty("shortLabel"), priority, defaultOpen); if (trackContainers.containsKey(name)) { throw new RuntimeException("Duplicate track container: " + name); } @@ -128,7 +129,7 @@ private TrackConfig getTrackConfig(Stanza t) { TrackConfig config = new TrackConfig(url); String format = t.format(); - if(format != null) { + if (format != null) { config.setFormat(format.toLowerCase()); } @@ -243,38 +244,43 @@ static Map parseMetadata(String metadata) { Map attrs = new HashMap(); while (metadata.length() > 0) { - int idx = metadata.indexOf("="); - if (idx == -1) { - break; - } - int idx2; - String key = StringUtils.stripQuotes(capitalize(metadata.substring(0, idx))); - String value; - - if ('"' == metadata.charAt(idx + 1)) { - idx++; - idx2 = metadata.indexOf("\" ", idx + 1); - value = idx2 > 0 ? metadata.substring(idx + 1, idx2) : metadata.substring(idx + 1); - idx2++; - } else { - idx2 = metadata.indexOf(" "); - if (idx2 == -1) { - idx2 = metadata.length(); + try { + int idx = metadata.indexOf("="); + if (idx == -1 || idx == metadata.length() - 1) { + break; } - value = metadata.substring(idx + 1, idx2); - } - value = StringUtils.stripQuotes(value); - if (value.endsWith("\"")) { - value = value.substring(0, value.length() - 1); - } - if (value.startsWith("<") && value.endsWith(">")) { - value = htmlText(value); - } - attrs.put(key, value); - if (idx2 == metadata.length()) { - break; + int idx2; + String key = StringUtils.stripQuotes(capitalize(metadata.substring(0, idx))); + String value; + + if ('"' == metadata.charAt(idx + 1)) { + idx++; + idx2 = metadata.indexOf("\" ", idx + 1); + value = idx2 > 0 ? metadata.substring(idx + 1, idx2) : metadata.substring(idx + 1); + idx2++; + } else { + idx2 = metadata.indexOf(" ", idx + 1); + if (idx2 == -1) { + idx2 = metadata.length(); + } + value = metadata.substring(idx + 1, idx2); + } + value = StringUtils.stripQuotes(value); + if (value.endsWith("\"")) { + value = value.substring(0, value.length() - 1); + } + if (value.startsWith("<") && value.endsWith(">")) { + value = htmlText(value); + } + attrs.put(key, value); + if (idx2 == metadata.length()) { + break; + } + metadata = idx2 > 0 ? metadata.substring(idx2 + 1).trim() : ""; + } catch (Exception e) { + // We don't want to fail parsing the hub due to a failure parsing metadata. Also we don't want to + // overwhelm the log. Metatdata is or marginal importance in IGV. } - metadata = idx2 > 0 ? metadata.substring(idx2 + 1) : ""; } return attrs; } diff --git a/src/main/java/org/broad/igv/util/stream/SeekableSplitStream.java b/src/main/java/org/broad/igv/util/stream/SeekableSplitStream.java index 994aea7f68..7015b9056e 100644 --- a/src/main/java/org/broad/igv/util/stream/SeekableSplitStream.java +++ b/src/main/java/org/broad/igv/util/stream/SeekableSplitStream.java @@ -27,13 +27,11 @@ import htsjdk.samtools.seekablestream.SeekableStream; +import org.broad.igv.Globals; import org.broad.igv.util.HttpUtils; import org.broad.igv.util.ParsingUtils; -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; +import java.io.*; import java.net.URL; import java.util.ArrayList; import java.util.List; @@ -131,7 +129,7 @@ private void parseDescriptors(String path) throws IOException { br = new BufferedReader(new InputStreamReader(ParsingUtils.openInputStream(path))); String nextLine; while ((nextLine = br.readLine()) != null) { - String[] tokens = nextLine.split(" "); + String[] tokens = Globals.whitespacePattern.split(nextLine); if (tokens.length == 2) { String p = tokens[0]; From 99a96b6481c7d917d49e2dbc14c9e520c166f3a8 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Fri, 7 Mar 2025 10:35:58 -0800 Subject: [PATCH 128/130] Add "toBedgraph" option to igv tools gui (tdf -> bedgraph conversion) --- .../java/org/broad/igv/tools/IgvToolsGui.java | 196 ++++++++++-------- 1 file changed, 113 insertions(+), 83 deletions(-) diff --git a/src/main/java/org/broad/igv/tools/IgvToolsGui.java b/src/main/java/org/broad/igv/tools/IgvToolsGui.java index 4c7fb531ec..de048a1929 100644 --- a/src/main/java/org/broad/igv/tools/IgvToolsGui.java +++ b/src/main/java/org/broad/igv/tools/IgvToolsGui.java @@ -26,6 +26,7 @@ package org.broad.igv.tools; import org.broad.igv.logging.*; +import org.broad.igv.tdf.TDFUtils; import org.broad.igv.track.WindowFunction; import org.broad.igv.ui.util.FileDialogUtils; @@ -41,7 +42,7 @@ import java.util.ArrayList; import java.util.Collection; -public class IgvToolsGui extends org.broad.igv.ui.IGVDialog { +public class IgvToolsGui extends org.broad.igv.ui.IGVDialog { private static Logger log = LogManager.getLogger(IgvToolsGui.class); @@ -49,7 +50,8 @@ public enum Tool { COUNT("Count"), SORT("Sort"), INDEX("Index"), - TILE("toTDF"); + TO_TDF("toTDF"), + TO_BEDGRAPH("TDFtoBedGraph"),; private String displayName; @@ -279,7 +281,7 @@ private void updateUI() { extFactorField.setEnabled(false); disableWindowFunctions(); - } else if (tool.equals(Tool.TILE)) { + } else if (tool.equals(Tool.TO_TDF) || tool.equals(Tool.TO_BEDGRAPH)) { inputField.setEnabled(true); inputButton.setEnabled(true); outputField.setEnabled(true); @@ -390,9 +392,12 @@ private void run() { case INDEX: doIndex(); break; - case TILE: + case TO_TDF: doTile(); break; + case TO_BEDGRAPH: + doBedgraph(); + break; } } catch (PreprocessingException e) { showMessage("Error performing " + tool + ": " + e.getMessage()); @@ -495,6 +500,28 @@ protected Object doInBackground() { swingWorker.execute(); } + private void doBedgraph() { + + SwingWorker swingWorker = new IgvToolsSwingWorker() { + + @Override + protected Object doInBackground() { + try { + setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + String ifile = inputField.getText(); + String ofile = outputField.getText(); + runButton.setEnabled(false); + TDFUtils.tdfToBedgraph(ifile, ofile); + } catch (Exception e) { + showMessage("Error: " + e.getMessage()); + } + + return null; + } + }; + swingWorker.execute(); + } + // public static void doIndex(String ifile, int indexType, int binSize) throws IOException { private void doIndex() { @@ -564,7 +591,7 @@ private Collection getWindowFunctions() { * @return */ private File chooseFile() { - return FileDialogUtils.chooseFile("Select File: ", lastDirectory, FileDialogUtils.LOAD); + return FileDialogUtils.chooseFile("Select File: ", lastDirectory, FileDialogUtils.LOAD); } public static void main(String[] args) { @@ -617,9 +644,12 @@ static String getDefaultOutputText(String inputFieldText, Tool tool) { if (inputFieldText.length() > 0) { switch (tool) { case COUNT: - case TILE: + case TO_TDF: defaultOutputText = inputFieldText + ".tdf"; break; + case TO_BEDGRAPH: + defaultOutputText = inputFieldText + ".bedgraph"; + break; case SORT: int ext = inputFieldText.lastIndexOf("."); if (ext > 0) { @@ -714,32 +744,32 @@ public void actionPerformed(ActionEvent e) { } }); requiredPanel.add(toolCombo, new GridBagConstraints(2, 1, 1, 1, 1.0, 1.0, - GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, + new Insets(0, 0, 0, 0), 0, 0)); //---- label1 ---- label1.setText("Command"); requiredPanel.add(label1, new GridBagConstraints(1, 1, 1, 1, 0.0, 1.0, - GridBagConstraints.WEST, GridBagConstraints.NONE, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.WEST, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0)); //---- label2 ---- label2.setText("Input File"); requiredPanel.add(label2, new GridBagConstraints(1, 2, 1, 1, 0.0, 1.0, - GridBagConstraints.WEST, GridBagConstraints.NONE, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.WEST, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0)); //---- outputLabel ---- outputLabel.setText("Output File"); requiredPanel.add(outputLabel, new GridBagConstraints(1, 3, 1, 1, 0.0, 1.0, - GridBagConstraints.WEST, GridBagConstraints.NONE, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.WEST, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0)); //---- outputButton ---- outputButton.setText("Browse"); requiredPanel.add(outputButton, new GridBagConstraints(3, 3, 1, 1, 0.0, 1.0, - GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, + new Insets(0, 0, 0, 0), 0, 0)); //---- inputField ---- inputField.addFocusListener(new FocusAdapter() { @@ -755,8 +785,8 @@ public void actionPerformed(ActionEvent e) { } }); requiredPanel.add(inputField, new GridBagConstraints(2, 2, 1, 1, 1.0, 1.0, - GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, + new Insets(0, 0, 0, 0), 0, 0)); //---- inputButton ---- inputButton.setText("Browse"); @@ -767,31 +797,31 @@ public void actionPerformed(ActionEvent e) { } }); requiredPanel.add(inputButton, new GridBagConstraints(3, 2, 1, 1, 0.0, 1.0, - GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, + new Insets(0, 0, 0, 0), 0, 0)); requiredPanel.add(outputField, new GridBagConstraints(2, 3, 1, 1, 1.0, 1.0, - GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, + new Insets(0, 0, 0, 0), 0, 0)); //---- genomeLabel ---- genomeLabel.setToolTipText("Either a genome ID (e.g. hg18) or the full path to a .genome file."); genomeLabel.setText("Genome"); requiredPanel.add(genomeLabel, new GridBagConstraints(1, 4, 1, 1, 0.0, 1.0, - GridBagConstraints.WEST, GridBagConstraints.NONE, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.WEST, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0)); requiredPanel.add(genomeField, new GridBagConstraints(2, 4, 1, 1, 1.0, 1.0, - GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, + new Insets(0, 0, 0, 0), 0, 0)); //---- genomeButton ---- genomeButton.setText("Browse"); requiredPanel.add(genomeButton, new GridBagConstraints(3, 4, 1, 1, 0.0, 1.0, - GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, + new Insets(0, 0, 0, 0), 0, 0)); } mainPanel.add(requiredPanel, new GridBagConstraints(1, 1, 1, 1, 1.0, 0.0, - GridBagConstraints.CENTER, GridBagConstraints.BOTH, - new Insets(0, 0, 10, 0), 0, 0)); + GridBagConstraints.CENTER, GridBagConstraints.BOTH, + new Insets(0, 0, 10, 0), 0, 0)); //======== tilePanel ======== { @@ -804,15 +834,15 @@ public void actionPerformed(ActionEvent e) { zoomLabel.setToolTipText("Specifies the maximum zoom level to precompute. The default value is 7.
To reduce file size at the expense of Iperformance this value can be reduced."); zoomLabel.setText("Zoom Levels"); tilePanel.add(zoomLabel, new GridBagConstraints(1, 1, 1, 1, 0.0, 0.0, - GridBagConstraints.WEST, GridBagConstraints.NONE, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.WEST, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0)); //---- windowFunctionLabel ---- windowFunctionLabel.setToolTipText("Window functions to use for summarizing data. "); windowFunctionLabel.setText("Window Functions"); tilePanel.add(windowFunctionLabel, new GridBagConstraints(1, 2, 1, 1, 0.0, 0.0, - GridBagConstraints.NORTHWEST, GridBagConstraints.NONE, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.NORTHWEST, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0)); //======== windowFunctionPanel ======== { @@ -856,65 +886,65 @@ public void actionPerformed(ActionEvent e) { windowFunctionPanel.add(a98CheckBox); } tilePanel.add(windowFunctionPanel, new GridBagConstraints(2, 2, 1, 1, 1.0, 0.0, - GridBagConstraints.CENTER, GridBagConstraints.BOTH, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.CENTER, GridBagConstraints.BOTH, + new Insets(0, 0, 0, 0), 0, 0)); //---- probeLabel ---- probeLabel.setFont(probeLabel.getFont()); probeLabel.setToolTipText("Specifies a \"bed\" file to be used to map probe identifiers to locations. This option is useful
when preprocessing gct files. The bed file should contain 4 columns: chr start end name\n
where name is the probe name in the gct file."); probeLabel.setText("Probe to Loci Mapping"); tilePanel.add(probeLabel, new GridBagConstraints(1, 3, 1, 1, 0.0, 1.0, - GridBagConstraints.WEST, GridBagConstraints.NONE, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.WEST, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0)); tilePanel.add(probeField, new GridBagConstraints(2, 3, 1, 1, 1.0, 1.0, - GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, + new Insets(0, 0, 0, 0), 0, 0)); //---- probeButton ---- probeButton.setText("Browse"); tilePanel.add(probeButton, new GridBagConstraints(3, 3, 1, 1, 0.0, 1.0, - GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, + new Insets(0, 0, 0, 0), 0, 0)); //---- zoomCombo ---- zoomCombo.setEditable(false); - zoomCombo.setModel(new DefaultComboBoxModel(new String[] { + zoomCombo.setModel(new DefaultComboBoxModel(new String[]{ })); tilePanel.add(zoomCombo, new GridBagConstraints(2, 1, 1, 1, 1.0, 0.0, - GridBagConstraints.WEST, GridBagConstraints.NONE, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.WEST, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0)); //---- windowSizeLabel ---- windowSizeLabel.setToolTipText("The window size over which coverage computed when using the count command. Defaults to 25 bp."); windowSizeLabel.setText("Window Size"); tilePanel.add(windowSizeLabel, new GridBagConstraints(1, 4, 1, 1, 0.0, 0.0, - GridBagConstraints.WEST, GridBagConstraints.NONE, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.WEST, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0)); tilePanel.add(windowSizeField, new GridBagConstraints(2, 4, 1, 1, 1.0, 0.0, - GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, + new Insets(0, 0, 0, 0), 0, 0)); //---- extFactorLabel ---- extFactorLabel.setText("Extension Factor"); extFactorLabel.setToolTipText("Extend read by this amount. Set to average fragment size of library"); tilePanel.add(extFactorLabel, new GridBagConstraints(1, 5, 1, 1, 0.0, 0.0, - GridBagConstraints.CENTER, GridBagConstraints.BOTH, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.CENTER, GridBagConstraints.BOTH, + new Insets(0, 0, 0, 0), 0, 0)); tilePanel.add(extFactorField, new GridBagConstraints(2, 5, 1, 1, 0.0, 0.0, - GridBagConstraints.CENTER, GridBagConstraints.BOTH, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.CENTER, GridBagConstraints.BOTH, + new Insets(0, 0, 0, 0), 0, 0)); //---- pairsCB ---- pairsCB.setText("Count as Pairs"); pairsCB.setToolTipText("Count area between proper pairs as covered."); tilePanel.add(pairsCB, new GridBagConstraints(1, 6, 1, 1, 0.0, 0.0, - GridBagConstraints.CENTER, GridBagConstraints.BOTH, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.CENTER, GridBagConstraints.BOTH, + new Insets(0, 0, 0, 0), 0, 0)); } mainPanel.add(tilePanel, new GridBagConstraints(1, 2, 1, 1, 1.0, 1.0, - GridBagConstraints.CENTER, GridBagConstraints.BOTH, - new Insets(0, 0, 10, 0), 0, 0)); + GridBagConstraints.CENTER, GridBagConstraints.BOTH, + new Insets(0, 0, 10, 0), 0, 0)); //======== sortPanel ======== { @@ -925,31 +955,31 @@ public void actionPerformed(ActionEvent e) { tmpDirectoryLabel.setToolTipText("Specify a temporary working directory. For large input files this directory will be used to
store intermediate results of the sort. The default is the users temp directory."); tmpDirectoryLabel.setText("Temp Directory"); sortPanel.add(tmpDirectoryLabel, new GridBagConstraints(1, 1, 1, 1, 0.0, 1.0, - GridBagConstraints.WEST, GridBagConstraints.NONE, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.WEST, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0)); //---- maxRecordsLabel ---- maxRecordsLabel.setToolTipText("The maximum number of records to keep in memory during the sort. The default value is
500000. Increase this number if you receive \"too many open files\" errors. Decrease it if you
experience \"out of memory\" errors."); maxRecordsLabel.setText("Max Records"); sortPanel.add(maxRecordsLabel, new GridBagConstraints(1, 2, 1, 1, 0.0, 1.0, - GridBagConstraints.WEST, GridBagConstraints.NONE, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.WEST, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0)); //---- tempButton ---- tempButton.setText("Browse"); sortPanel.add(tempButton, new GridBagConstraints(4, 1, 1, 1, 0.0, 1.0, - GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, + new Insets(0, 0, 0, 0), 0, 0)); sortPanel.add(tmpDirectoryField, new GridBagConstraints(3, 1, 1, 1, 1.0, 1.0, - GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, + new Insets(0, 0, 0, 0), 0, 0)); sortPanel.add(maxRecordsField, new GridBagConstraints(3, 2, 1, 1, 1.0, 1.0, - GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, + new Insets(0, 0, 0, 0), 0, 0)); } mainPanel.add(sortPanel, new GridBagConstraints(1, 3, 1, 1, 1.0, 1.0, - GridBagConstraints.CENTER, GridBagConstraints.BOTH, - new Insets(0, 0, 10, 0), 0, 0)); + GridBagConstraints.CENTER, GridBagConstraints.BOTH, + new Insets(0, 0, 10, 0), 0, 0)); //======== buttonPanel ======== { @@ -958,18 +988,18 @@ public void actionPerformed(ActionEvent e) { //---- runButton ---- runButton.setText("Run"); buttonPanel.add(runButton, new GridBagConstraints(2, 0, 1, 1, 0.0, 1.0, - GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, + new Insets(0, 0, 0, 0), 0, 0)); //---- closeButton ---- closeButton.setText("Close"); buttonPanel.add(closeButton, new GridBagConstraints(1, 0, 1, 1, 0.0, 1.0, - GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, + new Insets(0, 0, 0, 0), 0, 0)); } mainPanel.add(buttonPanel, new GridBagConstraints(1, 4, 1, 1, 1.0, 0.0, - GridBagConstraints.CENTER, GridBagConstraints.BOTH, - new Insets(0, 0, 10, 0), 0, 0)); + GridBagConstraints.CENTER, GridBagConstraints.BOTH, + new Insets(0, 0, 10, 0), 0, 0)); //======== OutputPanel ======== { @@ -990,7 +1020,7 @@ public void actionPerformed(ActionEvent e) { { // compute preferred size Dimension preferredSize = new Dimension(); - for(int i = 0; i < OutputPanel.getComponentCount(); i++) { + for (int i = 0; i < OutputPanel.getComponentCount(); i++) { Rectangle bounds = OutputPanel.getComponent(i).getBounds(); preferredSize.width = Math.max(bounds.x + bounds.width, preferredSize.width); preferredSize.height = Math.max(bounds.y + bounds.height, preferredSize.height); @@ -1003,14 +1033,14 @@ public void actionPerformed(ActionEvent e) { } } mainPanel.add(OutputPanel, new GridBagConstraints(1, 6, 1, 1, 1.0, 0.0, - GridBagConstraints.CENTER, GridBagConstraints.BOTH, - new Insets(0, 0, 10, 0), 0, 0)); + GridBagConstraints.CENTER, GridBagConstraints.BOTH, + new Insets(0, 0, 10, 0), 0, 0)); mainPanel.add(separator1, new GridBagConstraints(1, 5, 1, 1, 1.0, 0.0, - GridBagConstraints.CENTER, GridBagConstraints.BOTH, - new Insets(0, 0, 10, 0), 0, 0)); + GridBagConstraints.CENTER, GridBagConstraints.BOTH, + new Insets(0, 0, 10, 0), 0, 0)); mainPanel.add(progressBar, new GridBagConstraints(1, 7, 1, 1, 1.0, 0.0, - GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, - new Insets(0, 0, 0, 0), 0, 0)); + GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, + new Insets(0, 0, 0, 0), 0, 0)); } // JFormDesigner - End of component initialization //GEN-END:initComponents } @@ -1065,7 +1095,7 @@ public void actionPerformed(ActionEvent e) { private JProgressBar progressBar; // JFormDesigner - End of variables declaration //GEN-END:variables - private abstract class IgvToolsSwingWorker extends SwingWorker{ + private abstract class IgvToolsSwingWorker extends SwingWorker { @Override protected void done() { From bc268b9d07a0842175cf7f81459fd7887cb3f481 Mon Sep 17 00:00:00 2001 From: Jim Robinson <933148+jrobinso@users.noreply.github.com> Date: Sun, 9 Mar 2025 23:26:41 -0700 Subject: [PATCH 129/130] Refactoring, streamline genome list (#1670) * Refactoring, streamline genome list --- .../org/broad/igv/feature/genome/Genome.java | 43 ++++-- .../igv/feature/genome/load/GenomeConfig.java | 15 +- .../feature/genome/load/HubGenomeLoader.java | 15 +- .../feature/genome/load/JsonGenomeLoader.java | 4 - src/main/java/org/broad/igv/ucsc/hub/Hub.java | 4 + .../org/broad/igv/ucsc/hub/HubLoader.java | 12 ++ .../org/broad/igv/ucsc/hub/HubParser.java | 21 ++- .../org/broad/igv/ucsc/hub/TrackDbHub.java | 5 +- .../java/org/broad/igv/ui/IGVMenuBar.java | 146 +++++++++--------- .../igv/ui/action/LoadFromServerAction.java | 32 ++-- .../igv/ui/action/LoadFromURLMenuAction.java | 38 +++-- .../igv/ui/action/SelectHubTracksAction.java | 72 ++++----- .../org/broad/igv/prefs/preferences.tab | 4 +- 13 files changed, 246 insertions(+), 165 deletions(-) create mode 100644 src/main/java/org/broad/igv/ucsc/hub/HubLoader.java diff --git a/src/main/java/org/broad/igv/feature/genome/Genome.java b/src/main/java/org/broad/igv/feature/genome/Genome.java index 9a7ac4a887..01773ed161 100644 --- a/src/main/java/org/broad/igv/feature/genome/Genome.java +++ b/src/main/java/org/broad/igv/feature/genome/Genome.java @@ -74,7 +74,7 @@ public class Genome { private static final int MAX_WHOLE_GENOME_LONG = 100; private static Logger log = LogManager.getLogger(Genome.class); - GenomeConfig config; + private String id; private String displayName; private List chromosomeNames; @@ -98,9 +98,21 @@ public class Genome { private String defaultPos; private String nameSet; private List trackHubs; + private GenomeConfig config; + + + private static Genome nullGenome = null; + + public synchronized static Genome nullGenome() { + if (nullGenome == null) { + nullGenome = new Genome("None", Arrays.asList(new Chromosome(0, "", 0))); + } + return nullGenome; + } - public Genome(GenomeConfig config) throws IOException { + public Genome(GenomeConfig config) throws IOException { + this.config = config; id = config.getId(); displayName = config.getName(); nameSet = config.getNameSet(); @@ -180,7 +192,7 @@ public Genome(GenomeConfig config) throws IOException { } } else { // No chromosome list. Try to fetch chromosome names from the sequence - if(this.chromosomeNames.isEmpty()) { + if (this.chromosomeNames.isEmpty()) { this.chromosomeNames = sequence.getChromosomeNames(); } } @@ -228,8 +240,8 @@ public Genome(GenomeConfig config) throws IOException { // TODO -- no place to go } - if(config.getHubs() != null) { - for(String hubUrl : config.getHubs()) { + if (config.getHubs() != null) { + for (String hubUrl : config.getHubs()) { try { trackHubs.add(HubParser.loadHub(hubUrl, getUCSCId())); } catch (Exception e) { @@ -315,7 +327,7 @@ public String getCanonicalChrName(String str) { try { ChromAlias aliasRecord = chromAliasSource.search(str); - if(aliasRecord == null && !str.equals(str.toLowerCase())) { + if (aliasRecord == null && !str.equals(str.toLowerCase())) { aliasRecord = chromAliasSource.search(str.toLowerCase()); } @@ -817,12 +829,21 @@ public List getTrackHubs() { return trackHubs; } - public synchronized static Genome nullGenome() { - if(nullGenome == null) { - nullGenome = new Genome("None", Arrays.asList(new Chromosome(0, "", 0))); + public void addTrackHub(Hub hub) { + + if(!trackHubs.stream().anyMatch(h -> h.getUrl().equals(hub.getUrl()))) { + trackHubs.add(hub); + if (config.isFromJson()) { + config.addHub(hub.getUrl()); + } } - return nullGenome; } - private static Genome nullGenome = null; + public boolean isFromJson() { + return config != null && config.isFromJson(); + } + + public GenomeConfig getConfig() { + return config; + } } diff --git a/src/main/java/org/broad/igv/feature/genome/load/GenomeConfig.java b/src/main/java/org/broad/igv/feature/genome/load/GenomeConfig.java index ddd931ebbd..74273760f6 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/GenomeConfig.java +++ b/src/main/java/org/broad/igv/feature/genome/load/GenomeConfig.java @@ -61,12 +61,15 @@ public class GenomeConfig implements Cloneable { private Sequence sequence; private LinkedHashMap> cytobands; private List> chromAliases; + private boolean fromJson = false; // Until proven otherwise public static GenomeConfig fromJson(String json) { if (json.contains("chromosomeOrder")) { json = fixChromosomeOrder(json); } - return (new Gson()).fromJson(json, GenomeConfig.class); + GenomeConfig config = (new Gson()).fromJson(json, GenomeConfig.class); + config.fromJson = true; + return config; } public GenomeConfig() { @@ -84,6 +87,13 @@ public void setHubs(List hubs) { this.hubs = hubs; } + public void addHub(String hub) { + if (hubs == null) { + hubs = new ArrayList(); + } + hubs.add(hub); + } + public String getId() { return id; } @@ -339,4 +349,7 @@ private static String fixChromosomeOrder(String jsonString) { return (new Gson()).toJson(obj); } + public boolean isFromJson() { + return fromJson; + } } diff --git a/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java b/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java index fd462edd68..56180288c3 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java +++ b/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java @@ -31,7 +31,7 @@ public class HubGenomeLoader extends GenomeLoader { static Logger log = LogManager.getLogger(HubGenomeLoader.class); public static boolean isHubURL(String obj) { - return obj.endsWith("/hub.txt"); + return obj != null && obj.toLowerCase().endsWith("hub.txt"); // <= very crude, perhaps read the first line } public static String convertToHubURL(String accension) { @@ -90,9 +90,22 @@ protected void done() { }; worker.execute(); + } + /** + * Shortcut method provided to support load-from-url. The hub must be read and parsed to determine if it is an + * assembly hub. This method avoids the need to load and parse again. + * + * @param hub + * @throws IOException + */ + public static void loadAssemblyHub(Hub hub) throws IOException { + final GenomeConfig config = getGenomeConfig(hub); + File genomeFile = GenomeDownloadUtils.saveLocalGenome(config); + GenomeManager.getInstance().loadGenome(genomeFile.getAbsolutePath()); } + private static GenomeConfig getGenomeConfig(Hub hub) throws IOException { GenomeConfig config = hub.getGenomeConfig(); diff --git a/src/main/java/org/broad/igv/feature/genome/load/JsonGenomeLoader.java b/src/main/java/org/broad/igv/feature/genome/load/JsonGenomeLoader.java index b2361fa489..3fdd36f56b 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/JsonGenomeLoader.java +++ b/src/main/java/org/broad/igv/feature/genome/load/JsonGenomeLoader.java @@ -63,13 +63,9 @@ public Genome loadGenome() throws IOException { public GenomeConfig loadGenomeConfig() throws IOException { try (InputStream is = ParsingUtils.openInputStream(genomePath)) { - String jsonString = ParsingUtils.readContentsFromStream(is); - GenomeConfig genomeConfig = GenomeConfig.fromJson(jsonString); - fixPaths(genomeConfig); - return genomeConfig; } diff --git a/src/main/java/org/broad/igv/ucsc/hub/Hub.java b/src/main/java/org/broad/igv/ucsc/hub/Hub.java index 753620e397..244c714d7f 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/Hub.java +++ b/src/main/java/org/broad/igv/ucsc/hub/Hub.java @@ -50,6 +50,10 @@ public class Hub { } } + public boolean isAssemblyHub() { + return genomeStanza.hasProperty("twoBitPath"); + } + public String getShortLabel() { return hubStanza.hasProperty("shortLabel") ? hubStanza.getProperty("shortLabel") : this.url; diff --git a/src/main/java/org/broad/igv/ucsc/hub/HubLoader.java b/src/main/java/org/broad/igv/ucsc/hub/HubLoader.java new file mode 100644 index 0000000000..6c24717c75 --- /dev/null +++ b/src/main/java/org/broad/igv/ucsc/hub/HubLoader.java @@ -0,0 +1,12 @@ +package org.broad.igv.ucsc.hub; + +import org.broad.igv.ui.IGV; + +public class HubLoader { + + + static void loadHub(Hub hub) { + + + } +} diff --git a/src/main/java/org/broad/igv/ucsc/hub/HubParser.java b/src/main/java/org/broad/igv/ucsc/hub/HubParser.java index 486e83f0c1..d27489796a 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/HubParser.java +++ b/src/main/java/org/broad/igv/ucsc/hub/HubParser.java @@ -25,10 +25,6 @@ public static Hub loadAssemblyHub(String url) throws IOException { public static Hub loadHub(String url, String genomeId) throws IOException { - boolean assemblyHub = genomeId == null; - - int idx = url.lastIndexOf("/"); - // Load stanzas from the hub.txt file, which might be all stanzas if 'useOneFile' is on List stanzas = HubParser.loadStanzas(url); @@ -51,6 +47,14 @@ public static Hub loadHub(String url, String genomeId) throws IOException { throw new RuntimeException("Unexpected hub file -- expected 'genome' stanza but found " + stanzas.get(1).type); } genomeStanza = stanzas.get(1); + + if(!genomeStanza.hasProperty("twoBitPath")) { + // Not an assembly hub, validate genome + if(!genomeStanza.getProperty("genome").equals(genomeId)) { + throw new RuntimeException("Hub file does not contain tracks for genome " + genomeId); + } + } + trackStanzas = new ArrayList<>(stanzas.subList(2, stanzas.size())); } else { @@ -63,6 +67,7 @@ public static Hub loadHub(String url, String genomeId) throws IOException { List genomeStanzaList = HubParser.loadStanzas(hubStanza.getProperty("genomesFile")); // Find stanza for requested genome, or if no requested genome the first. + for (Stanza s : genomeStanzaList) { if (genomeId == null || genomeId.equals(s.getProperty("genome"))) { stanzas.add(s); @@ -71,16 +76,16 @@ public static Hub loadHub(String url, String genomeId) throws IOException { break; } } + if(trackDbURL == null) { + throw new RuntimeException("Hub file does not contain tracks for genome " + genomeId); + } } // Assembly hub validation - if (assemblyHub) { + if (genomeId == null ) { if (!genomeStanza.hasProperty("twoBitPath")) { throw new RuntimeException("Assembly hubs must specify 'twoBitPath'"); } - if (!genomeStanza.hasProperty("groups")) { - throw new RuntimeException("Assembly hubs must specify 'groups'"); - } } if (genomeStanza.hasProperty("groups")) { diff --git a/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java b/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java index a7ae2a7c5b..a7db40fa11 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java +++ b/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java @@ -12,7 +12,8 @@ public class TrackDbHub { - static Set supportedTypes = new HashSet(Arrays.asList("bigBed", "bigWig", "bigGenePred", "vcfTabix", "refgene", "bam", "sampleInfo", "vcf.list")); + static Set supportedTypes = new HashSet(Arrays.asList("bigbed", "bigwig", "biggenepred", "vcftabix", "refgene", + "bam", "sampleinfo", "vcf.list", "ucscsnp", "bed", "tdf", "gff", "gff3", "gtf")); static Set filterTracks = new HashSet(Arrays.asList("cytoBandIdeo", "assembly", "gap", "gapOverlap", "allGaps", "cpgIslandExtUnmasked", "windowMasker")); @@ -99,7 +100,7 @@ public List getGroupedTrackConfigurations(String hubName) } else if (!filterTracks.contains(s.name) && s.hasProperty("bigDataUrl") && - supportedTypes.contains(s.format())) { + supportedTypes.contains(s.format().toLowerCase())) { final TrackConfig trackConfig = getTrackConfig(s); if (parent != null) { diff --git a/src/main/java/org/broad/igv/ui/IGVMenuBar.java b/src/main/java/org/broad/igv/ui/IGVMenuBar.java index c8141032e5..e62e80cc94 100644 --- a/src/main/java/org/broad/igv/ui/IGVMenuBar.java +++ b/src/main/java/org/broad/igv/ui/IGVMenuBar.java @@ -271,111 +271,102 @@ public void enableExtrasMenu() { JMenu createFileMenu(Genome genome) { - if (genome == null) { - return new JMenu("File"); - } - - String genomeId = genome.getId(); - - List menuItems = new ArrayList<>(); + final JMenu menu = new JMenu("File"); MenuAction menuAction; - menuItems.add(new JSeparator()); + menu.add(new JSeparator()); // Load menu items menuAction = new LoadFilesMenuAction("Load from File...", KeyEvent.VK_L, igv); menuAction.setToolTipText(UIConstants.LOAD_TRACKS_TOOLTIP); - menuItems.add(MenuAndToolbarUtils.createMenuItem(menuAction)); + menu.add(MenuAndToolbarUtils.createMenuItem(menuAction)); menuAction = new LoadFromURLMenuAction(LoadFromURLMenuAction.LOAD_FROM_URL, KeyEvent.VK_U, igv); menuAction.setToolTipText(UIConstants.LOAD_TRACKS_TOOLTIP); - menuItems.add(MenuAndToolbarUtils.createMenuItem(menuAction)); + menu.add(MenuAndToolbarUtils.createMenuItem(menuAction)); - if (genomeId != null && - genome.getTrackHubs().isEmpty() && - LoadFromServerAction.getNodeURLs(genomeId) != null) { + if (genome != null && LoadFromServerAction.getNodeURLs(genome.getId()) != null) { menuAction = new LoadFromServerAction("Load from Server...", KeyEvent.VK_S, igv); menuAction.setToolTipText(UIConstants.LOAD_SERVER_DATA_TOOLTIP); JMenuItem loadTracksFromServerMenuItem = MenuAndToolbarUtils.createMenuItem(menuAction); - menuItems.add(loadTracksFromServerMenuItem); + menu.add(loadTracksFromServerMenuItem); } recentFilesMenu = new RecentUrlsMenu(); - menuItems.add(recentFilesMenu); + menu.add(recentFilesMenu); // Track hubs - if (genome.getTrackHubs().size() > 0) { - menuItems.add(new JSeparator()); + if (genome != null && genome.getTrackHubs().size() > 0) { + menu.add(new JSeparator()); for (Hub trackHub : genome.getTrackHubs()) { - menuAction = new SelectHubTracksAction("Hub: " + trackHub.getShortLabel(), igv, trackHub); - menuAction.setToolTipText(trackHub.getLongLabel()); - JMenuItem selectGenomeAnnotationsItem = MenuAndToolbarUtils.createMenuItem(menuAction); - selectGenomeAnnotationsItem.setToolTipText(trackHub.getLongLabel()); - menuItems.add(selectGenomeAnnotationsItem); + menu.add(createTrackHubItem(trackHub)); } } // ENCODE items. These will be hidden / shown depending on genome chosen - if (EncodeTrackChooserFactory.genomeSupportedUCSC(genomeId) || EncodeTrackChooserFactory.genomeSupported(genomeId)) { - - JSeparator separator = new JSeparator(); - menuItems.add(separator); - - // Post 2012 ENCODE menu - if (EncodeTrackChooserFactory.genomeSupported(genomeId)) { - JMenuItem chipItem = new JMenuItem(); - chipItem.setAction(new BrowseEncodeAction("ENCODE ChIP Signals ...", 0, BrowseEncodeAction.Type.SIGNALS_CHIP, igv)); - menuItems.add(chipItem); - - JMenuItem otherSignalsItem = new JMenuItem(); - otherSignalsItem.setAction(new BrowseEncodeAction("ENCODE Other Signals ...", 0, BrowseEncodeAction.Type.SIGNALS_OTHER, igv)); - menuItems.add(otherSignalsItem); - - JMenuItem otherItem = new JMenuItem(); - otherItem.setAction(new BrowseEncodeAction("ENCODE Other ...", 0, BrowseEncodeAction.Type.OTHER, igv)); - menuItems.add(otherItem); - } + if (genome != null) { + String genomeId = genome.getUCSCId(); + if (EncodeTrackChooserFactory.genomeSupportedUCSC(genomeId) || EncodeTrackChooserFactory.genomeSupported(genomeId)) { + + JSeparator separator = new JSeparator(); + menu.add(separator); + + // Post 2012 ENCODE menu + if (EncodeTrackChooserFactory.genomeSupported(genomeId)) { + JMenuItem chipItem = new JMenuItem(); + chipItem.setAction(new BrowseEncodeAction("ENCODE ChIP Signals ...", 0, BrowseEncodeAction.Type.SIGNALS_CHIP, igv)); + menu.add(chipItem); + + JMenuItem otherSignalsItem = new JMenuItem(); + otherSignalsItem.setAction(new BrowseEncodeAction("ENCODE Other Signals ...", 0, BrowseEncodeAction.Type.SIGNALS_OTHER, igv)); + menu.add(otherSignalsItem); + + JMenuItem otherItem = new JMenuItem(); + otherItem.setAction(new BrowseEncodeAction("ENCODE Other ...", 0, BrowseEncodeAction.Type.OTHER, igv)); + menu.add(otherItem); + } - // UCSC hosted ENCODE menu. - if (EncodeTrackChooserFactory.genomeSupportedUCSC(genomeId)) { - JMenuItem encodeUCSCMenuItem = MenuAndToolbarUtils.createMenuItem( - new BrowseEncodeAction("ENCODE 2012 UCSC Repository ...", KeyEvent.VK_E, BrowseEncodeAction.Type.UCSC, igv)); - menuItems.add(encodeUCSCMenuItem); + // UCSC hosted ENCODE menu. + if (EncodeTrackChooserFactory.genomeSupportedUCSC(genomeId)) { + JMenuItem encodeUCSCMenuItem = MenuAndToolbarUtils.createMenuItem( + new BrowseEncodeAction("ENCODE 2012 UCSC Repository ...", KeyEvent.VK_E, BrowseEncodeAction.Type.UCSC, igv)); + menu.add(encodeUCSCMenuItem); + } } } - menuItems.add(new JSeparator()); + menu.add(new JSeparator()); menuAction = new ReloadTracksMenuAction("Reload Tracks", -1, igv); menuAction.setToolTipText(RELOAD_SESSION_TOOLTIP); - menuItems.add(MenuAndToolbarUtils.createMenuItem(menuAction)); + menu.add(MenuAndToolbarUtils.createMenuItem(menuAction)); - menuItems.add(new JSeparator()); + menu.add(new JSeparator()); // Session menu items menuAction = new NewSessionMenuAction("New Session...", KeyEvent.VK_N, igv); menuAction.setToolTipText(UIConstants.NEW_SESSION_TOOLTIP); - menuItems.add(MenuAndToolbarUtils.createMenuItem(menuAction)); + menu.add(MenuAndToolbarUtils.createMenuItem(menuAction)); menuAction = new OpenSessionMenuAction("Open Session...", KeyEvent.VK_O, igv); menuAction.setToolTipText(OPEN_SESSION_TOOLTIP); - menuItems.add(MenuAndToolbarUtils.createMenuItem(menuAction)); + menu.add(MenuAndToolbarUtils.createMenuItem(menuAction)); menuAction = new SaveSessionMenuAction("Save Session...", KeyEvent.VK_V, igv); menuAction.setToolTipText(UIConstants.SAVE_SESSION_TOOLTIP); JMenuItem saveSessionItem = MenuAndToolbarUtils.createMenuItem(menuAction); - menuItems.add(saveSessionItem); + menu.add(saveSessionItem); menuAction = new ReloadSessionMenuAction("Reload Session", -1, igv); menuAction.setToolTipText(RELOAD_SESSION_TOOLTIP); reloadSessionItem = MenuAndToolbarUtils.createMenuItem(menuAction); reloadSessionItem.setEnabled(false); - menuItems.add(reloadSessionItem); + menu.add(reloadSessionItem); autosaveMenu = new AutosaveMenu(); - menuItems.add(autosaveMenu); + menu.add(autosaveMenu); - menuItems.add(new JSeparator()); + menu.add(new JSeparator()); // ***** Snapshots // Snapshot Application @@ -389,7 +380,7 @@ public void actionPerformed(ActionEvent e) { }; menuAction.setToolTipText(SAVE_PNG_IMAGE_TOOLTIP); - menuItems.add(MenuAndToolbarUtils.createMenuItem(menuAction)); + menu.add(MenuAndToolbarUtils.createMenuItem(menuAction)); menuAction = new MenuAction("Save SVG Image ...", null) { @@ -401,10 +392,10 @@ public void actionPerformed(ActionEvent e) { }; menuAction.setToolTipText(SAVE_SVG_IMAGE_TOOLTIP); - menuItems.add(MenuAndToolbarUtils.createMenuItem(menuAction)); + menu.add(MenuAndToolbarUtils.createMenuItem(menuAction)); // TODO -- change "Exit" to "Close" for BioClipse - menuItems.add(new JSeparator()); // Exit + menu.add(new JSeparator()); // Exit menuAction = new MenuAction("Exit", null, KeyEvent.VK_X) { @@ -415,25 +406,33 @@ public void actionPerformed(ActionEvent e) { }; menuAction.setToolTipText(EXIT_TOOLTIP); - menuItems.add(MenuAndToolbarUtils.createMenuItem(menuAction)); + menu.add(MenuAndToolbarUtils.createMenuItem(menuAction)); JSeparator recentSessionsSep = new JSeparator(); recentSessionsSep.setVisible(false); - menuItems.add(recentSessionsSep); - //menuItems.addAll(addRecentSessionMenuItems()); - menuItems.add(new JSeparator()); + menu.add(recentSessionsSep); + //menu.addAll(addRecentSessionMenuItems()); + menu.add(new JSeparator()); ; MenuAction fileMenuAction = new MenuAction("File", null, KeyEvent.VK_F); - JMenu fileMenu = MenuAndToolbarUtils.createMenu(menuItems, fileMenuAction); //Add dynamic list of recent sessions - fileMenu.addMenuListener(new DynamicMenuItemsAdjustmentListener<>( - fileMenu, + menu.addMenuListener(new DynamicMenuItemsAdjustmentListener<>( + menu, recentSessionsSep, IGV.getInstance().getRecentSessionList(), session -> MenuAndToolbarUtils.createMenuItem(new OpenSessionMenuAction(session, IGV.getInstance()))) ); - return fileMenu; + return menu; + } + + private JMenuItem createTrackHubItem(Hub trackHub) { + MenuAction menuAction; + menuAction = new SelectHubTracksAction("Hub: " + trackHub.getShortLabel(), igv, trackHub); + menuAction.setToolTipText(trackHub.getLongLabel()); + JMenuItem selectHubTracksItem = MenuAndToolbarUtils.createMenuItem(menuAction); + selectHubTracksItem.setToolTipText(trackHub.getLongLabel()); + return selectHubTracksItem; } private JMenu createGenomesMenu() { @@ -1164,15 +1163,18 @@ static void destroyInstance() { public void receiveEvent(final IGVEvent event) { if (event instanceof GenomeChangeEvent) { - UIUtilities.invokeOnEventThread(() -> { - IGVMenuBar.this.remove(0); - final Genome genome = ((GenomeChangeEvent) event).genome(); - JMenu fileMenu = createFileMenu(genome); - IGVMenuBar.this.add(fileMenu, 0); - }); + updateFileMenu(((GenomeChangeEvent) event).genome()); } } + public synchronized void updateFileMenu(Genome genome) { + UIUtilities.invokeOnEventThread(() -> { + IGVMenuBar.this.remove(0); + JMenu fileMenu = createFileMenu(genome); + IGVMenuBar.this.add(fileMenu, 0); + }); + } + public void enableReloadSession() { this.reloadSessionItem.setEnabled(true); } diff --git a/src/main/java/org/broad/igv/ui/action/LoadFromServerAction.java b/src/main/java/org/broad/igv/ui/action/LoadFromServerAction.java index 9403742b9a..2e63fd67eb 100644 --- a/src/main/java/org/broad/igv/ui/action/LoadFromServerAction.java +++ b/src/main/java/org/broad/igv/ui/action/LoadFromServerAction.java @@ -71,7 +71,7 @@ public LoadFromServerAction(String label, int mnemonic, IGV mainFrame) { public static String getGenomeDataURL(String genomeId) { String urlString = PreferencesManager.getPreferences().getDataServerURL(); - String genomeURL = urlString.replaceAll("\\$\\$", genomeId); + String genomeURL = urlString == null ? null : urlString.replaceAll("\\$\\$", genomeId); return genomeURL; } @@ -105,21 +105,23 @@ public void actionPerformed(ActionEvent evt) { public static LinkedHashSet getNodeURLs(String genomeId) { String genomeURL = getGenomeDataURL(genomeId); - - InputStream is = null; LinkedHashSet nodeURLs = null; - try { - is = ParsingUtils.openInputStreamGZ(new ResourceLocator(genomeURL)); - BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is)); - nodeURLs = getResourceUrls(bufferedReader); - } catch (IOException e) { - //This is common and not an error, a load-from-server "data registry" file is optional - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException e) { - log.error("Error closing input stream", e); + + if(genomeURL != null && genomeURL.length() > 0) { + InputStream is = null; + try { + is = ParsingUtils.openInputStreamGZ(new ResourceLocator(genomeURL)); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is)); + nodeURLs = getResourceUrls(bufferedReader); + } catch (IOException e) { + //This is common and not an error, a load-from-server "data registry" file is optional + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + log.error("Error closing input stream", e); + } } } } diff --git a/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java b/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java index f58818d28a..ff43fce459 100644 --- a/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java +++ b/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java @@ -31,11 +31,15 @@ import org.broad.igv.Globals; import org.broad.igv.feature.genome.Genome; +import org.broad.igv.feature.genome.GenomeDownloadUtils; import org.broad.igv.feature.genome.load.HubGenomeLoader; import org.broad.igv.logging.*; import org.broad.igv.feature.genome.GenomeManager; import org.broad.igv.session.SessionReader; +import org.broad.igv.ucsc.hub.Hub; +import org.broad.igv.ucsc.hub.HubParser; import org.broad.igv.ui.IGV; +import org.broad.igv.ui.IGVMenuBar; import org.broad.igv.ui.util.LoadFromURLDialog; import org.broad.igv.ui.util.MessageUtils; import org.broad.igv.util.*; @@ -80,12 +84,11 @@ public void actionPerformed(ActionEvent e) { dlg.setVisible(true); if (!dlg.isCanceled()) { - loadUrls(dlg.getFileURLs(), dlg.getIndexURLs(), isHtsGet); + loadUrls(dlg.getFileURLs(), dlg.getIndexURLs(), isHtsGet); } } else if ((command.equalsIgnoreCase(LOAD_GENOME_FROM_URL))) { - String url = JOptionPane.showInputDialog(IGV.getInstance().getMainFrame(), ta, "Enter URL to .json, hub.txt, or FASTA file", - JOptionPane.QUESTION_MESSAGE); + String url = JOptionPane.showInputDialog(IGV.getInstance().getMainFrame(), ta, "Enter URL to .json, hub.txt, or FASTA file", JOptionPane.QUESTION_MESSAGE); loadGenomeFromUrl(url); @@ -97,9 +100,23 @@ public void actionPerformed(ActionEvent e) { private void loadUrls(List inputs, List indexes, boolean isHtsGet) { if (inputs.size() == 1 && HubGenomeLoader.isHubURL(inputs.getFirst())) { + LongRunningTask.submit(() -> { try { - GenomeManager.getInstance().loadGenome(inputs.getFirst()); + Genome genome = GenomeManager.getInstance().getCurrentGenome(); + String id = genome != null ? genome.getId() : null; + Hub hub = HubParser.loadHub(inputs.getFirst(), id); + if (hub.isAssemblyHub()) { + HubGenomeLoader.loadAssemblyHub(hub); + } else if(genome != null) { + SelectHubTracksAction.selectTracks(hub); + genome.addTrackHub(hub); + IGVMenuBar.getInstance().updateFileMenu(genome); + if(genome.isFromJson()){ + GenomeDownloadUtils.saveLocalGenome(genome.getConfig()); + } + } + } catch (IOException ex) { log.error("Error loading tack hub", ex); MessageUtils.showMessage("Error loading track hub: " + ex.getMessage()); @@ -128,15 +145,14 @@ private void loadUrls(List inputs, List indexes, boolean isHtsGe // Note: this is not currently used private static void loadTrackHub(JPanel ta) { - String urlOrAccension = JOptionPane.showInputDialog(IGV.getInstance().getMainFrame(), ta, "Enter GCA or GCF accession, or URL to hub.txt file", - JOptionPane.QUESTION_MESSAGE); + String urlOrAccension = JOptionPane.showInputDialog(IGV.getInstance().getMainFrame(), ta, "Enter GCA or GCF accession, or URL to hub.txt file", JOptionPane.QUESTION_MESSAGE); - if(urlOrAccension == null) return; + if (urlOrAccension == null) return; urlOrAccension = urlOrAccension.trim(); final String url; - if(urlOrAccension.startsWith("GC")) { + if (urlOrAccension.startsWith("GC")) { url = HubGenomeLoader.convertToHubURL(urlOrAccension); - if(!FileUtils.resourceExists(url)) { + if (!FileUtils.resourceExists(url)) { MessageUtils.showMessage("Unrecognized hub identifier: " + urlOrAccension); } } else { @@ -150,7 +166,7 @@ private static void loadGenomeFromUrl(String url) { if (url != null && !url.isBlank()) { url = url.trim(); try { - if(isHubURL(url)) { + if (isHubURL(url)) { HubGenomeLoader.loadGenome(url); } else { GenomeManager.getInstance().loadGenome(url); @@ -161,7 +177,7 @@ private static void loadGenomeFromUrl(String url) { } } - private static List getResourceLocators(Listinputs, List indexes, boolean isHtsGet) { + private static List getResourceLocators(List inputs, List indexes, boolean isHtsGet) { List locators = new ArrayList<>(); for (int i = 0; i < inputs.size(); i++) { final String url = inputs.get(i); diff --git a/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java b/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java index d9842a3311..828affb58e 100644 --- a/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java +++ b/src/main/java/org/broad/igv/ui/action/SelectHubTracksAction.java @@ -77,45 +77,8 @@ public void actionPerformed(ActionEvent evt) { @Override protected Object doInBackground() throws Exception { try { - Genome genome = GenomeManager.getInstance().getCurrentGenome(); + selectTracks(hub); - final List loadedTracks = IGV.getInstance().getAllTracks().stream().filter(t -> t.getResourceLocator() != null).toList(); - Set loadedTrackPaths = new HashSet<>(loadedTracks.stream().map(t -> t.getResourceLocator().getPath()).toList()); - - TrackHubSelectionDialog dlg = TrackHubSelectionDialog.getTrackHubSelectionDialog(hub, loadedTrackPaths, false); - - SwingUtilities.invokeAndWait(() -> dlg.setVisible(true)); - - if (!dlg.isCanceled()) { - - List groups = hub.getGroupedTrackConfigurations(); - - // The dialog action will modify the visible state for each track config - List tracksToLoad = new ArrayList<>(); - List selected = new ArrayList<>(); - for (TrackConfigContainer g : groups) { - g.map(config -> { - if (config.getVisible()) { - selected.add(config); - if (!loadedTrackPaths.contains(config.getUrl())) { - tracksToLoad.add(config); - } - } - return null; - }); - } - - List locators = tracksToLoad.stream().map(t -> ResourceLocator.fromTrackConfig(t)).toList(); - IGV.getInstance().loadTracks(locators); - - // Update genome -// if (updateGenome) { -// genome.setAnnotationResources(locators); -// // Update preferences -// String key = "hub:" + hub.getUrl(); -// PreferencesManager.getPreferences().put(key, String.join(",", selected.stream().map(c -> c.getName()).toList())); -// } - } } catch (Exception e) { log.error("Error loading track configurations", e); MessageUtils.showMessage(e.getMessage()); @@ -137,4 +100,37 @@ protected void done() { } + public static void selectTracks(Hub hub) { + + final List loadedTracks = IGV.getInstance().getAllTracks().stream().filter(t -> t.getResourceLocator() != null).toList(); + Set loadedTrackPaths = new HashSet<>(loadedTracks.stream().map(t -> t.getResourceLocator().getPath()).toList()); + + TrackHubSelectionDialog dlg = TrackHubSelectionDialog.getTrackHubSelectionDialog(hub, loadedTrackPaths, false); + + dlg.setVisible(true); + + if (!dlg.isCanceled()) { + + List groups = hub.getGroupedTrackConfigurations(); + + // The dialog action will modify the visible state for each track config + List tracksToLoad = new ArrayList<>(); + List selected = new ArrayList<>(); + for (TrackConfigContainer g : groups) { + g.map(config -> { + if (config.getVisible()) { + selected.add(config); + if (!loadedTrackPaths.contains(config.getUrl())) { + tracksToLoad.add(config); + } + } + return null; + }); + } + + List locators = tracksToLoad.stream().map(t -> ResourceLocator.fromTrackConfig(t)).toList(); + IGV.getInstance().loadTracks(locators); + } + } + } diff --git a/src/main/resources/org/broad/igv/prefs/preferences.tab b/src/main/resources/org/broad/igv/prefs/preferences.tab index b4822d5f7a..dddbf01277 100644 --- a/src/main/resources/org/broad/igv/prefs/preferences.tab +++ b/src/main/resources/org/broad/igv/prefs/preferences.tab @@ -272,8 +272,8 @@ PROXY.PW Password string null PORT_ENABLED Enable port boolean TRUE PORT_NUMBER Port number integer 60151 --- -IGV.genome.sequence.dir Genome server URL string https://igv.org/genomes/genomes.tsv -MASTER_RESOURCE_FILE_KEY Data registry URL string https://igv.org/genomes/registry/$$_dataServerRegistry.txt +IGV.genome.sequence.dir Genome server URL string https://raw.githubusercontent.com/igvteam/igv-genomes/refs/heads/main/genomes.tsv +MASTER_RESOURCE_FILE_KEY Data registry URL string null --- PROVISIONING.URL OAuth provisioning URL string null --- From 9996f4ca8907d13a1981b927acd76220a1aed353 Mon Sep 17 00:00:00 2001 From: Jim Robinson <933148+jrobinso@users.noreply.github.com> Date: Tue, 11 Mar 2025 09:10:22 -0700 Subject: [PATCH 130/130] Track panel refactor (#1673) * Track panel simplification. * Autoselect gene tracks only for track assembly hubs --- .../feature/genome/GenomeDownloadUtils.java | 20 ++- .../igv/feature/genome/GenomeManager.java | 8 +- .../igv/feature/genome/load/GenomeConfig.java | 3 +- .../feature/genome/load/HubGenomeLoader.java | 1 - .../igv/session/IndexAwareSessionReader.java | 9 +- .../broad/igv/session/UCSCSessionReader.java | 6 +- .../tools/motiffinder/MotifFinderPlugin.java | 2 +- .../igv/track/CombinedDataSourceDialog.java | 3 +- .../broad/igv/ucsc/hub/CollapsiblePanel.java | 24 +-- .../java/org/broad/igv/ucsc/hub/Stanza.java | 2 +- .../org/broad/igv/ucsc/hub/TrackDbHub.java | 21 +-- src/main/java/org/broad/igv/ui/IGV.java | 158 +++++------------- src/main/java/org/broad/igv/ui/PanelName.java | 2 +- .../igv/ui/action/LoadFromURLMenuAction.java | 4 +- .../broad/igv/batch/CommandExecutorTest.java | 2 +- 15 files changed, 98 insertions(+), 167 deletions(-) diff --git a/src/main/java/org/broad/igv/feature/genome/GenomeDownloadUtils.java b/src/main/java/org/broad/igv/feature/genome/GenomeDownloadUtils.java index 681e271ae8..85bc0206fc 100644 --- a/src/main/java/org/broad/igv/feature/genome/GenomeDownloadUtils.java +++ b/src/main/java/org/broad/igv/feature/genome/GenomeDownloadUtils.java @@ -58,14 +58,18 @@ public static File downloadGenome(GenomeConfig c, boolean downloadSequence, bool // Create a directory for the data files (sequence and annotations) final File genomeDirectory = DirectoryManager.getGenomeCacheDirectory(); - File dataDirectory = new File(genomeDirectory, config.getId()); - if (dataDirectory.exists()) { - if (!dataDirectory.isDirectory()) { - throw new RuntimeException("Error downloading genome. " + dataDirectory.getAbsolutePath() + " exists and is not a directory."); - } - } else { - if (!dataDirectory.mkdir()) { - throw new RuntimeException("Error downloading genome. Could not create directory: " + dataDirectory.getAbsolutePath()); + File dataDirectory = null; + + if(downloadSequence || downloadAnnotations) { + dataDirectory = new File(genomeDirectory, config.getId()); + if (dataDirectory.exists()) { + if (!dataDirectory.isDirectory()) { + throw new RuntimeException("Error downloading genome. " + dataDirectory.getAbsolutePath() + " exists and is not a directory."); + } + } else { + if (!dataDirectory.mkdir()) { + throw new RuntimeException("Error downloading genome. Could not create directory: " + dataDirectory.getAbsolutePath()); + } } } diff --git a/src/main/java/org/broad/igv/feature/genome/GenomeManager.java b/src/main/java/org/broad/igv/feature/genome/GenomeManager.java index 420cabc9ea..62892ebfa0 100644 --- a/src/main/java/org/broad/igv/feature/genome/GenomeManager.java +++ b/src/main/java/org/broad/igv/feature/genome/GenomeManager.java @@ -64,8 +64,6 @@ import java.util.List; import java.util.stream.Stream; -import static org.broad.igv.prefs.Constants.SHOW_SINGLE_TRACK_PANE_KEY; - /** * @author jrobinso */ @@ -241,12 +239,11 @@ public void restoreGenomeTracks(Genome genome) { // Fetch the gene track, defined by .genome files. In this format the genome data is encoded in the .genome file FeatureTrack geneFeatureTrack = genome.getGeneTrack(); // Can be null if (geneFeatureTrack != null) { - PanelName panelName = PreferencesManager.getPreferences().getAsBoolean(SHOW_SINGLE_TRACK_PANE_KEY) ? - PanelName.DATA_PANEL : PanelName.FEATURE_PANEL; geneFeatureTrack.setAttributeValue(Globals.TRACK_NAME_ATTRIBUTE, geneFeatureTrack.getName()); geneFeatureTrack.setAttributeValue(Globals.TRACK_DATA_FILE_ATTRIBUTE, ""); geneFeatureTrack.setAttributeValue(Globals.TRACK_DATA_TYPE_ATTRIBUTE, geneFeatureTrack.getTrackType().toString()); - IGV.getInstance().addTracks(Arrays.asList(geneFeatureTrack), panelName); + geneFeatureTrack.getResourceLocator().setPanelName(PanelName.ANNOTATION_PANEL.getName()); + IGV.getInstance().addTracks(Arrays.asList(geneFeatureTrack)); } List resources = genome.getAnnotationResources(); @@ -254,6 +251,7 @@ public void restoreGenomeTracks(Genome genome) { if (resources != null) { for (ResourceLocator locator : resources) { try { + locator.setPanelName(PanelName.ANNOTATION_PANEL.getName()); List tracks = IGV.getInstance().load(locator); annotationTracks.addAll(tracks); } catch (DataLoadException e) { diff --git a/src/main/java/org/broad/igv/feature/genome/load/GenomeConfig.java b/src/main/java/org/broad/igv/feature/genome/load/GenomeConfig.java index 74273760f6..6046b5a621 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/GenomeConfig.java +++ b/src/main/java/org/broad/igv/feature/genome/load/GenomeConfig.java @@ -239,9 +239,8 @@ public void setBlatDB(String blatDB) { } public String getUcsdID() { - return ucsdID; + return ucsdID == null ? id : ucsdID; } - public void setUcsdID(String ucsdID) { this.ucsdID = ucsdID; } diff --git a/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java b/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java index 56180288c3..be1a18ed52 100644 --- a/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java +++ b/src/main/java/org/broad/igv/feature/genome/load/HubGenomeLoader.java @@ -19,7 +19,6 @@ import javax.swing.*; import java.io.File; import java.io.IOException; -import java.lang.reflect.InvocationTargetException; import java.util.*; import java.util.stream.Collectors; diff --git a/src/main/java/org/broad/igv/session/IndexAwareSessionReader.java b/src/main/java/org/broad/igv/session/IndexAwareSessionReader.java index 93990e1b79..0ff941bcd7 100644 --- a/src/main/java/org/broad/igv/session/IndexAwareSessionReader.java +++ b/src/main/java/org/broad/igv/session/IndexAwareSessionReader.java @@ -122,11 +122,7 @@ public void loadSession(InputStream inputStream, Session session, String session locator.setTrackLine(trackLine); // Alignment tracks must be loaded synchronously if (isAlignmentFile(locator.getPath())) { - TrackPanel panel = igv.getPanelFor(locator); - if (panel == null) { - panel = igv.getTrackPanel(DATA_PANEL_NAME); - } - panel.addTracks(igv.load(locator)); + igv.addTracks(igv.load(locator)); } else { aSync.add(locator); } @@ -233,8 +229,7 @@ private ResourceLocator parseResourceLine(String line) { String[] tokens = line.split("\\s+"); if (tokens.length == 1) { locator = new ResourceLocator(tokens[0]); - } - else if (tokens.length >= 2) { + } else if (tokens.length >= 2) { // Might want to do some error checking here on // making sure file prefixes match and file extensions // indicate the index comes second. diff --git a/src/main/java/org/broad/igv/session/UCSCSessionReader.java b/src/main/java/org/broad/igv/session/UCSCSessionReader.java index 648bc2bcab..1604886489 100644 --- a/src/main/java/org/broad/igv/session/UCSCSessionReader.java +++ b/src/main/java/org/broad/igv/session/UCSCSessionReader.java @@ -112,11 +112,7 @@ public void loadSession(InputStream inputStream, Session session, String session locator.setTrackLine(trackLine); // Alignment tracks must be loaded synchronously if (isAlignmentFile(locator.getPath())) { - TrackPanel panel = igv.getPanelFor(locator); - if (panel == null) { - panel = igv.getTrackPanel(DATA_PANEL_NAME); - } - panel.addTracks(igv.load(locator)); + igv.addTracks(igv.load(locator)); } else { aSync.add(locator); } diff --git a/src/main/java/org/broad/igv/tools/motiffinder/MotifFinderPlugin.java b/src/main/java/org/broad/igv/tools/motiffinder/MotifFinderPlugin.java index ee5ddc0bb5..b040393b99 100644 --- a/src/main/java/org/broad/igv/tools/motiffinder/MotifFinderPlugin.java +++ b/src/main/java/org/broad/igv/tools/motiffinder/MotifFinderPlugin.java @@ -80,7 +80,7 @@ static void handleDialogResult(MotifFinderDialog dialog) { */ static List addTracksForPatterns(String[] pattern, String[] posTrackNames, String[] negTrackNames) { List trackList = generateTracksForPatterns(pattern, posTrackNames, negTrackNames); - IGV.getInstance().addTracks(trackList, PanelName.FEATURE_PANEL); + IGV.getInstance().addTracks(trackList); return trackList; } diff --git a/src/main/java/org/broad/igv/track/CombinedDataSourceDialog.java b/src/main/java/org/broad/igv/track/CombinedDataSourceDialog.java index 1f0697b04a..710c73b738 100644 --- a/src/main/java/org/broad/igv/track/CombinedDataSourceDialog.java +++ b/src/main/java/org/broad/igv/track/CombinedDataSourceDialog.java @@ -103,7 +103,8 @@ private void okButtonActionPerformed(ActionEvent e) { TrackMenuUtils.changeRendererClass(Arrays.asList(newTrack), track0.getRenderer().getClass()); newTrack.setDataRange(track0.getDataRange()); newTrack.setColorScale(track0.getColorScale()); - IGV.getInstance().addTracks(Arrays.asList(newTrack), PanelName.DATA_PANEL); + + IGV.getInstance().addTracks(Arrays.asList(newTrack)); this.setVisible(false); } diff --git a/src/main/java/org/broad/igv/ucsc/hub/CollapsiblePanel.java b/src/main/java/org/broad/igv/ucsc/hub/CollapsiblePanel.java index 5d2a014a9d..92f5babbed 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/CollapsiblePanel.java +++ b/src/main/java/org/broad/igv/ucsc/hub/CollapsiblePanel.java @@ -27,18 +27,18 @@ public class CollapsiblePanel extends JPanel { private final JLabel jlabel; private final List selectionBoxes; private final boolean autoselectDefaults; - private final TrackConfigContainer configGroup; + private final TrackConfigContainer configContainer; private JButton collapseButton; private JComponent content; private JPanel header; private ImageIcon openIcon; private ImageIcon closeIcon; - public CollapsiblePanel(TrackConfigContainer configGroup, boolean autoselectDefaults) { + public CollapsiblePanel(TrackConfigContainer configContainer, boolean autoselectDefaults) { Color backgroundColor = HEADER_BG; - this.configGroup = configGroup; + this.configContainer = configContainer; this.autoselectDefaults = autoselectDefaults; setLayout(new BorderLayout()); @@ -47,10 +47,10 @@ public CollapsiblePanel(TrackConfigContainer configGroup, boolean autoselectDefa trackPanel.setLayout(new BoxLayout(trackPanel, BoxLayout.Y_AXIS)); // There is a (so far) intractable bug if a large # of JCheckboxes are created for this widget. - int totalTrackCount = configGroup.countTracks(); + int totalTrackCount = configContainer.countTracks(); SelectionBox.CheckboxType checkboxType = totalTrackCount < 1000 ? SelectionBox.CheckboxType.SWING : SelectionBox.CheckboxType.CUSTOM; - selectionBoxes = addSelectionBoxes(null, configGroup, trackPanel, checkboxType); + selectionBoxes = addSelectionBoxes(null, configContainer, trackPanel, checkboxType); boolean isSelected = false; int maxWidth = 0; @@ -68,9 +68,9 @@ public CollapsiblePanel(TrackConfigContainer configGroup, boolean autoselectDefa selectionBox.setPreferredWidth(maxWidth); } - String label = configGroup.label + " (" + selectionBoxes.size() + " tracks, " + selectionCount + " selected)"; + String label = configContainer.label + " (" + selectionBoxes.size() + " tracks, " + selectionCount + " selected)"; - boolean isOpen = isSelected || configGroup.defaultOpen; + boolean isOpen = isSelected || configContainer.defaultOpen; this.content = trackPanel; this.openIcon = IconFactory.getInstance().getIcon(IconFactory.IconID.MINUS); @@ -102,7 +102,7 @@ public CollapsiblePanel(TrackConfigContainer configGroup, boolean autoselectDefa this.add(header, BorderLayout.NORTH); - final JButton searchButton = createSearchButton("Search " + configGroup.label, selectionBoxes, + final JButton searchButton = createSearchButton("Search " + configContainer.label, selectionBoxes, (selectedCount) -> { this.updateLabel(); return null; @@ -121,7 +121,9 @@ public CollapsiblePanel(TrackConfigContainer configGroup, boolean autoselectDefa public void resetSelectionBoxes(Set loadedTrackPaths) { for (CollapsiblePanel.SelectionBox box : selectionBoxes) { final boolean isLoaded = loadedTrackPaths != null && loadedTrackPaths.contains(box.trackConfig.getUrl()); - box.setSelected(isLoaded || (autoselectDefaults && box.trackConfig.getVisible() == true)); + box.setSelected( + isLoaded || + (autoselectDefaults && box.trackConfig.getVisible() == true) && configContainer.name.toLowerCase().equals("genes")); box.setEnabled(!isLoaded); updateLabel(); } @@ -134,7 +136,7 @@ public void updateLabel() { count++; } } - String label = configGroup.label + " (" + selectionBoxes.size() + " tracks, " + count + " selected)"; + String label = configContainer.label + " (" + selectionBoxes.size() + " tracks, " + count + " selected)"; jlabel.setText(label); } @@ -144,7 +146,7 @@ public void clearSelections() { box.setSelected(false); } } - String label = configGroup.label + " (" + selectionBoxes.size() + " tracks, 0 selected)"; + String label = configContainer.label + " (" + selectionBoxes.size() + " tracks, 0 selected)"; jlabel.setText(label); } diff --git a/src/main/java/org/broad/igv/ucsc/hub/Stanza.java b/src/main/java/org/broad/igv/ucsc/hub/Stanza.java index d1023475ec..ff644eb857 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/Stanza.java +++ b/src/main/java/org/broad/igv/ucsc/hub/Stanza.java @@ -64,7 +64,7 @@ boolean hasProperty(String key) { return getProperty(key) != null; } - String format() { + String type() { String type = this.getOwnProperty("type"); if (type != null) { // Trim extra bed qualifiers (e.g. bigBed + 4) diff --git a/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java b/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java index a7db40fa11..62c50b79e5 100644 --- a/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java +++ b/src/main/java/org/broad/igv/ucsc/hub/TrackDbHub.java @@ -13,7 +13,7 @@ public class TrackDbHub { static Set supportedTypes = new HashSet(Arrays.asList("bigbed", "bigwig", "biggenepred", "vcftabix", "refgene", - "bam", "sampleinfo", "vcf.list", "ucscsnp", "bed", "tdf", "gff", "gff3", "gtf")); + "bam", "sampleinfo", "vcf.list", "ucscsnp", "bed", "tdf", "gff", "gff3", "gtf", "vcf")); static Set filterTracks = new HashSet(Arrays.asList("cytoBandIdeo", "assembly", "gap", "gapOverlap", "allGaps", "cpgIslandExtUnmasked", "windowMasker")); @@ -24,11 +24,15 @@ public class TrackDbHub { "squish", "SQUISHED", "dense", "COLLAPSED"); + static Map typeFormatMap = Map.of( + "vcftabix", "vcf" + ); + + List trackStanzas; List groupStanzas; List groupTrackConfigs; - public TrackDbHub(List trackStanzas, List groupStanzas) { this.groupStanzas = groupStanzas; this.trackStanzas = trackStanzas; @@ -100,7 +104,7 @@ public List getGroupedTrackConfigurations(String hubName) } else if (!filterTracks.contains(s.name) && s.hasProperty("bigDataUrl") && - supportedTypes.contains(s.format().toLowerCase())) { + supportedTypes.contains(s.type().toLowerCase())) { final TrackConfig trackConfig = getTrackConfig(s); if (parent != null) { @@ -123,19 +127,17 @@ public List getGroupedTrackConfigurations(String hubName) return groupTrackConfigs; } - private TrackConfig getTrackConfig(Stanza t) { String url = t.getProperty("bigDataUrl"); TrackConfig config = new TrackConfig(url); - String format = t.format(); - if (format != null) { + String type = t.type(); + if (type != null) { + String format = typeFormatMap.containsKey(type) ? typeFormatMap.get(type) : type; config.setFormat(format.toLowerCase()); } - config.setPanelName(IGV.DATA_PANEL_NAME); - config.setId(t.getProperty("track")); config.setName(t.getProperty("shortLabel")); @@ -154,7 +156,7 @@ private TrackConfig getTrackConfig(Stanza t) { if (t.hasProperty("bigDataIndex")) { config.setIndexURL(t.getProperty("bigDataIndex")); - } else if (t.format().equals("vcfTabix")) { + } else if (t.type().equals("vcfTabix")) { config.setIndexURL(t.getProperty("bigDataUrl") + ".tbi"); } @@ -181,7 +183,6 @@ private TrackConfig getTrackConfig(Stanza t) { config.setVisibilityWindow(vizWindow); } - if (t.hasProperty("autoScale")) { config.setAutoscale(t.getProperty("autoScale").toLowerCase().equals("on")); } diff --git a/src/main/java/org/broad/igv/ui/IGV.java b/src/main/java/org/broad/igv/ui/IGV.java index fe25f126c3..d30e747e83 100644 --- a/src/main/java/org/broad/igv/ui/IGV.java +++ b/src/main/java/org/broad/igv/ui/IGV.java @@ -378,10 +378,40 @@ public Future loadTracks(final Collection locators) { contentPane.getStatusBar().setMessage("Loading ..."); NamedRunnable runnable = new NamedRunnable() { + public void run() { //Collect size statistics before loading List> trackPanelAttrs = getTrackPanelAttrs(); - loadResources(locators); + + final MessageCollection messages = new MessageCollection(); + for (final ResourceLocator locator : locators) { + + // If it's 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 tracks = load(locator); + addTracks(tracks); + + } 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); + } + } + + resetPanelHeights(trackPanelAttrs.get(0), trackPanelAttrs.get(1)); showLoadedTrackCount(); revalidateTrackPanels(); @@ -1121,7 +1151,7 @@ public MainPanel getMainPanel() { } public RecentFileSet getRecentSessionList() { - if(recentSessionList == null){ + if (recentSessionList == null) { recentSessionList = PreferencesManager.getPreferences().getRecentSessions(); //remove sessions that no longer exist recentSessionList.removeIf(file -> !(new File(file)).exists()); @@ -1130,7 +1160,7 @@ public RecentFileSet getRecentSessionList() { } public RecentUrlsSet getRecentUrls() { - if(recentUrlsList == null){ + if (recentUrlsList == null) { recentUrlsList = PreferencesManager.getPreferences().getRecentUrls(); } return recentUrlsList; @@ -1141,10 +1171,10 @@ public RecentUrlsSet getRecentUrls() { * allows showing the menu when the first URL is added to the collection. * @param toAdd */ - public void addToRecentUrls(Collection toAdd){ + public void addToRecentUrls(Collection toAdd) { RecentUrlsSet recentFiles = getRecentUrls(); recentFiles.addAll(toAdd); - if(!recentFiles.isEmpty()){ + if (!recentFiles.isEmpty()) { menuBar.showRecentFilesMenu(); } } @@ -1175,85 +1205,18 @@ public void setStatusWindowText(String text) { } } - /** - * Load resources into IGV. Tracks are added to the appropriate panel - *

- * NOTE: this must be run on the event tread as UI components are added here. - * - * @param locators - */ - public void loadResources(Collection locators) { - - final MessageCollection messages = new MessageCollection(); - - for (final ResourceLocator locator : locators) { - - // If it's 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 tracks = load(locator); - addTracks(tracks); - - } 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 tracks, PanelName panelName) { - TrackPanel panel = getTrackPanel(panelName.getName()); - panel.addTracks(tracks); - repaint(); - } - - /** - * Add the specified tracks to the appropriate panel. Panel + * Add the specified tracks to the appropriate panel. The list of tracks share a common file (locator). Panel * is chosen based on characteristics of the {@code locator}. - * - * @param tracks */ - public void addTracks(List tracks) { + public void addTracks(List trackList) { - if (tracks.size() > 0) { - String path = tracks.get(0).getResourceLocator().getPath();// locator.getPath(); - Track representativeTrack = tracks.get(0); + // Group tracks by locator. + Map> map = trackList.stream().collect(Collectors.groupingBy(t -> t.getResourceLocator().getPath())); - // Get an appropriate panel. If its a VCF file create a new panel if the number of genotypes - // is greater than 10 + for(List tracks : map.values()) { + Track representativeTrack = tracks.get(0); TrackPanel panel = getPanelFor(representativeTrack); - 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); } } @@ -1338,29 +1301,6 @@ public TrackPanel getPanelFor(Track track) { } ResourceLocator locator = track.getResourceLocator(); - if (locator != null) { - TrackPanel panel = getPanelFor(locator); - if (panel != null) { - return panel; - } - } - - if (track.getClass() == FeatureTrack.class && !PreferencesManager.getPreferences().getAsBoolean(SHOW_SINGLE_TRACK_PANE_KEY)) - return getTrackPanel(FEATURE_PANEL_NAME); - else { - return getTrackPanel(DATA_PANEL_NAME); - } - } - - /** - * Return a DataPanel appropriate for the resource type. This method should be considered deprecated in - * favor of getPanelFor(Track), however the UCSCSessionReader still uses this form. - * - * @param locator - * @return - */ - public TrackPanel getPanelFor(ResourceLocator locator) { - final String format = locator.getFormat(); if (locator.getPanelName() != null) { return getTrackPanel(locator.getPanelName()); @@ -1371,15 +1311,12 @@ public TrackPanel getPanelFor(ResourceLocator locator) { } else if (TrackLoader.isAlignmentTrack(format)) { String newPanelName = "Panel" + System.currentTimeMillis(); return addDataPanel(newPanelName).getTrackPanel(); + } else if (track instanceof VariantTrack && ((VariantTrack) track).getAllSamples().size() > 10) { + String newPanelName = "Panel" + System.currentTimeMillis(); + return addDataPanel(newPanelName).getTrackPanel(); } else { - if (format != null && format.equalsIgnoreCase("das")) { - return getTrackPanel(FEATURE_PANEL_NAME); - } - if (isAnnotationFile(format)) { - return getTrackPanel(FEATURE_PANEL_NAME); - } else { - return null; // Can't determine from locator - } + // Default + return getTrackPanel(DATA_PANEL_NAME); } } @@ -1853,7 +1790,6 @@ static void sortByRegionScore(List tracks, //////////////////////////////////////////////////////////////////////////////////////// // Groups - public String getGroupByAttribute() { return session.getGroupByAttribute(); } @@ -1877,8 +1813,6 @@ private void resetGroups() { ////////////////////////////////////////////////////////////////////////////////////////// // Startup - - public Future startUp(Main.IGVArgs igvArgs) { if (log.isDebugEnabled()) { diff --git a/src/main/java/org/broad/igv/ui/PanelName.java b/src/main/java/org/broad/igv/ui/PanelName.java index 90bdbbe3b3..b07680509f 100644 --- a/src/main/java/org/broad/igv/ui/PanelName.java +++ b/src/main/java/org/broad/igv/ui/PanelName.java @@ -33,7 +33,7 @@ * @api */ public enum PanelName { - FEATURE_PANEL(IGV.FEATURE_PANEL_NAME), + ANNOTATION_PANEL(IGV.FEATURE_PANEL_NAME), DATA_PANEL(IGV.DATA_PANEL_NAME); private final String panelName; diff --git a/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java b/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java index ff43fce459..fa31d133d9 100644 --- a/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java +++ b/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java @@ -103,10 +103,12 @@ private void loadUrls(List inputs, List indexes, boolean isHtsGe LongRunningTask.submit(() -> { try { + + Genome genome = GenomeManager.getInstance().getCurrentGenome(); String id = genome != null ? genome.getId() : null; Hub hub = HubParser.loadHub(inputs.getFirst(), id); - if (hub.isAssemblyHub()) { + if (hub.isAssemblyHub() && (genome == null || !hub.getGenomeConfig().getUcsdID().equals(genome.getUCSCId()))) { HubGenomeLoader.loadAssemblyHub(hub); } else if(genome != null) { SelectHubTracksAction.selectTracks(hub); diff --git a/src/test/java/org/broad/igv/batch/CommandExecutorTest.java b/src/test/java/org/broad/igv/batch/CommandExecutorTest.java index d2bc983405..bac517d2d5 100755 --- a/src/test/java/org/broad/igv/batch/CommandExecutorTest.java +++ b/src/test/java/org/broad/igv/batch/CommandExecutorTest.java @@ -552,7 +552,7 @@ public void testSnapshotsize() throws Exception { int numLoads = 1; for (int ii = 0; ii < numLoads; ii++) { - IGV.getInstance().loadResources(Arrays.asList(new ResourceLocator(filePath))); + IGV.getInstance().loadTracks(Arrays.asList(new ResourceLocator(filePath))); } exec.execute("goto chr1:9,713,386-9,733,865");