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 b38554c66e..1ac4e85654 100644 --- a/src/main/java/org/broad/igv/feature/genome/Genome.java +++ b/src/main/java/org/broad/igv/feature/genome/Genome.java @@ -257,7 +257,7 @@ private void addTracks(GenomeConfig config) { res.setVisibilityWindow(vw); } - if(trackConfig.searchTrix != null) { + if (trackConfig.searchTrix != null) { res.setTrixURL(trackConfig.searchTrix); } @@ -355,7 +355,7 @@ public Genome(String id, List chromosomes) { * Return the canonical chromosome name for the (possibly) alias * * @param str chromosome or alias name - * @return the canonical chromsoome name -- i.e. chromosome name as defined by the reference sequence + * @return the canonical chromsoome name -if the chromosome exists. */ public String getCanonicalChrName(String str) { if (str == null) { @@ -365,9 +365,7 @@ public String getCanonicalChrName(String str) { } else if (chromAliasSource != null) { try { ChromAlias aliasRecord = chromAliasSource.search(str); - if (aliasRecord == null) { - return str; - } else { + if (aliasRecord != null) { String chr = aliasRecord.getChr(); chrAliasCache.put(str, chr); return chr; @@ -491,9 +489,9 @@ public Chromosome getChromosome(String name) { if (chromosomeMap.containsKey(chrName)) { return chromosomeMap.get(chrName); } else { - int idx = this.chromosomeMap.size(); 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; diff --git a/src/main/java/org/broad/igv/feature/genome/Sequence.java b/src/main/java/org/broad/igv/feature/genome/Sequence.java index 0d8c31641b..891a89b027 100644 --- a/src/main/java/org/broad/igv/feature/genome/Sequence.java +++ b/src/main/java/org/broad/igv/feature/genome/Sequence.java @@ -38,13 +38,26 @@ */ public interface Sequence { - byte[] getSequence(String chr, int start, int end); + /** + * Return the sequence for the given range. If sequence named "seq" does not exist returns null. + * @param chr + * @param start + * @param end + * @return The sequence in bytes, or null if no sequence exists + */ + byte[] getSequence(String seq, int start, int end) ; - byte getBase(String chr, int position); + byte getBase(String seq, int position); List getChromosomeNames(); - int getChromosomeLength(String chrname); + /** + * Return the given sequence length. If no sequence exists with name "seq" return -1. + * + * @param seq + * @return + */ + int getChromosomeLength(String seq); List getChromosomes(); diff --git a/src/main/java/org/broad/igv/feature/genome/SequenceNotFoundException.java b/src/main/java/org/broad/igv/feature/genome/SequenceNotFoundException.java new file mode 100644 index 0000000000..7c6910d0c5 --- /dev/null +++ b/src/main/java/org/broad/igv/feature/genome/SequenceNotFoundException.java @@ -0,0 +1,7 @@ +package org.broad.igv.feature.genome; + +public class SequenceNotFoundException extends RuntimeException { + public SequenceNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/org/broad/igv/feature/genome/fasta/FastaIndexedSequence.java b/src/main/java/org/broad/igv/feature/genome/fasta/FastaIndexedSequence.java index 1944a5e0af..ffa9f25c2d 100644 --- a/src/main/java/org/broad/igv/feature/genome/fasta/FastaIndexedSequence.java +++ b/src/main/java/org/broad/igv/feature/genome/fasta/FastaIndexedSequence.java @@ -191,7 +191,7 @@ public List getChromosomeNames() { @Override public int getChromosomeLength(String chrname) { - return index.getSequenceSize(chrname); + return index.getSequenceSize(chrname); } @Override diff --git a/src/main/java/org/broad/igv/session/History.java b/src/main/java/org/broad/igv/session/History.java index 06ff6dad50..8f4719f787 100644 --- a/src/main/java/org/broad/igv/session/History.java +++ b/src/main/java/org/broad/igv/session/History.java @@ -31,6 +31,7 @@ import org.broad.igv.ui.IGV; import org.broad.igv.ui.action.SearchCommand; import org.broad.igv.ui.panel.FrameManager; +import org.broad.igv.util.LongRunningTask; import java.util.ArrayList; import java.util.LinkedList; @@ -121,7 +122,7 @@ public void processItem(Entry entry) { if (FrameManager.isGeneListMode()) { IGV.getInstance().setGeneList(null, false); } - (new SearchCommand(FrameManager.getDefaultFrame(), locus, false)).execute(); + LongRunningTask.submit(new SearchCommand(FrameManager.getDefaultFrame(), locus, false)); //Zoom should be implicit in the locus //FrameManager.getDefaultFrame().setZoom(entry.getZoom()); } diff --git a/src/main/java/org/broad/igv/track/FeatureSource.java b/src/main/java/org/broad/igv/track/FeatureSource.java index b001557f65..0e0f9be3c0 100644 --- a/src/main/java/org/broad/igv/track/FeatureSource.java +++ b/src/main/java/org/broad/igv/track/FeatureSource.java @@ -26,6 +26,7 @@ package org.broad.igv.track; import htsjdk.tribble.Feature; +import htsjdk.tribble.NamedFeature; import org.broad.igv.feature.LocusScore; import org.broad.igv.ui.panel.ReferenceFrame; @@ -92,4 +93,17 @@ default void close() { default Object getHeader() { return null; } + + /** + * Return true if the source can be searched for a feature by name + * + * @return + */ + default boolean isSearchable() { + return false; + } + + default NamedFeature search(String name) { + 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 3af5ea1414..b78480e832 100644 --- a/src/main/java/org/broad/igv/track/FeatureTrack.java +++ b/src/main/java/org/broad/igv/track/FeatureTrack.java @@ -26,6 +26,7 @@ package org.broad.igv.track; import htsjdk.tribble.Feature; +import htsjdk.tribble.NamedFeature; import htsjdk.tribble.TribbleException; import org.broad.igv.event.IGVEvent; import org.broad.igv.logging.*; @@ -1056,6 +1057,16 @@ public void marshalXML(Document document, Element element) { } + @Override + public boolean isSearchable() { + return source.isSearchable(); + } + + @Override + public NamedFeature search(String token) { + return source.search(token); + } + @Override public void unmarshalXML(Element element, Integer version) { diff --git a/src/main/java/org/broad/igv/track/Track.java b/src/main/java/org/broad/igv/track/Track.java index 2732bd2e79..c127af7607 100644 --- a/src/main/java/org/broad/igv/track/Track.java +++ b/src/main/java/org/broad/igv/track/Track.java @@ -31,6 +31,7 @@ import htsjdk.tribble.Feature; +import htsjdk.tribble.NamedFeature; import org.broad.igv.renderer.ContinuousColorScale; import org.broad.igv.renderer.DataRange; import org.broad.igv.renderer.Renderer; @@ -272,9 +273,12 @@ default Color getExplicitAltColor() { void setAutoScale(boolean autoScale); - default boolean isShowFeatureNames() {return true;} + default boolean isShowFeatureNames() { + return true; + } - default void setShowFeatureNames(boolean b) {} + default void setShowFeatureNames(boolean b) { + } /** * Return the java property or attribute for the feature display name. Default is "null", in which case the @@ -286,6 +290,19 @@ default String getLabelField() { return null; } + /** + * Return true if the track can be searched for a feature by name. + * + * @return + */ + default boolean isSearchable() { + return false; + } + + default NamedFeature search(String token) { + return null; + } + default void repaint() { IGV.getInstance().repaint(this); } 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 0587990963..bef7a13667 100644 --- a/src/main/java/org/broad/igv/ucsc/bb/BBFeatureSource.java +++ b/src/main/java/org/broad/igv/ucsc/bb/BBFeatureSource.java @@ -25,6 +25,7 @@ package org.broad.igv.ucsc.bb; +import htsjdk.tribble.NamedFeature; import org.broad.igv.feature.BasicFeature; import org.broad.igv.feature.LocusScore; import org.broad.igv.feature.genome.Genome; @@ -32,7 +33,6 @@ import org.broad.igv.logging.Logger; import org.broad.igv.track.FeatureSource; import org.broad.igv.track.WindowFunction; -import org.broad.igv.ucsc.Trix; import java.io.IOException; import java.util.*; @@ -111,7 +111,8 @@ public boolean isSearchable() { return reader.isSearchable(); } - BasicFeature search(String term) { + @Override + public NamedFeature search(String term) { try { return reader.search(term); } catch (IOException e) { 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 b27ff7b187..fa032a9e06 100644 --- a/src/main/java/org/broad/igv/ucsc/bb/BBFile.java +++ b/src/main/java/org/broad/igv/ucsc/bb/BBFile.java @@ -1,6 +1,7 @@ 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; @@ -400,6 +401,7 @@ public boolean isSearchable() { * @param term * @returns {Promise} */ + public BasicFeature search(String term) throws IOException { if (this.header == null) { 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 c396fff00e..8c76aa36d0 100644 --- a/src/main/java/org/broad/igv/ucsc/twobit/TwoBitSequence.java +++ b/src/main/java/org/broad/igv/ucsc/twobit/TwoBitSequence.java @@ -2,6 +2,9 @@ import org.broad.igv.feature.Chromosome; import org.broad.igv.feature.genome.Sequence; +import org.broad.igv.feature.genome.SequenceNotFoundException; +import org.broad.igv.logging.LogManager; +import org.broad.igv.logging.Logger; import org.broad.igv.ucsc.BPIndex; import org.broad.igv.ucsc.BPTree; @@ -22,6 +25,8 @@ public class TwoBitSequence implements Sequence { + private static Logger log = LogManager.getLogger(TwoBitSequence.class); + // the number 0x1A412743 in the architecture of the machine that created the file static int SIGNATURE = 0x1a412743; String path; @@ -85,12 +90,15 @@ public List getChromosomeNames() { } @Override - public int getChromosomeLength(String chrname) { + public int getChromosomeLength(String seq) { try { - SequenceRecord sequenceRecord = getSequenceRecord(chrname); + SequenceRecord sequenceRecord = getSequenceRecord(seq); return sequenceRecord.getDnaSize(); - } catch (Exception e) { - throw new RuntimeException(e); + } catch (SequenceNotFoundException e) { + return -1; + } catch (IOException e) { + log.error("Error reading sequence " + seq, e); + return -1; } } @@ -183,7 +191,7 @@ public SequenceRecord getSequenceRecord(String seqName) throws IOException { if (record == null) { long[] offset_length = this.index.search(seqName); if (offset_length == null) { - throw new RuntimeException("Unknown sequence: " + seqName); + throw new SequenceNotFoundException("Unknown sequence: " + seqName); } long offset = offset_length[0]; diff --git a/src/main/java/org/broad/igv/ui/action/SearchCommand.java b/src/main/java/org/broad/igv/ui/action/SearchCommand.java index 6e89e48442..b4675beea6 100644 --- a/src/main/java/org/broad/igv/ui/action/SearchCommand.java +++ b/src/main/java/org/broad/igv/ui/action/SearchCommand.java @@ -39,21 +39,18 @@ import org.broad.igv.lists.GeneList; import org.broad.igv.prefs.Constants; import org.broad.igv.prefs.PreferencesManager; +import org.broad.igv.track.Track; import org.broad.igv.ui.IGV; import org.broad.igv.ui.panel.FrameManager; import org.broad.igv.ui.panel.ReferenceFrame; -import org.broad.igv.ui.util.IGVMouseInputAdapter; import org.broad.igv.ui.util.MessageUtils; import org.broad.igv.util.HttpUtils; -import org.broad.igv.util.liftover.Liftover; -import javax.swing.*; -import java.awt.*; -import java.awt.event.MouseEvent; import java.net.URL; import java.util.List; import java.util.*; + /** * A class for performing search actions. The class takes a view context and * search string as parameters. The search string can be either @@ -62,7 +59,7 @@ * * @author jrobinso */ -public class SearchCommand { +public class SearchCommand implements Runnable { private static Logger log = LogManager.getLogger(SearchCommand.class); public static int SEARCH_LIMIT = 10000; @@ -121,7 +118,7 @@ public SearchCommand(ReferenceFrame referenceFrame, String searchString, boolean } - public void execute() { + public void run() { List results = runSearch(searchString); showSearchResult(results); } @@ -146,60 +143,64 @@ public List runSearch(String searchString) { // Check for special "liftover" syntax. This allows searching based on coordinates from another genome // (the "target" genome) if an associated liftover map is defined for the target genome. - Liftover liftover = null; - if (searchString.startsWith("!") && genome.getLiftoverMap() != null) { - int idx = searchString.indexOf(' '); - String genomeKey = searchString.substring(1, idx); - liftover = genome.getLiftoverMap().get(genomeKey); - if (liftover != null) { - searchString = searchString.substring(idx + 1); - } - } +// Liftover liftover = null; +// if (searchString.startsWith("!") && genome.getLiftoverMap() != null) { +// int idx = searchString.indexOf(' '); +// String genomeKey = searchString.substring(1, idx); +// liftover = genome.getLiftoverMap().get(genomeKey); +// if (liftover != null) { +// searchString = searchString.substring(idx + 1); +// } +// } List results = new ArrayList<>(); searchString = searchString.replace("\"", ""); - Set wholeStringType = checkTokenType(searchString); - if (wholeStringType.contains(ResultType.LOCUS)) { - results.add(calcChromoLocus(searchString)); - } else { - // Space delimited? - String[] tokens = searchString.split("\\s+"); - for (String s : tokens) { - SearchResult result = parseToken(s); - if (result != null) { - results.add(result); - } else { - SearchResult unknownResult = new SearchResult(); - unknownResult.setMessage("Unknown search term: " + s); - results.add(unknownResult); + // If the search string is space delimited see if it looks like a space delimited locus string (e.g. chr 100 200) + String[] tokens = searchString.split("\\s+"); + if (tokens.length > 1 && tokens.length <= 3) { + boolean mightBeLocus = true; + for (int i = 1; i < tokens.length; i++) { + mightBeLocus = mightBeLocus && isInteger(tokens[i]); + } + if (mightBeLocus) { + Chromosome c1 = genome.getChromosome(tokens[0]); + if(c1 != null) { + Chromosome c2 = genome.getChromosome(tokens[1]); + if(c2 == null) { + results.add (calcChromoLocus(searchString)); + return results; + } } } } - if (results.size() == 0) { - SearchResult result = new SearchResult(); - result.setMessage("\"" + searchString + " \" not found."); - results.add(result); + for (String s : tokens) { + SearchResult result = parseToken(s); + if (result != null) { + results.add(result); + } } + + + // If this is a liftover search map the results - // TODO -- support gene name lookup - if(liftover != null) { - List mappedResults = new ArrayList<>(); - for(SearchResult result : results) { - if(result.getType() == ResultType.LOCUS) { - List mapped = liftover.map(new Range(result.getChr(), result.getStart(), result.getEnd())); - for(Range m : mapped) { - mappedResults.add(new SearchResult(result.type, m.chr, m.start, m.end)); - } - } else { - // ??? Error - } - } - results = mappedResults; - } +// if (liftover != null) { +// List mappedResults = new ArrayList<>(); +// for (SearchResult result : results) { +// if (result.getType() == ResultType.LOCUS) { +// List mapped = liftover.map(new Range(result.getChr(), result.getStart(), result.getEnd())); +// for (Range m : mapped) { +// mappedResults.add(new SearchResult(result.type, m.chr, m.start, m.end)); +// } +// } else { +// // ??? Error +// } +// } +// results = mappedResults; +// } return results; } @@ -336,20 +337,36 @@ Set checkTokenType(String token) { */ private SearchResult parseToken(String token) { - List features; + // Check featureDB first -- this is cheap + NamedFeature feat = searchFeatureDBs(token); + if (feat != null) { + return new SearchResult(feat); + } //Guess at token type via regex. //We don't assume success Set types = checkTokenType(token); - SearchResult result; + if (types.contains(ResultType.LOCUS) || types.contains(ResultType.CHROMOSOME)) { //Check if a full or partial locus string - result = calcChromoLocus(token); - if (result.type != ResultType.ERROR) { + SearchResult result = calcChromoLocus(token); + if (result != null) { return result; } } + + if (types.contains(ResultType.FEATURE)) { + //Check if we have an exact match for the feature name + List searchableTracks = IGV.getInstance().getAllTracks().stream().filter(Track::isSearchable).toList(); + for (Track t : searchableTracks) { + NamedFeature match = t.search(token); + if (match != null) { + return new SearchResult(match); + } + } + } + //2 possible mutation notations, either amino acid (A123B) or nucleotide (123G>C) if (types.contains(ResultType.FEATURE_MUT_AA) || types.contains(ResultType.FEATURE_MUT_NT)) { //We know it has the right form, but may @@ -383,20 +400,15 @@ private SearchResult parseToken(String token) { } for (int genomePos : genomePosList.keySet()) { - Feature feat = genomePosList.get(genomePos); + Feature feature = genomePosList.get(genomePos); //Zoom in on mutation of interest //The +2 accounts for centering on the center of the amino acid, not beginning //and converting from 0-based to 1-based (which getStartEnd expects) int[] locs = getStartEnd("" + (genomePos + 2)); - return new SearchResult(ResultType.LOCUS, feat.getChr(), locs[0], locs[1]); - } - } else if (types.contains(ResultType.FEATURE)) { - //Check if we have an exact name for the feature name - NamedFeature feat = searchFeatureDBs(token); - if (feat != null) { - return new SearchResult(feat); + return new SearchResult(ResultType.LOCUS, feature.getChr(), locs[0], locs[1]); } } + return null; } @@ -473,12 +485,11 @@ private SearchResult calcChromoLocus(String searchString) { } //startEnd will have coordinates if found. - chr = genome.getCanonicalChrName(chr); Chromosome chromosome = genome.getChromosome(chr); //If we couldn't find chromosome, check //whole string if (chromosome == null) { - chr = genome.getCanonicalChrName(tokens[0]); + chr = tokens[0]; chromosome = genome.getChromosome(chr); if (chromosome != null) { //Found chromosome @@ -488,17 +499,13 @@ private SearchResult calcChromoLocus(String searchString) { if (chromosome != null && !searchString.equals(Globals.CHR_ALL)) { if (startEnd != null) { - if (startEnd[1] >= startEnd[0]) { - return new SearchResult(ResultType.LOCUS, chr, startEnd[0], startEnd[1]); - } else { - SearchResult error = new SearchResult(ResultType.ERROR, chr, startEnd[0], startEnd[1]); - error.setMessage("End must be greater than start"); - return error; - } + int start = Math.min(startEnd[0], startEnd[1]); + int end = Math.max(startEnd[0], startEnd[1]); + return new SearchResult(ResultType.LOCUS, chr, start, end); } return new SearchResult(ResultType.CHROMOSOME, chr, 0, chromosome.getLength() - 1); } - return new SearchResult(ResultType.ERROR, chr, -1, -1); + return null; } private void showFlankedRegion(String chr, int start, int end) { @@ -675,4 +682,11 @@ public static List getResults(List objects) { return results; } + private static boolean isInteger(String str) { + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (c < '0' || c > '9') return false; + } + return true; + } } \ No newline at end of file diff --git a/src/main/java/org/broad/igv/ui/commandbar/SearchTextField.java b/src/main/java/org/broad/igv/ui/commandbar/SearchTextField.java index bf50368aca..8444142639 100644 --- a/src/main/java/org/broad/igv/ui/commandbar/SearchTextField.java +++ b/src/main/java/org/broad/igv/ui/commandbar/SearchTextField.java @@ -7,6 +7,7 @@ import org.broad.igv.logging.Logger; import org.broad.igv.ui.action.SearchCommand; import org.broad.igv.ui.panel.FrameManager; +import org.broad.igv.util.LongRunningTask; import javax.swing.*; import javax.swing.text.JTextComponent; @@ -32,9 +33,7 @@ public SearchTextField() { public void searchByLocus(final String searchText) { - - (new SearchCommand(FrameManager.getDefaultFrame(), searchText)).execute(); - + LongRunningTask.submit((new SearchCommand(FrameManager.getDefaultFrame(), searchText))); } private class SearchHints extends ListDataIntelliHints {