From: gmungoc Date: Tue, 16 Jul 2019 16:13:19 +0000 (+0100) Subject: Merge branch 'develop' into feature/JAL-3187linkedFeatures X-Git-Tag: Release_2_11_1_0~31 X-Git-Url: http://source.jalview.org/gitweb/?p=jalview.git;a=commitdiff_plain;h=c3f8260b41c41ceca04c86f3fe56059e4704d834;hp=140c8de58b9a30d013e7fcc3904df91fa81880fc Merge branch 'develop' into feature/JAL-3187linkedFeatures --- diff --git a/resources/lang/Messages.properties b/resources/lang/Messages.properties index d2d8e88..fdd15cd 100644 --- a/resources/lang/Messages.properties +++ b/resources/lang/Messages.properties @@ -1400,3 +1400,7 @@ label.pca = PCA label.create_image_of = Create {0} image of {1} label.click_to_edit = Click to edit, right-click for menu label.by_annotation_tooltip = Annotation Colour is configured from the main Colour menu +label.show_linked_features = Show {0} features +label.on_top = on top +label.include_linked_features = Include {0} features +label.include_linked_tooltip = Include visible {0} features
converted to local sequence coordinates \ No newline at end of file diff --git a/resources/lang/Messages_es.properties b/resources/lang/Messages_es.properties index e9e18ce..daa4418 100644 --- a/resources/lang/Messages_es.properties +++ b/resources/lang/Messages_es.properties @@ -1401,3 +1401,7 @@ label.pca = ACP label.create_image_of = Crear imagen {0} de {1} label.click_to_edit = Haga clic para editar, clic en el botón derecho para ver el menú label.by_annotation_tooltip = El color de anotación se configura desde el menú principal de colores +label.show_linked_features = Características de {0} +label.on_top = encima +label.include_linked_features = Incluir características de {0} +label.include_linked_tooltip = Incluir características de {0}
convertidas a coordenadas de secuencia local \ No newline at end of file diff --git a/src/jalview/analysis/AlignmentUtils.java b/src/jalview/analysis/AlignmentUtils.java index 0bc8180..8e0335f 100644 --- a/src/jalview/analysis/AlignmentUtils.java +++ b/src/jalview/analysis/AlignmentUtils.java @@ -22,6 +22,7 @@ package jalview.analysis; import static jalview.io.gff.GffConstants.CLINICAL_SIGNIFICANCE; +import jalview.commands.RemoveGapColCommand; import jalview.datamodel.AlignedCodon; import jalview.datamodel.AlignedCodonFrame; import jalview.datamodel.AlignedCodonFrame.SequenceToSequenceMapping; @@ -1746,8 +1747,10 @@ public class AlignmentUtils /* * add a mapping from CDS to the (unchanged) mapped to range */ - List cdsRange = Collections.singletonList(new int[] { 1, - cdsSeq.getLength() }); + List cdsRange = Collections + .singletonList(new int[] + { cdsSeq.getStart(), + cdsSeq.getLength() + cdsSeq.getStart() - 1 }); MapList cdsToProteinMap = new MapList(cdsRange, mapList.getToRanges(), mapList.getFromRatio(), mapList.getToRatio()); @@ -1984,39 +1987,61 @@ public class AlignmentUtils static SequenceI makeCdsSequence(SequenceI seq, Mapping mapping, AlignmentI dataset) { - char[] seqChars = seq.getSequence(); - List fromRanges = mapping.getMap().getFromRanges(); - int cdsWidth = MappingUtils.getLength(fromRanges); - char[] newSeqChars = new char[cdsWidth]; + /* + * construct CDS sequence name as "CDS|" with 'from id' held in the mapping + * if set (e.g. EMBL protein_id), else sequence name appended + */ + String mapFromId = mapping.getMappedFromId(); + final String seqId = "CDS|" + + (mapFromId != null ? mapFromId : seq.getName()); + + SequenceI newSeq = null; - int newPos = 0; - for (int[] range : fromRanges) + final MapList maplist = mapping.getMap(); + if (maplist.isContiguous() && maplist.isFromForwardStrand()) { - if (range[0] <= range[1]) - { - // forward strand mapping - just copy the range - int length = range[1] - range[0] + 1; - System.arraycopy(seqChars, range[0] - 1, newSeqChars, newPos, - length); - newPos += length; - } - else + /* + * just a subsequence, keep same dataset sequence + */ + int start = maplist.getFromLowest(); + int end = maplist.getFromHighest(); + newSeq = seq.getSubSequence(start - 1, end); + newSeq.setName(seqId); + } + else + { + /* + * construct by splicing mapped from ranges + */ + char[] seqChars = seq.getSequence(); + List fromRanges = maplist.getFromRanges(); + int cdsWidth = MappingUtils.getLength(fromRanges); + char[] newSeqChars = new char[cdsWidth]; + + int newPos = 0; + for (int[] range : fromRanges) { - // reverse strand mapping - copy and complement one by one - for (int i = range[0]; i >= range[1]; i--) + if (range[0] <= range[1]) + { + // forward strand mapping - just copy the range + int length = range[1] - range[0] + 1; + System.arraycopy(seqChars, range[0] - 1, newSeqChars, newPos, + length); + newPos += length; + } + else { - newSeqChars[newPos++] = Dna.getComplement(seqChars[i - 1]); + // reverse strand mapping - copy and complement one by one + for (int i = range[0]; i >= range[1]; i--) + { + newSeqChars[newPos++] = Dna.getComplement(seqChars[i - 1]); + } } } + + newSeq = new Sequence(seqId, newSeqChars, 1, newPos); } - /* - * assign 'from id' held in the mapping if set (e.g. EMBL protein_id), - * else generate a sequence name - */ - String mapFromId = mapping.getMappedFromId(); - String seqId = "CDS|" + (mapFromId != null ? mapFromId : seq.getName()); - SequenceI newSeq = new Sequence(seqId, newSeqChars, 1, newPos); if (dataset != null) { SequenceI[] matches = dataset.findSequenceMatch(newSeq.getName()); @@ -2159,6 +2184,10 @@ public class AlignmentUtils { copyTo = copyTo.getDatasetSequence(); } + if (fromSeq == copyTo || fromSeq.getDatasetSequence() == copyTo) + { + return 0; // shared dataset sequence + } /* * get features, optionally restricted by an ontology term @@ -2412,23 +2441,24 @@ public class AlignmentUtils } /** - * Computes non-synonymous peptide variants from codon variants and adds them - * as sequence_variant features on the protein sequence (one feature per - * allele variant). Selected attributes (variant id, clinical significance) - * are copied over to the new features. + * Computes non-synonymous peptide variants from codon variants and adds them as + * sequence_variant features on the protein sequence (one feature per allele + * variant). Selected attributes (variant id, clinical significance) are copied + * over to the new features. * * @param peptide - * the protein sequence + * the protein dataset (ungapped) sequence * @param peptidePos - * the position to compute peptide variants for + * the position to compute peptide variants for * @param codonVariants - * a list of dna variants per codon position + * a list of dna variants per codon position * @return the number of features added */ static int computePeptideVariants(SequenceI peptide, int peptidePos, List[] codonVariants) { - String residue = String.valueOf(peptide.getCharAt(peptidePos - 1)); + String residue = String + .valueOf(peptide.getCharAt(peptidePos - peptide.getStart())); int count = 0; String base1 = codonVariants[0].get(0).base; String base2 = codonVariants[1].get(0).base; @@ -2879,10 +2909,10 @@ public class AlignmentUtils * true; else returns false * * @param unaligned - * - sequences to be aligned based on aligned + * - sequences to be aligned based on aligned * @param aligned - * - 'guide' alignment containing sequences derived from same dataset - * as unaligned + * - 'guide' alignment containing sequences derived from same + * dataset as unaligned * @return */ static boolean alignAsSameSequences(AlignmentI unaligned, @@ -2906,15 +2936,22 @@ public class AlignmentUtils } /* - * first pass - check whether all sequences to be aligned share a dataset - * sequence with an aligned sequence + * first pass - check whether all sequences to be aligned share a + * dataset sequence with an aligned sequence; also note the leftmost + * ungapped column from which to copy */ + int leftmost = Integer.MAX_VALUE; for (SequenceI seq : unaligned.getSequences()) { - if (!alignedDatasets.containsKey(seq.getDatasetSequence())) + final SequenceI ds = seq.getDatasetSequence(); + if (!alignedDatasets.containsKey(ds)) { return false; } + SequenceI alignedSeq = alignedDatasets.get(ds) + .get(0); + int startCol = alignedSeq.findIndex(seq.getStart()); // 1.. + leftmost = Math.min(leftmost, startCol); } /* @@ -2922,13 +2959,25 @@ public class AlignmentUtils * heuristic rule: pair off sequences in order for the case where * more than one shares the same dataset sequence */ + final char gapCharacter = aligned.getGapCharacter(); for (SequenceI seq : unaligned.getSequences()) { List alignedSequences = alignedDatasets .get(seq.getDatasetSequence()); - // TODO: getSequenceAsString() will be deprecated in the future - // TODO: need to leave to SequenceI implementor to update gaps - seq.setSequence(alignedSequences.get(0).getSequenceAsString()); + SequenceI alignedSeq = alignedSequences.get(0); + + /* + * gap fill for leading (5') UTR if any + */ + // TODO this copies intron columns - wrong! + int startCol = alignedSeq.findIndex(seq.getStart()); // 1.. + int endCol = alignedSeq.findIndex(seq.getEnd()); + char[] seqchars = new char[endCol - leftmost + 1]; + Arrays.fill(seqchars, gapCharacter); + char[] toCopy = alignedSeq.getSequence(startCol - 1, endCol); + System.arraycopy(toCopy, 0, seqchars, startCol - leftmost, + toCopy.length); + seq.setSequence(String.valueOf(seqchars)); if (alignedSequences.size() > 0) { // pop off aligned sequences (except the last one) @@ -2936,6 +2985,12 @@ public class AlignmentUtils } } + /* + * finally remove gapped columns (e.g. introns) + */ + new RemoveGapColCommand("", unaligned.getSequencesArray(), 0, + unaligned.getWidth() - 1, unaligned); + return true; } diff --git a/src/jalview/analysis/scoremodels/FeatureDistanceModel.java b/src/jalview/analysis/scoremodels/FeatureDistanceModel.java index 8545e94..0aa77fa 100644 --- a/src/jalview/analysis/scoremodels/FeatureDistanceModel.java +++ b/src/jalview/analysis/scoremodels/FeatureDistanceModel.java @@ -202,7 +202,7 @@ public class FeatureDistanceModel extends DistanceScoreModel */ Set types = new HashSet<>(); List sfs = fr.findFeaturesAtResidue( - seq.getRefSeq(), spos); + seq.getRefSeq(), spos, spos); for (SequenceFeature sf : sfs) { types.add(sf.getType()); diff --git a/src/jalview/api/FeatureRenderer.java b/src/jalview/api/FeatureRenderer.java index 404c497..8aa2858 100644 --- a/src/jalview/api/FeatureRenderer.java +++ b/src/jalview/api/FeatureRenderer.java @@ -20,6 +20,7 @@ */ package jalview.api; +import jalview.datamodel.MappedFeatures; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; import jalview.datamodel.features.FeatureMatcherSetI; @@ -161,15 +162,18 @@ public interface FeatureRenderer List findFeaturesAtColumn(SequenceI sequence, int column); /** - * Returns features at the specified residue position on the given sequence. - * Non-positional features are not included. + * Returns features at the specified residue positions on the given sequence. + * Non-positional features are not included. Features are returned in render + * order of their feature type (last is on top). Within feature type, ordering + * is undefined. * * @param sequence - * @param resNo - * residue position (start..) + * @param fromResNo + * @param toResNo * @return */ - List findFeaturesAtResidue(SequenceI sequence, int resNo); + List findFeaturesAtResidue(SequenceI sequence, + int fromResNo, int toResNo); /** * get current displayed types, in ordering of rendering (on top last) @@ -280,4 +284,17 @@ public interface FeatureRenderer * @return */ boolean isVisible(SequenceFeature feature); + + /** + * Answers a bean containing a mapping, and a list of visible features in this + * alignment at a position (or range) which is mappable from the given sequence + * residue position in a mapped alignment. Features are returned in render order + * of feature type (on top last), with order within feature type undefined. If + * no features or mapping are found, answers null. + * + * @param sequence + * @param pos + * @return + */ + MappedFeatures findComplementFeaturesAtResidue(SequenceI sequence, int pos); } diff --git a/src/jalview/api/ViewStyleI.java b/src/jalview/api/ViewStyleI.java index 2b554ea..a348300 100644 --- a/src/jalview/api/ViewStyleI.java +++ b/src/jalview/api/ViewStyleI.java @@ -24,6 +24,13 @@ import java.awt.Color; public interface ViewStyleI { + void setShowComplementFeatures(boolean b); + + boolean isShowComplementFeatures(); + + void setShowComplementFeaturesOnTop(boolean b); + + boolean isShowComplementFeaturesOnTop(); void setColourAppliesToAllGroups(boolean b); diff --git a/src/jalview/appletgui/AlignFrame.java b/src/jalview/appletgui/AlignFrame.java index eb3be88..3bb5fe8 100644 --- a/src/jalview/appletgui/AlignFrame.java +++ b/src/jalview/appletgui/AlignFrame.java @@ -1448,12 +1448,13 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener, { features = formatter.printJalviewFormat( viewport.getAlignment().getSequencesArray(), - alignPanel.getFeatureRenderer(), true); + alignPanel.getFeatureRenderer(), true, false); } else { features = formatter.printGffFormat(viewport.getAlignment() - .getSequencesArray(), alignPanel.getFeatureRenderer(), true); + .getSequencesArray(), alignPanel.getFeatureRenderer(), true, + false); } if (displayTextbox) diff --git a/src/jalview/appletgui/SeqPanel.java b/src/jalview/appletgui/SeqPanel.java index fee68c8..2c07153 100644 --- a/src/jalview/appletgui/SeqPanel.java +++ b/src/jalview/appletgui/SeqPanel.java @@ -744,7 +744,7 @@ public class SeqPanel extends Panel implements MouseMotionListener, } @Override - public void highlightSequence(SearchResultsI results) + public String highlightSequence(SearchResultsI results) { if (av.isFollowHighlight()) { @@ -761,7 +761,7 @@ public class SeqPanel extends Panel implements MouseMotionListener, } setStatusMessage(results); seqCanvas.highlightSearchResults(results); - + return null; } @Override diff --git a/src/jalview/datamodel/AlignedCodonFrame.java b/src/jalview/datamodel/AlignedCodonFrame.java index ec11fc1..fffa137 100644 --- a/src/jalview/datamodel/AlignedCodonFrame.java +++ b/src/jalview/datamodel/AlignedCodonFrame.java @@ -116,7 +116,7 @@ public class AlignedCodonFrame */ public AlignedCodonFrame() { - mappings = new ArrayList(); + mappings = new ArrayList<>(); } /** @@ -179,7 +179,7 @@ public class AlignedCodonFrame { // TODO return a list instead? // return dnaSeqs; - List seqs = new ArrayList(); + List seqs = new ArrayList<>(); for (SequenceToSequenceMapping ssm : mappings) { seqs.add(ssm.fromSeq); @@ -190,7 +190,7 @@ public class AlignedCodonFrame public SequenceI[] getAaSeqs() { // TODO not used - remove? - List seqs = new ArrayList(); + List seqs = new ArrayList<>(); for (SequenceToSequenceMapping ssm : mappings) { seqs.add(ssm.mapping.to); @@ -200,7 +200,7 @@ public class AlignedCodonFrame public MapList[] getdnaToProt() { - List maps = new ArrayList(); + List maps = new ArrayList<>(); for (SequenceToSequenceMapping ssm : mappings) { maps.add(ssm.mapping.map); @@ -210,7 +210,7 @@ public class AlignedCodonFrame public Mapping[] getProtMappings() { - List maps = new ArrayList(); + List maps = new ArrayList<>(); for (SequenceToSequenceMapping ssm : mappings) { maps.add(ssm.mapping); @@ -220,7 +220,7 @@ public class AlignedCodonFrame /** * Returns the first mapping found which is to or from the given sequence, or - * null. + * null if none is found * * @param seq * @return @@ -485,7 +485,7 @@ public class AlignedCodonFrame { MapList ml = null; SequenceI dnaSeq = null; - List result = new ArrayList(); + List result = new ArrayList<>(); for (SequenceToSequenceMapping ssm : mappings) { @@ -524,8 +524,8 @@ public class AlignedCodonFrame */ public List getMappingsFromSequence(SequenceI seq) { - List result = new ArrayList(); - List related = new ArrayList(); + List result = new ArrayList<>(); + List related = new ArrayList<>(); SequenceI seqDs = seq.getDatasetSequence(); seqDs = seqDs != null ? seqDs : seq; diff --git a/src/jalview/datamodel/MappedFeatures.java b/src/jalview/datamodel/MappedFeatures.java new file mode 100644 index 0000000..3f43355 --- /dev/null +++ b/src/jalview/datamodel/MappedFeatures.java @@ -0,0 +1,236 @@ +package jalview.datamodel; + +import jalview.io.gff.Gff3Helper; +import jalview.schemes.ResidueProperties; +import jalview.util.MappingUtils; +import jalview.util.StringUtils; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * A data bean to hold a list of mapped sequence features (e.g. CDS features + * mapped from protein), and the mapping between the sequences. It also provides + * a method to derive peptide variants from codon variants. + * + * @author gmcarstairs + */ +public class MappedFeatures +{ + private static final String HGV_SP = "HGVSp"; + + private static final String CSQ = "CSQ"; + + /* + * the mapping from one sequence to another + */ + public final Mapping mapping; + + /** + * the sequence mapped from + */ + public final SequenceI fromSeq; + + /* + * features on the sequence mapped to that overlap the mapped positions + */ + public final List features; + + /* + * the residue position in the sequence mapped to + */ + private final int toPosition; + + /* + * the residue at toPosition + */ + private final char toResidue; + + /* + * if the mapping is 3:1 or 1:3 (peptide to CDS), this holds the + * mapped positions i.e. codon base positions in CDS; to + * support calculation of peptide variants from alleles + */ + private final int[] codonPos; + + private final char[] baseCodon; + + /** + * Constructor + * + * @param theMapping + * @param from + * the sequence mapped from (e.g. CDS) + * @param pos + * the residue position in the sequence mapped to + * @param res + * the residue character at position pos + * @param theFeatures + * list of mapped features found in the 'from' sequence at + * the mapped position(s) + */ + public MappedFeatures(Mapping theMapping, SequenceI from, int pos, + char res, List theFeatures) + { + mapping = theMapping; + fromSeq = from; + toPosition = pos; + toResidue = res; + features = theFeatures; + + /* + * determine codon positions and canonical codon + * for a peptide-to-CDS mapping + */ + int[] codonIntervals = mapping.getMap().locateInFrom(toPosition, toPosition); + if (codonIntervals != null) + { + codonPos = MappingUtils.flattenRanges(codonIntervals); + if (codonPos.length == 3) + { + baseCodon = new char[3]; + int cdsStart = fromSeq.getStart(); + baseCodon[0] = fromSeq.getCharAt(codonPos[0] - cdsStart); + baseCodon[1] = fromSeq.getCharAt(codonPos[1] - cdsStart); + baseCodon[2] = fromSeq.getCharAt(codonPos[2] - cdsStart); + } + else + { + baseCodon = null; + } + } + else + { + codonPos = null; + baseCodon = null; // todo tidy! + } + } + + /** + * Computes and returns comma-delimited HGVS notation peptide variants derived + * from codon allele variants. If no variants are found, answers an empty + * string. + * + * @param sf + * a sequence feature (which must be one of those held in this + * object) + * @return + */ + public String findProteinVariants(SequenceFeature sf) + { + if (!features.contains(sf) || baseCodon == null) + { + return ""; + } + + /* + * VCF data may already contain the protein consequence + */ + String hgvsp = sf.getValueAsString(CSQ, HGV_SP); + if (hgvsp != null) + { + int colonPos = hgvsp.lastIndexOf(':'); + if (colonPos >= 0) + { + String var = hgvsp.substring(colonPos + 1); + return var; + } + } + + /* + * otherwise, compute codon and peptide variant + */ + // todo avoid duplication of code in AlignmentUtils.buildDnaVariantsMap + int cdsPos = sf.getBegin(); + if (cdsPos != sf.getEnd()) + { + // not handling multi-locus variant features + return ""; + } + if (cdsPos != codonPos[0] && cdsPos != codonPos[1] + && cdsPos != codonPos[2]) + { + // e.g. feature on intron within spliced codon! + return ""; + } + + String alls = (String) sf.getValue(Gff3Helper.ALLELES); + if (alls == null) + { + return ""; + } + + String from3 = StringUtils.toSentenceCase( + ResidueProperties.aa2Triplet.get(String.valueOf(toResidue))); + + /* + * make a peptide variant for each SNP allele + * e.g. C,G,T gives variants G and T for base C + */ + Set variantPeptides = new HashSet<>(); + String[] alleles = alls.toUpperCase().split(","); + StringBuilder vars = new StringBuilder(); + + for (String allele : alleles) + { + allele = allele.trim().toUpperCase(); + if (allele.length() > 1 || "-".equals(allele)) + { + continue; // multi-locus variant + } + char[] variantCodon = new char[3]; + variantCodon[0] = baseCodon[0]; + variantCodon[1] = baseCodon[1]; + variantCodon[2] = baseCodon[2]; + + /* + * poke variant base into canonical codon; + * ignore first 'allele' (canonical base) + */ + final int i = cdsPos == codonPos[0] ? 0 + : (cdsPos == codonPos[1] ? 1 : 2); + variantCodon[i] = allele.toUpperCase().charAt(0); + if (variantCodon[i] == baseCodon[i]) + { + continue; + } + String codon = new String(variantCodon); + String peptide = ResidueProperties.codonTranslate(codon); + boolean synonymous = toResidue == peptide.charAt(0); + StringBuilder var = new StringBuilder(); + if (synonymous) + { + /* + * synonymous variant notation e.g. c.1062C>A(p.=) + */ + var.append("c.").append(String.valueOf(cdsPos)) + .append(String.valueOf(baseCodon[i])).append(">") + .append(String.valueOf(variantCodon[i])) + .append("(p.=)"); + } + else + { + /* + * missense variant notation e.g. p.Arg355Met + */ + String to3 = ResidueProperties.STOP.equals(peptide) ? "Ter" + : StringUtils.toSentenceCase( + ResidueProperties.aa2Triplet.get(peptide)); + var.append("p.").append(from3).append(String.valueOf(toPosition)) + .append(to3); + } + if (!variantPeptides.contains(peptide)) // duplicate consequence + { + variantPeptides.add(peptide); + if (vars.length() > 0) + { + vars.append(","); + } + vars.append(var); + } + } + + return vars.toString(); + } +} diff --git a/src/jalview/datamodel/SequenceFeature.java b/src/jalview/datamodel/SequenceFeature.java index 7052f34..c8a7def 100755 --- a/src/jalview/datamodel/SequenceFeature.java +++ b/src/jalview/datamodel/SequenceFeature.java @@ -345,6 +345,11 @@ public class SequenceFeature implements FeatureLocationI return featureGroup; } + /** + * Adds a hyperlink for the feature. This should have the format label|url. + * + * @param labelLink + */ public void addLink(String labelLink) { if (links == null) @@ -602,9 +607,11 @@ public class SequenceFeature implements FeatureLocationI /** * Answers an html-formatted report of feature details * + * @param seqName + * * @return */ - public String getDetailsReport() + public String getDetailsReport(String seqName) { FeatureSourceI metadata = FeatureSources.getInstance() .getSource(source); @@ -612,9 +619,10 @@ public class SequenceFeature implements FeatureLocationI StringBuilder sb = new StringBuilder(128); sb.append("
"); sb.append(""); + sb.append(String.format(ROW_DATA, "Location", seqName, + begin == end ? begin + : begin + (isContactFeature() ? ":" : "-") + end)); sb.append(String.format(ROW_DATA, "Type", type, "")); - sb.append(String.format(ROW_DATA, "Start/end", begin == end ? begin - : begin + (isContactFeature() ? ":" : "-") + end, "")); String desc = StringUtils.stripHtmlTags(description); sb.append(String.format(ROW_DATA, "Description", desc, "")); if (!Float.isNaN(score) && score != 0f) diff --git a/src/jalview/datamodel/features/SequenceFeatures.java b/src/jalview/datamodel/features/SequenceFeatures.java index ba8396a..db2f0e1 100644 --- a/src/jalview/datamodel/features/SequenceFeatures.java +++ b/src/jalview/datamodel/features/SequenceFeatures.java @@ -25,7 +25,6 @@ import jalview.io.gff.SequenceOntologyFactory; import jalview.io.gff.SequenceOntologyI; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -204,7 +203,9 @@ public class SequenceFeatures implements SequenceFeaturesI /** * A convenience method that converts a vararg for feature types to an - * Iterable over matched feature sets in key order + * Iterable over matched feature sets. If no types are specified, all feature + * sets are returned. If one or more types are specified, feature sets for + * those types are returned, preserving the order of the types. * * @param type * @return @@ -220,12 +221,11 @@ public class SequenceFeatures implements SequenceFeaturesI } List types = new ArrayList<>(); - List args = Arrays.asList(type); - for (Entry featureType : featureStore.entrySet()) + for (String theType : type) { - if (args.contains(featureType.getKey())) + if (theType != null && featureStore.containsKey(theType)) { - types.add(featureType.getValue()); + types.add(featureStore.get(theType)); } } return types; diff --git a/src/jalview/datamodel/features/SequenceFeaturesI.java b/src/jalview/datamodel/features/SequenceFeaturesI.java index ca0283e..7213cba 100644 --- a/src/jalview/datamodel/features/SequenceFeaturesI.java +++ b/src/jalview/datamodel/features/SequenceFeaturesI.java @@ -42,7 +42,8 @@ public interface SequenceFeaturesI /** * Returns a (possibly empty) list of features, optionally restricted to * specified types, which overlap the given (inclusive) sequence position - * range + * range. If types are specified, features are returned in the order of the + * types given. * * @param from * @param to diff --git a/src/jalview/ext/ensembl/EnsemblSeqProxy.java b/src/jalview/ext/ensembl/EnsemblSeqProxy.java index 5dc701d..001e18e 100644 --- a/src/jalview/ext/ensembl/EnsemblSeqProxy.java +++ b/src/jalview/ext/ensembl/EnsemblSeqProxy.java @@ -330,8 +330,8 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient * copy exon features to protein, compute peptide variants from dna * variants and add as features on the protein sequence ta-da */ - AlignmentUtils.computeProteinFeatures(querySeq, proteinSeq, - mapList); + // JAL-3187 render on the fly instead + // AlignmentUtils.computeProteinFeatures(querySeq, proteinSeq, mapList); } } catch (Exception e) { diff --git a/src/jalview/ext/jmol/JalviewJmolBinding.java b/src/jalview/ext/jmol/JalviewJmolBinding.java index c0a1e0d..1ceabd1 100644 --- a/src/jalview/ext/jmol/JalviewJmolBinding.java +++ b/src/jalview/ext/jmol/JalviewJmolBinding.java @@ -49,6 +49,7 @@ import java.util.BitSet; import java.util.Hashtable; import java.util.List; import java.util.Map; +import java.util.StringTokenizer; import java.util.Vector; import org.jmol.adapter.smarter.SmarterJmolAdapter; @@ -64,6 +65,8 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel implements JmolStatusListener, JmolSelectionListener, ComponentListener { + private String lastMessage; + boolean allChainsSelected = false; /* @@ -88,8 +91,6 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel String lastCommand; - String lastMessage; - boolean loadedInline; StringBuffer resetLastRes = new StringBuffer(); @@ -753,7 +754,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel viewer.openStringInline(string); } - public void mouseOverStructure(int atomIndex, String strInfo) + protected void mouseOverStructure(int atomIndex, final String strInfo) { int pdbResNum; int alocsep = strInfo.indexOf("^"); @@ -834,18 +835,36 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel } catch (Exception e) { } - ; } - if (lastMessage == null || !lastMessage.equals(strInfo)) + + /* + * highlight position on alignment(s); if some text is returned, + * show this as a second line on the structure hover tooltip + */ + String label = getSsm().mouseOverStructure(pdbResNum, chainId, + pdbfilename); + if (label != null) { - getSsm().mouseOverStructure(pdbResNum, chainId, pdbfilename); + // change comma to pipe separator (newline token for Jmol) + label = label.replace(',', '|'); + StringTokenizer toks = new StringTokenizer(strInfo, " "); + StringBuilder sb = new StringBuilder(); + sb.append("select ").append(String.valueOf(pdbResNum)).append(":") + .append(chainId).append("/1"); + sb.append(";set hoverLabel \"").append(toks.nextToken()).append(" ") + .append(toks.nextToken()); + sb.append("|").append(label).append("\""); + evalStateCommand(sb.toString()); } - - lastMessage = strInfo; } public void notifyAtomHovered(int atomIndex, String strInfo, String data) { + if (strInfo.equals(lastMessage)) + { + return; + } + lastMessage = strInfo; if (data != null) { System.err.println("Ignoring additional hover info: " + data diff --git a/src/jalview/ext/rbvi/chimera/ChimeraCommands.java b/src/jalview/ext/rbvi/chimera/ChimeraCommands.java index dad8511..3caaac3 100644 --- a/src/jalview/ext/rbvi/chimera/ChimeraCommands.java +++ b/src/jalview/ext/rbvi/chimera/ChimeraCommands.java @@ -26,8 +26,10 @@ import jalview.api.FeatureRenderer; import jalview.api.SequenceRenderer; import jalview.datamodel.AlignmentI; import jalview.datamodel.HiddenColumns; +import jalview.datamodel.MappedFeatures; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; +import jalview.gui.Desktop; import jalview.renderer.seqfeatures.FeatureColourFinder; import jalview.structure.StructureMapping; import jalview.structure.StructureMappingcommandSet; @@ -104,7 +106,7 @@ public class ChimeraCommands * delimited). If length limit issues arise, refactor to return one color * command per colour. */ - List commands = new ArrayList(); + List commands = new ArrayList<>(); StringBuilder sb = new StringBuilder(256); boolean firstColour = true; for (Object key : colourMap.keySet()) @@ -196,7 +198,7 @@ public class ChimeraCommands AlignViewportI viewport = viewPanel.getAlignViewport(); HiddenColumns cs = viewport.getAlignment().getHiddenColumns(); AlignmentI al = viewport.getAlignment(); - Map colourMap = new LinkedHashMap(); + Map colourMap = new LinkedHashMap<>(); Color lastColour = null; for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++) @@ -257,7 +259,7 @@ public class ChimeraCommands { if (startPos != -1) { - addColourRange(colourMap, lastColour, pdbfnum, startPos, + addAtomSpecRange(colourMap, lastColour, pdbfnum, startPos, lastPos, lastChain); } startPos = pos; @@ -269,7 +271,7 @@ public class ChimeraCommands // final colour range if (lastColour != null) { - addColourRange(colourMap, lastColour, pdbfnum, startPos, + addAtomSpecRange(colourMap, lastColour, pdbfnum, startPos, lastPos, lastChain); } // break; @@ -281,26 +283,32 @@ public class ChimeraCommands } /** - * Helper method to add one contiguous colour range to the colour map. + * Helper method to add one contiguous range to the AtomSpec model for the given + * value (creating the model if necessary). As used by Jalview, {@code value} is + *
    + *
  • a colour, when building a 'colour structure by sequence' command
  • + *
  • a feature value, when building a 'set Chimera attributes from features' + * command
  • + *
* * @param map - * @param key + * @param value * @param model * @param startPos * @param endPos * @param chain */ - protected static void addColourRange(Map map, - Object key, int model, int startPos, int endPos, String chain) + protected static void addAtomSpecRange(Map map, + Object value, int model, int startPos, int endPos, String chain) { /* * Get/initialize map of data for the colour */ - AtomSpecModel atomSpec = map.get(key); + AtomSpecModel atomSpec = map.get(value); if (atomSpec == null) { atomSpec = new AtomSpecModel(); - map.put(key, atomSpec); + map.put(value, atomSpec); } atomSpec.addRange(model, startPos, endPos, chain); @@ -349,7 +357,7 @@ public class ChimeraCommands StructureSelectionManager ssm, String[] files, SequenceI[][] seqs, AlignmentViewPanel viewPanel) { - Map> theMap = new LinkedHashMap>(); + Map> theMap = new LinkedHashMap<>(); FeatureRenderer fr = viewPanel.getFeatureRenderer(); if (fr == null) @@ -357,8 +365,27 @@ public class ChimeraCommands return theMap; } + AlignViewportI viewport = viewPanel.getAlignViewport(); List visibleFeatures = fr.getDisplayedFeatureTypes(); - if (visibleFeatures.isEmpty()) + + /* + * if alignment is showing features from complement, we also transfer + * these features to the corresponding mapped structure residues + */ + boolean showLinkedFeatures = viewport.isShowComplementFeatures(); + List complementFeatures = new ArrayList<>(); + FeatureRenderer complementRenderer = null; + if (showLinkedFeatures) + { + AlignViewportI comp = fr.getViewport().getCodingComplement(); + if (comp != null) + { + complementRenderer = Desktop.getAlignFrameFor(comp) + .getFeatureRenderer(); + complementFeatures = complementRenderer.getDisplayedFeatureTypes(); + } + } + if (visibleFeatures.isEmpty() && complementFeatures.isEmpty()) { return theMap; } @@ -379,16 +406,23 @@ public class ChimeraCommands { final SequenceI seq = seqs[pdbfnum][seqNo]; int sp = alignment.findIndex(seq); - if (mapping[m].getSequence() == seq && sp > -1) + StructureMapping structureMapping = mapping[m]; + if (structureMapping.getSequence() == seq && sp > -1) { /* * found a sequence with a mapping to a structure; * now scan its features */ - SequenceI asp = alignment.getSequenceAt(sp); - - scanSequenceFeatures(visibleFeatures, mapping[m], asp, theMap, - pdbfnum); + if (!visibleFeatures.isEmpty()) + { + scanSequenceFeatures(visibleFeatures, structureMapping, seq, + theMap, pdbfnum); + } + if (showLinkedFeatures) + { + scanComplementFeatures(complementRenderer, structureMapping, + seq, theMap, pdbfnum); + } } } } @@ -397,9 +431,85 @@ public class ChimeraCommands } /** - * Inspect features on the sequence; for each feature that is visible, - * determine its mapped ranges in the structure (if any) according to the - * given mapping, and add them to the map + * Scans visible features in mapped positions of the CDS/peptide complement, and + * adds any found to the map of attribute values/structure positions + * + * @param complementRenderer + * @param structureMapping + * @param seq + * @param theMap + * @param modelNumber + */ + protected static void scanComplementFeatures( + FeatureRenderer complementRenderer, + StructureMapping structureMapping, SequenceI seq, + Map> theMap, int modelNumber) + { + /* + * for each sequence residue mapped to a structure position... + */ + for (int seqPos : structureMapping.getMapping().keySet()) + { + /* + * find visible complementary features at mapped position(s) + */ + MappedFeatures mf = complementRenderer + .findComplementFeaturesAtResidue(seq, seqPos); + if (mf != null) + { + for (SequenceFeature sf : mf.features) + { + String type = sf.getType(); + + /* + * Don't copy features which originated from Chimera + */ + if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP + .equals(sf.getFeatureGroup())) + { + continue; + } + + /* + * record feature 'value' (score/description/type) as at the + * corresponding structure position + */ + List mappedRanges = structureMapping + .getPDBResNumRanges(seqPos, seqPos); + + if (!mappedRanges.isEmpty()) + { + String value = sf.getDescription(); + if (value == null || value.length() == 0) + { + value = type; + } + float score = sf.getScore(); + if (score != 0f && !Float.isNaN(score)) + { + value = Float.toString(score); + } + Map featureValues = theMap.get(type); + if (featureValues == null) + { + featureValues = new HashMap<>(); + theMap.put(type, featureValues); + } + for (int[] range : mappedRanges) + { + addAtomSpecRange(featureValues, value, modelNumber, range[0], + range[1], structureMapping.getChain()); + } + } + } + } + } + } + + /** + * Inspect features on the sequence; for each feature that is visible, determine + * its mapped ranges in the structure (if any) according to the given mapping, + * and add them to the map. * * @param visibleFeatures * @param mapping @@ -418,15 +528,14 @@ public class ChimeraCommands String type = sf.getType(); /* - * Only copy visible features, don't copy any which originated - * from Chimera, and suppress uninteresting ones (e.g. RESNUM) + * Don't copy features which originated from Chimera */ - boolean isFromViewer = JalviewChimeraBinding.CHIMERA_FEATURE_GROUP - .equals(sf.getFeatureGroup()); - if (isFromViewer) + if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP + .equals(sf.getFeatureGroup())) { continue; } + List mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(), sf.getEnd()); @@ -445,12 +554,12 @@ public class ChimeraCommands Map featureValues = theMap.get(type); if (featureValues == null) { - featureValues = new HashMap(); + featureValues = new HashMap<>(); theMap.put(type, featureValues); } for (int[] range : mappedRanges) { - addColourRange(featureValues, value, modelNumber, range[0], + addAtomSpecRange(featureValues, value, modelNumber, range[0], range[1], mapping.getChain()); } } @@ -475,7 +584,7 @@ public class ChimeraCommands protected static List buildSetAttributeCommands( Map> featureMap) { - List commands = new ArrayList(); + List commands = new ArrayList<>(); for (String featureType : featureMap.keySet()) { String attributeName = makeAttributeName(featureType); diff --git a/src/jalview/gui/AnnotationExporter.java b/src/jalview/gui/AnnotationExporter.java index fac531e..0b0916d 100644 --- a/src/jalview/gui/AnnotationExporter.java +++ b/src/jalview/gui/AnnotationExporter.java @@ -30,17 +30,17 @@ import jalview.io.JalviewFileChooser; import jalview.io.JalviewFileView; import jalview.util.MessageManager; -import java.awt.BorderLayout; import java.awt.Color; -import java.awt.FlowLayout; +import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.FileWriter; import java.io.PrintWriter; -import javax.swing.BorderFactory; +import javax.swing.BoxLayout; import javax.swing.ButtonGroup; import javax.swing.JButton; +import javax.swing.JCheckBox; import javax.swing.JInternalFrame; import javax.swing.JLabel; import javax.swing.JLayeredPane; @@ -71,6 +71,29 @@ public class AnnotationExporter extends JPanel private boolean wholeView; + /* + * option to export linked (CDS/peptide) features when shown + * on the alignment, converted to this alignment's coordinates + */ + private JCheckBox includeLinkedFeatures; + + /* + * output format option shown for feature export + */ + JRadioButton GFFFormat = new JRadioButton(); + + /* + * output format option shown for annotation export + */ + JRadioButton CSVFormat = new JRadioButton(); + + private JPanel linkedFeaturesPanel; + + /** + * Constructor + * + * @param panel + */ public AnnotationExporter(AlignmentPanel panel) { this.ap = panel; @@ -85,17 +108,25 @@ public class AnnotationExporter extends JPanel frame = new JInternalFrame(); frame.setContentPane(this); frame.setLayer(JLayeredPane.PALETTE_LAYER); - Desktop.addInternalFrame(frame, "", frame.getPreferredSize().width, - frame.getPreferredSize().height); + Dimension preferredSize = frame.getPreferredSize(); + Desktop.addInternalFrame(frame, "", true, preferredSize.width, + preferredSize.height, true, true); } /** - * Configures the diglog for options to export visible features + * Configures the dialog for options to export visible features. If from a split + * frame panel showing linked features, make the option to include these in the + * export visible. */ public void exportFeatures() { exportFeatures = true; CSVFormat.setVisible(false); + if (ap.av.isShowComplementFeatures()) + { + linkedFeaturesPanel.setVisible(true); + frame.pack(); + } frame.setTitle(MessageManager.getString("label.export_features")); } @@ -215,14 +246,17 @@ public class AnnotationExporter extends JPanel FeaturesFile formatter = new FeaturesFile(); final FeatureRenderer fr = ap.getFeatureRenderer(); + boolean includeComplement = includeLinkedFeatures.isSelected(); + if (GFFFormat.isSelected()) { - text = formatter.printGffFormat(sequences, fr, includeNonPositional); + text = formatter.printGffFormat(sequences, fr, includeNonPositional, + includeComplement); } else { text = formatter.printJalviewFormat(sequences, fr, - includeNonPositional); + includeNonPositional, includeComplement); } return text; } @@ -268,11 +302,70 @@ public class AnnotationExporter extends JPanel } } + /** + * Adds widgets to the panel + * + * @throws Exception + */ private void jbInit() throws Exception { - this.setLayout(new BorderLayout()); + this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + this.setBackground(Color.white); + + JPanel formatPanel = buildFormatOptionsPanel(); + JPanel linkedFeatures = buildLinkedFeaturesPanel(); + JPanel actionsPanel = buildActionsPanel(); + + this.add(formatPanel); + this.add(linkedFeatures); + this.add(actionsPanel); + } + + /** + * Builds a panel with a checkbox for the option to export linked (CDS/peptide) + * features. This is hidden by default, and only made visible if exporting + * features from a split frame panel which is configured to show linked + * features. + * + * @return + */ + private JPanel buildLinkedFeaturesPanel() + { + linkedFeaturesPanel = new JPanel(); + linkedFeaturesPanel.setOpaque(false); + + boolean nucleotide = ap.av.isNucleotide(); + String complement = nucleotide + ? MessageManager.getString("label.protein").toLowerCase() + : "CDS"; + JLabel label = new JLabel( + MessageManager.formatMessage("label.include_linked_features", + complement)); + label.setHorizontalAlignment(SwingConstants.TRAILING); + String tooltip = MessageManager + .formatMessage("label.include_linked_tooltip", complement); + label.setToolTipText( + JvSwingUtils.wrapTooltip(true, tooltip)); + + includeLinkedFeatures = new JCheckBox(); + linkedFeaturesPanel.add(label); + linkedFeaturesPanel.add(includeLinkedFeatures); + linkedFeaturesPanel.setVisible(false); + + return linkedFeaturesPanel; + } + + /** + * Builds the panel with to File or Textbox or Close actions + * + * @return + */ + JPanel buildActionsPanel() + { + JPanel actionsPanel = new JPanel(); + actionsPanel.setOpaque(false); - toFile.setText(MessageManager.getString("label.to_file")); + JButton toFile = new JButton(MessageManager.getString("label.to_file")); toFile.addActionListener(new ActionListener() { @Override @@ -281,7 +374,8 @@ public class AnnotationExporter extends JPanel toFile_actionPerformed(); } }); - toTextbox.setText(MessageManager.getString("label.to_textbox")); + JButton toTextbox = new JButton( + MessageManager.getString("label.to_textbox")); toTextbox.addActionListener(new ActionListener() { @Override @@ -290,7 +384,7 @@ public class AnnotationExporter extends JPanel toTextbox_actionPerformed(); } }); - close.setText(MessageManager.getString("action.close")); + JButton close = new JButton(MessageManager.getString("action.close")); close.addActionListener(new ActionListener() { @Override @@ -299,52 +393,49 @@ public class AnnotationExporter extends JPanel close_actionPerformed(); } }); + + actionsPanel.add(toFile); + actionsPanel.add(toTextbox); + actionsPanel.add(close); + + return actionsPanel; + } + + /** + * Builds the panel with options to output in Jalview, GFF or CSV format. GFF is + * only made visible when exporting features, CSV only when exporting + * annotation. + * + * @return + */ + JPanel buildFormatOptionsPanel() + { + JPanel formatPanel = new JPanel(); + // formatPanel.setBorder(BorderFactory.createEtchedBorder()); + formatPanel.setOpaque(false); + + JRadioButton jalviewFormat = new JRadioButton("Jalview"); jalviewFormat.setOpaque(false); jalviewFormat.setSelected(true); - jalviewFormat.setText("Jalview"); GFFFormat.setOpaque(false); GFFFormat.setText("GFF"); CSVFormat.setOpaque(false); CSVFormat.setText(MessageManager.getString("label.csv_spreadsheet")); - jLabel1.setHorizontalAlignment(SwingConstants.TRAILING); - jLabel1.setText(MessageManager.getString("action.format") + " "); - this.setBackground(Color.white); - jPanel3.setBorder(BorderFactory.createEtchedBorder()); - jPanel3.setOpaque(false); - jPanel1.setOpaque(false); - jPanel1.add(toFile); - jPanel1.add(toTextbox); - jPanel1.add(close); - jPanel3.add(jLabel1); - jPanel3.add(jalviewFormat); - jPanel3.add(GFFFormat); - jPanel3.add(CSVFormat); + + ButtonGroup buttonGroup = new ButtonGroup(); buttonGroup.add(jalviewFormat); buttonGroup.add(GFFFormat); buttonGroup.add(CSVFormat); - this.add(jPanel3, BorderLayout.CENTER); - this.add(jPanel1, BorderLayout.SOUTH); - } - - JPanel jPanel1 = new JPanel(); - - JButton toFile = new JButton(); - - JButton toTextbox = new JButton(); - - JButton close = new JButton(); - ButtonGroup buttonGroup = new ButtonGroup(); + JLabel format = new JLabel( + MessageManager.getString("action.format") + " "); + format.setHorizontalAlignment(SwingConstants.TRAILING); - JRadioButton jalviewFormat = new JRadioButton(); + formatPanel.add(format); + formatPanel.add(jalviewFormat); + formatPanel.add(GFFFormat); + formatPanel.add(CSVFormat); - JRadioButton GFFFormat = new JRadioButton(); - - JRadioButton CSVFormat = new JRadioButton(); - - JLabel jLabel1 = new JLabel(); - - JPanel jPanel3 = new JPanel(); - - FlowLayout flowLayout1 = new FlowLayout(); + return formatPanel; + } } diff --git a/src/jalview/gui/CrossRefAction.java b/src/jalview/gui/CrossRefAction.java index 9ad6596..92947e2 100644 --- a/src/jalview/gui/CrossRefAction.java +++ b/src/jalview/gui/CrossRefAction.java @@ -39,6 +39,7 @@ import jalview.util.DBRefUtils; import jalview.util.MapList; import jalview.util.MappingUtils; import jalview.util.MessageManager; +import jalview.viewmodel.seqfeatures.FeatureRendererModel; import jalview.ws.SequenceFetcher; import java.util.ArrayList; @@ -177,17 +178,17 @@ public class CrossRefAction implements Runnable .isShowSequenceFeatures(); newFrame.setShowSeqFeatures(showSequenceFeatures); copyThis.setShowSeqFeatures(showSequenceFeatures); - FeatureRenderer myFeatureStyling = alignFrame.alignPanel + FeatureRendererModel myFeatureStyling = alignFrame.alignPanel .getSeqPanel().seqCanvas.getFeatureRenderer(); /* * copy feature rendering settings to split frame */ - FeatureRenderer fr1 = newFrame.alignPanel.getSeqPanel().seqCanvas + FeatureRendererModel fr1 = newFrame.alignPanel.getSeqPanel().seqCanvas .getFeatureRenderer(); fr1.transferSettings(myFeatureStyling); fr1.findAllFeatures(true); - FeatureRenderer fr2 = copyThis.alignPanel.getSeqPanel().seqCanvas + FeatureRendererModel fr2 = copyThis.alignPanel.getSeqPanel().seqCanvas .getFeatureRenderer(); fr2.transferSettings(myFeatureStyling); fr2.findAllFeatures(true); diff --git a/src/jalview/gui/FeatureSettings.java b/src/jalview/gui/FeatureSettings.java index 9ca409b..441dca7 100644 --- a/src/jalview/gui/FeatureSettings.java +++ b/src/jalview/gui/FeatureSettings.java @@ -20,8 +20,10 @@ */ package jalview.gui; +import jalview.api.AlignViewportI; import jalview.api.FeatureColourI; import jalview.api.FeatureSettingsControllerI; +import jalview.api.ViewStyleI; import jalview.datamodel.AlignmentI; import jalview.datamodel.SequenceI; import jalview.datamodel.features.FeatureMatcher; @@ -35,6 +37,7 @@ import jalview.schemes.FeatureColour; import jalview.util.MessageManager; import jalview.util.Platform; import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean; +import jalview.viewmodel.styles.ViewStyle; import jalview.xml.binding.jalview.JalviewUserColours; import jalview.xml.binding.jalview.JalviewUserColours.Colour; import jalview.xml.binding.jalview.JalviewUserColours.Filter; @@ -44,6 +47,7 @@ import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; +import java.awt.FlowLayout; import java.awt.Font; import java.awt.Graphics; import java.awt.GridLayout; @@ -143,6 +147,8 @@ public class FeatureSettings extends JPanel private float originalTransparency; + private ViewStyleI originalViewStyle; + private Map originalFilters; final JInternalFrame frame; @@ -190,6 +196,7 @@ public class FeatureSettings extends JPanel transparency.setMaximum(100 - originalTransparencyAsPercent); originalFilters = new HashMap<>(fr.getFeatureFilters()); // shallow copy + originalViewStyle = new ViewStyle(af.viewport.getViewStyle()); try { @@ -290,6 +297,7 @@ public class FeatureSettings extends JPanel boolean extendSelection = evt.isShiftDown(); fr.ap.alignFrame.avc.markColumnsContainingFeatures( invertSelection, extendSelection, toggleSelection, type); + fr.ap.av.sendSelection(); } } @@ -398,8 +406,6 @@ public class FeatureSettings extends JPanel final Object typeCol, final Map minmax, int x, int y) { - final FeatureColourI featureColour = (FeatureColourI) typeCol; - JPopupMenu men = new JPopupMenu(MessageManager .formatMessage("label.settings_for_param", new String[] { type })); @@ -444,6 +450,7 @@ public class FeatureSettings extends JPanel { fr.ap.alignFrame.avc.markColumnsContainingFeatures(false, false, false, type); + fr.ap.av.sendSelection(); } }); JMenuItem clearCols = new JMenuItem(MessageManager @@ -455,6 +462,7 @@ public class FeatureSettings extends JPanel { fr.ap.alignFrame.avc.markColumnsContainingFeatures(true, false, false, type); + fr.ap.av.sendSelection(); } }); JMenuItem hideCols = new JMenuItem( @@ -465,6 +473,7 @@ public class FeatureSettings extends JPanel public void actionPerformed(ActionEvent arg0) { fr.ap.alignFrame.hideFeatureColumns(type, true); + fr.ap.av.sendSelection(); } }); JMenuItem hideOtherCols = new JMenuItem( @@ -475,6 +484,7 @@ public class FeatureSettings extends JPanel public void actionPerformed(ActionEvent arg0) { fr.ap.alignFrame.hideFeatureColumns(type, false); + fr.ap.av.sendSelection(); } }); men.add(selCols); @@ -538,7 +548,7 @@ public class FeatureSettings extends JPanel { fr.setGroupVisibility(check.getText(), check.isSelected()); resetTable(new String[] { grp }); - af.alignPanel.paintAlignment(true, true); + refreshDisplay(); } }); groupPanel.add(check); @@ -1096,7 +1106,7 @@ public class FeatureSettings extends JPanel if (fr.setFeaturePriority(rowData, visibleNew)) { - af.alignPanel.paintAlignment(true, true); + refreshDisplay(); } } @@ -1205,6 +1215,7 @@ public class FeatureSettings extends JPanel fr.setTransparency(originalTransparency); fr.setFeatureFilters(originalFilters); updateFeatureRenderer(originalData); + af.getViewport().setViewStyle(originalViewStyle); close(); } }); @@ -1255,7 +1266,7 @@ public class FeatureSettings extends JPanel if (!inConstruction) { fr.setTransparency((100 - transparency.getValue()) / 100f); - af.alignPanel.paintAlignment(true, true); + refreshDisplay(); } } }); @@ -1264,8 +1275,42 @@ public class FeatureSettings extends JPanel transparency.setToolTipText( MessageManager.getString("label.transparency_tip")); - JPanel transPanel = new JPanel(new GridLayout(1, 2)); - bigPanel.add(transPanel, BorderLayout.SOUTH); + boolean nucleotide = af.getViewport().getAlignment().isNucleotide(); + String text = MessageManager.formatMessage("label.show_linked_features", + nucleotide + ? MessageManager.getString("label.protein") + .toLowerCase() + : "CDS"); + JCheckBox showComplement = new JCheckBox(text); + showComplement.setSelected(af.getViewport().isShowComplementFeatures()); + showComplement.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + af.getViewport() + .setShowComplementFeatures(showComplement.isSelected()); + refreshDisplay(); + } + }); + + JCheckBox showComplementOnTop = new JCheckBox( + MessageManager.getString("label.on_top")); + showComplementOnTop + .setSelected(af.getViewport().isShowComplementFeaturesOnTop()); + showComplementOnTop.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + af.getViewport().setShowComplementFeaturesOnTop( + showComplementOnTop.isSelected()); + refreshDisplay(); + } + }); + + JPanel lowerPanel = new JPanel(new GridLayout(1, 2)); + bigPanel.add(lowerPanel, BorderLayout.SOUTH); JPanel transbuttons = new JPanel(new GridLayout(5, 1)); transbuttons.add(optimizeOrder); @@ -1273,8 +1318,21 @@ public class FeatureSettings extends JPanel transbuttons.add(sortByScore); transbuttons.add(sortByDens); transbuttons.add(help); - transPanel.add(transparency); - transPanel.add(transbuttons); + + boolean hasComplement = af.getViewport().getCodingComplement() != null; + JPanel transPanelLeft = new JPanel( + new GridLayout(hasComplement ? 3 : 2, 1)); + transPanelLeft.add(new JLabel(" Colour transparency" + ":")); + transPanelLeft.add(transparency); + if (hasComplement) + { + JPanel cp = new JPanel(new FlowLayout(FlowLayout.LEFT)); + cp.add(showComplement); + cp.add(showComplementOnTop); + transPanelLeft.add(cp); + } + lowerPanel.add(transPanelLeft); + lowerPanel.add(transbuttons); JPanel buttonPanel = new JPanel(); buttonPanel.add(ok); @@ -1288,11 +1346,27 @@ public class FeatureSettings extends JPanel } /** + * Repaints alignment, structure and overview (if shown). If there is a + * complementary view which is showing this view's features, then also + * repaints that. + */ + void refreshDisplay() + { + af.alignPanel.paintAlignment(true, true); + AlignViewportI complement = af.getViewport().getCodingComplement(); + if (complement != null && complement.isShowComplementFeatures()) + { + AlignFrame af2 = Desktop.getAlignFrameFor(complement); + af2.alignPanel.paintAlignment(true, true); + } + } + + /** * Answers a suitable tooltip to show on the colour cell of the table * * @param fcol * @param withHint - * if true include 'click to edit' and similar text + * if true include 'click to edit' and similar text * @return */ public static String getColorTooltip(FeatureColourI fcol, @@ -1429,13 +1503,22 @@ public class FeatureSettings extends JPanel } /** - * Answers the class of the object in column c of the first row of the table + * Answers the class of column c of the table */ @Override public Class getColumnClass(int c) { - Object v = getValueAt(0, c); - return v == null ? null : v.getClass(); + switch (c) + { + case TYPE_COLUMN: + return String.class; + case COLOUR_COLUMN: + return FeatureColour.class; + case FILTER_COLUMN: + return FeatureMatcherSet.class; + default: + return Boolean.class; + } } @Override diff --git a/src/jalview/gui/FeatureTypeSettings.java b/src/jalview/gui/FeatureTypeSettings.java index 7456e18..5b77dfc 100644 --- a/src/jalview/gui/FeatureTypeSettings.java +++ b/src/jalview/gui/FeatureTypeSettings.java @@ -20,6 +20,7 @@ */ package jalview.gui; +import jalview.api.AlignViewportI; import jalview.api.AlignmentViewPanel; import jalview.api.FeatureColourI; import jalview.bin.Cache; @@ -674,7 +675,7 @@ public class FeatureTypeSettings extends JalviewDialog */ if (ap != null) { - ap.paintAlignment(true, true); + refreshDisplay(true); } } }); @@ -875,7 +876,7 @@ public class FeatureTypeSettings extends JalviewDialog * save the colour, and repaint stuff */ fr.setColour(featureType, acg); - ap.paintAlignment(updateStructsAndOverview, updateStructsAndOverview); + refreshDisplay(updateStructsAndOverview); updateColoursTab(); } @@ -1014,7 +1015,7 @@ public class FeatureTypeSettings extends JalviewDialog { fr.setColour(featureType, originalColour); fr.setFeatureFilter(featureType, originalFilter); - ap.paintAlignment(true, true); + refreshDisplay(true); } /** @@ -1753,8 +1754,26 @@ public class FeatureTypeSettings extends JalviewDialog * (note this might now be an empty filter with no conditions) */ fr.setFeatureFilter(featureType, combined.isEmpty() ? null : combined); - ap.paintAlignment(true, true); + refreshDisplay(true); updateFiltersTab(); } + + /** + * Repaints alignment, structure and overview (if shown). If there is a + * complementary view which is showing this view's features, then also + * repaints that. + * + * @param updateStructsAndOverview + */ + void refreshDisplay(boolean updateStructsAndOverview) + { + ap.paintAlignment(true, updateStructsAndOverview); + AlignViewportI complement = ap.getAlignViewport().getCodingComplement(); + if (complement != null && complement.isShowComplementFeatures()) + { + AlignFrame af2 = Desktop.getAlignFrameFor(complement); + af2.alignPanel.paintAlignment(true, updateStructsAndOverview); + } + } } diff --git a/src/jalview/gui/IdPanel.java b/src/jalview/gui/IdPanel.java index d11d7a1..2b1507a 100755 --- a/src/jalview/gui/IdPanel.java +++ b/src/jalview/gui/IdPanel.java @@ -22,7 +22,6 @@ package jalview.gui; import jalview.datamodel.AlignmentAnnotation; import jalview.datamodel.Sequence; -import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceGroup; import jalview.datamodel.SequenceI; import jalview.gui.SeqPanel.MousePos; @@ -369,28 +368,12 @@ public class IdPanel extends JPanel } Sequence sq = (Sequence) av.getAlignment().getSequenceAt(pos.seqIndex); - - /* - * build a new links menu based on the current links - * and any non-positional features - */ - List features = null; if (sq != null) { - List nlinks = Preferences.sequenceUrlLinks.getLinksForMenu(); - features = sq.getFeatures().getNonPositionalFeatures(); - for (SequenceFeature sf : features) - { - if (sf.links != null) - { - nlinks.addAll(sf.links); - } - } + PopupMenu pop = new PopupMenu(alignPanel, sq, + Preferences.getGroupURLLinks()); + pop.show(this, e.getX(), e.getY()); } - - PopupMenu pop = new PopupMenu(alignPanel, sq, features, - Preferences.getGroupURLLinks()); - pop.show(this, e.getX(), e.getY()); } /** diff --git a/src/jalview/gui/JalviewChimeraBindingModel.java b/src/jalview/gui/JalviewChimeraBindingModel.java index 2f11c30..9d63c6a 100644 --- a/src/jalview/gui/JalviewChimeraBindingModel.java +++ b/src/jalview/gui/JalviewChimeraBindingModel.java @@ -27,6 +27,7 @@ import jalview.datamodel.SequenceI; import jalview.ext.rbvi.chimera.JalviewChimeraBinding; import jalview.io.DataSourceType; import jalview.structure.StructureSelectionManager; +import jalview.viewmodel.seqfeatures.FeatureRendererModel; import javax.swing.SwingUtilities; @@ -43,7 +44,7 @@ public class JalviewChimeraBindingModel extends JalviewChimeraBinding } @Override - public FeatureRenderer getFeatureRenderer(AlignmentViewPanel alignment) + public FeatureRendererModel getFeatureRenderer(AlignmentViewPanel alignment) { AlignmentPanel ap = (alignment == null) ? cvf.getAlignmentPanel() : (AlignmentPanel) alignment; diff --git a/src/jalview/gui/OverviewCanvas.java b/src/jalview/gui/OverviewCanvas.java index 89088b8d..0f49381 100644 --- a/src/jalview/gui/OverviewCanvas.java +++ b/src/jalview/gui/OverviewCanvas.java @@ -25,6 +25,7 @@ import jalview.bin.Cache; import jalview.renderer.OverviewRenderer; import jalview.renderer.OverviewResColourFinder; import jalview.viewmodel.OverviewDimensions; +import jalview.viewmodel.seqfeatures.FeatureRendererModel; import java.awt.Color; import java.awt.Dimension; @@ -132,7 +133,7 @@ public class OverviewCanvas extends JComponent * the renderer to transfer feature colouring from */ public void draw(boolean showSequenceFeatures, boolean showAnnotation, - FeatureRenderer transferRenderer) + FeatureRendererModel transferRenderer) { miniMe = null; diff --git a/src/jalview/gui/PopupMenu.java b/src/jalview/gui/PopupMenu.java index 702773b..415054e 100644 --- a/src/jalview/gui/PopupMenu.java +++ b/src/jalview/gui/PopupMenu.java @@ -24,15 +24,16 @@ import jalview.analysis.AAFrequency; import jalview.analysis.AlignmentAnnotationUtils; import jalview.analysis.AlignmentUtils; import jalview.analysis.Conservation; +import jalview.api.AlignViewportI; import jalview.bin.Cache; import jalview.commands.ChangeCaseCommand; import jalview.commands.EditCommand; import jalview.commands.EditCommand.Action; import jalview.datamodel.AlignmentAnnotation; import jalview.datamodel.AlignmentI; -import jalview.datamodel.Annotation; import jalview.datamodel.DBRefEntry; import jalview.datamodel.HiddenColumns; +import jalview.datamodel.MappedFeatures; import jalview.datamodel.PDBEntry; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceGroup; @@ -47,11 +48,13 @@ import jalview.schemes.ColourSchemeI; import jalview.schemes.ColourSchemes; import jalview.schemes.PIDColourScheme; import jalview.schemes.ResidueColourScheme; +import jalview.util.Comparison; import jalview.util.GroupUrlLink; import jalview.util.GroupUrlLink.UrlStringTooLongException; import jalview.util.MessageManager; import jalview.util.StringUtils; import jalview.util.UrlLink; +import jalview.viewmodel.seqfeatures.FeatureRendererModel; import java.awt.Color; import java.awt.event.ActionEvent; @@ -65,6 +68,7 @@ import java.util.Hashtable; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.SortedMap; import java.util.TreeMap; import java.util.Vector; @@ -83,6 +87,19 @@ import javax.swing.JRadioButtonMenuItem; */ public class PopupMenu extends JPopupMenu implements ColourChangeListener { + /* + * true for ID Panel menu, false for alignment panel menu + */ + private final boolean forIdPanel; + + private final AlignmentPanel ap; + + /* + * the sequence under the cursor when clicked + * (additional sequences may be selected) + */ + private final SequenceI sequence; + JMenu groupMenu = new JMenu(); JMenuItem groupName = new JMenuItem(); @@ -97,28 +114,14 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener protected JMenuItem modifyConservation = new JMenuItem(); - AlignmentPanel ap; - JMenu sequenceMenu = new JMenu(); - JMenuItem sequenceName = new JMenuItem(); - - JMenuItem sequenceDetails = new JMenuItem(); - - JMenuItem sequenceSelDetails = new JMenuItem(); - JMenuItem makeReferenceSeq = new JMenuItem(); - JMenuItem chooseAnnotations = new JMenuItem(); - - SequenceI sequence; - JMenuItem createGroupMenuItem = new JMenuItem(); JMenuItem unGroupMenuItem = new JMenuItem(); - JMenuItem outline = new JMenuItem(); - JMenu colourMenu = new JMenu(); JCheckBoxMenuItem showBoxes = new JCheckBoxMenuItem(); @@ -131,18 +134,12 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener JMenu editMenu = new JMenu(); - JMenuItem cut = new JMenuItem(); - - JMenuItem copy = new JMenuItem(); - JMenuItem upperCase = new JMenuItem(); JMenuItem lowerCase = new JMenuItem(); JMenuItem toggle = new JMenuItem(); - JMenu pdbMenu = new JMenu(); - JMenu outputMenu = new JMenu(); JMenu seqShowAnnotationsMenu = new JMenu(); @@ -159,22 +156,14 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener JMenuItem groupAddReferenceAnnotations = new JMenuItem( MessageManager.getString("label.add_reference_annotations")); - JMenuItem sequenceFeature = new JMenuItem(); - JMenuItem textColour = new JMenuItem(); - JMenu jMenu1 = new JMenu(); + JMenu editGroupMenu = new JMenu(); - JMenuItem pdbStructureDialog = new JMenuItem(); + JMenuItem chooseStructure = new JMenuItem(); JMenu rnaStructureMenu = new JMenu(); - JMenuItem editSequence = new JMenuItem(); - - JMenu groupLinksMenu; - - JMenuItem hideInsertions = new JMenuItem(); - /** * Constructs a menu with sub-menu items for any hyperlinks for the sequence * and/or features provided. Hyperlinks may include a lookup by sequence id, @@ -185,7 +174,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener * @param features * @return */ - static JMenu buildLinkMenu(final SequenceI seq, + protected static JMenu buildLinkMenu(final SequenceI seq, List features) { JMenu linkMenu = new JMenu(MessageManager.getString("action.link")); @@ -357,41 +346,55 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener } /** - * Creates a new PopupMenu object. + * Constructor for a PopupMenu for a click in the alignment panel (on a residue) * * @param ap + * the panel in which the mouse is clicked * @param seq - * @param features - * non-positional features (for seq not null), or positional features - * at residue (for seq equal to null) + * the sequence under the mouse + * @throws NullPointerException + * if seq is null */ - public PopupMenu(final AlignmentPanel ap, SequenceI seq, - List features) + public PopupMenu(final AlignmentPanel ap, SequenceI seq, int column) { - this(ap, seq, features, null); + this(false, ap, seq, column, null); } /** - * Constructor + * Constructor for a PopupMenu for a click in the sequence id panel * * @param alignPanel + * the panel in which the mouse is clicked * @param seq - * the sequence under the cursor if in the Id panel, null if in the - * sequence panel - * @param features - * non-positional features if in the Id panel, features at the - * clicked residue if in the sequence panel + * the sequence under the mouse click * @param groupLinks + * templates for sequence external links + * @throws NullPointerException + * if seq is null */ public PopupMenu(final AlignmentPanel alignPanel, final SequenceI seq, - List features, List groupLinks) + List groupLinks) + { + this(true, alignPanel, seq, -1, groupLinks); + } + + /** + * Private constructor that constructs a popup menu for either sequence ID + * Panel, or alignment context + * + * @param fromIdPanel + * @param alignPanel + * @param seq + * @param column + * aligned column position (0...) + * @param groupLinks + */ + private PopupMenu(boolean fromIdPanel, + final AlignmentPanel alignPanel, + final SequenceI seq, final int column, List groupLinks) { - // ///////////////////////////////////////////////////////// - // If this is activated from the sequence panel, the user may want to - // edit or annotate a particular residue. Therefore display the residue menu - // - // If from the IDPanel, we must display the sequence menu - // //////////////////////////////////////////////////////// + Objects.requireNonNull(seq); + this.forIdPanel = fromIdPanel; this.ap = alignPanel; sequence = seq; @@ -416,9 +419,9 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener * 'reference annotations' that may be added to the alignment. First for the * currently selected sequence (if there is one): */ - final List selectedSequence = (seq == null - ? Collections. emptyList() - : Arrays.asList(seq)); + final List selectedSequence = (forIdPanel && seq != null + ? Arrays.asList(seq) + : Collections. emptyList()); buildAnnotationTypesMenus(seqShowAnnotationsMenu, seqHideAnnotationsMenu, selectedSequence); configureReferenceAnnotationsMenu(seqAddReferenceAnnotations, @@ -443,9 +446,9 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener e.printStackTrace(); } - JMenuItem menuItem; - if (seq != null) + if (forIdPanel) { + JMenuItem menuItem; sequenceMenu.setText(sequence.getName()); if (seq == alignPanel.av.getAlignment().getSeqrep()) { @@ -600,7 +603,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener } if (addOption) { - menuItem = new JMenuItem( + JMenuItem menuItem = new JMenuItem( MessageManager.getString("action.reveal_all")); menuItem.addActionListener(new ActionListener() { @@ -692,27 +695,57 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener { createGroupMenuItem.setVisible(true); unGroupMenuItem.setVisible(false); - jMenu1.setText(MessageManager.getString("action.edit_new_group")); + editGroupMenu.setText(MessageManager.getString("action.edit_new_group")); } else { createGroupMenuItem.setVisible(false); unGroupMenuItem.setVisible(true); - jMenu1.setText(MessageManager.getString("action.edit_group")); + editGroupMenu.setText(MessageManager.getString("action.edit_group")); } - if (seq == null) + if (!forIdPanel) { sequenceMenu.setVisible(false); - pdbStructureDialog.setVisible(false); + chooseStructure.setVisible(false); rnaStructureMenu.setVisible(false); } + addLinksAndFeatures(seq, column); + } + + /** + * Adds + *
    + *
  • configured sequence database links (ID panel popup menu)
  • + *
  • non-positional feature links (ID panel popup menu)
  • + *
  • positional feature links (alignment panel popup menu)
  • + *
  • feature details links (alignment panel popup menu)
  • + *
+ * If this panel is also showed complementary (CDS/protein) features, then links + * to their feature details are also added. + * + * @param seq + * @param column + */ + void addLinksAndFeatures(final SequenceI seq, final int column) + { + List features = null; + if (forIdPanel) + { + features = sequence.getFeatures().getNonPositionalFeatures(); + } + else + { + features = ap.getFeatureRenderer().findFeaturesAtColumn(sequence, + column + 1); + } + addLinks(seq, features); - if (seq == null) + if (!forIdPanel) { - addFeatureDetails(features); + addFeatureDetails(features, seq, column); } } @@ -720,75 +753,127 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener * Add a link to show feature details for each sequence feature * * @param features + * @param column + * @param seq */ - protected void addFeatureDetails(List features) + protected void addFeatureDetails(List features, + SequenceI seq, int column) { - if (features == null || features.isEmpty()) + /* + * add features in CDS/protein complement at the corresponding + * position if configured to do so + */ + MappedFeatures mf = null; + if (ap.av.isShowComplementFeatures()) + { + if (!Comparison.isGap(sequence.getCharAt(column))) + { + AlignViewportI complement = ap.getAlignViewport() + .getCodingComplement(); + AlignFrame af = Desktop.getAlignFrameFor(complement); + FeatureRendererModel fr2 = af.getFeatureRenderer(); + int seqPos = sequence.findPosition(column); + mf = fr2.findComplementFeaturesAtResidue(sequence, seqPos); + } + } + + if (features.isEmpty() && mf == null) { + /* + * no features to show at this position + */ return; } + JMenu details = new JMenu( MessageManager.getString("label.feature_details")); add(details); + String name = seq.getName(); for (final SequenceFeature sf : features) { - int start = sf.getBegin(); - int end = sf.getEnd(); - String desc = null; - if (start == end) + addFeatureDetailsMenuItem(details, name, sf); + } + + if (mf != null) + { + name = mf.fromSeq == seq ? mf.mapping.getTo().getName() + : mf.fromSeq.getName(); + for (final SequenceFeature sf : mf.features) { - desc = String.format("%s %d", sf.getType(), start); + addFeatureDetailsMenuItem(details, name, sf); } - else + } + } + + /** + * A helper method to add one menu item whose action is to show details for one + * feature + * + * @param details + * @param seqName + * @param sf + */ + void addFeatureDetailsMenuItem(JMenu details, final String seqName, + final SequenceFeature sf) + { + int start = sf.getBegin(); + int end = sf.getEnd(); + String desc = null; + if (start == end) + { + desc = String.format("%s %d", sf.getType(), start); + } + else + { + desc = String.format("%s %d-%d", sf.getType(), start, end); + } + String tooltip = desc; + String description = sf.getDescription(); + if (description != null) + { + description = StringUtils.stripHtmlTags(description); + if (description.length() > 12) { - desc = String.format("%s %d-%d", sf.getType(), start, end); + desc = desc + " " + description.substring(0, 12) + ".."; } - String tooltip = desc; - String description = sf.getDescription(); - if (description != null) + else { - description = StringUtils.stripHtmlTags(description); - if (description.length() > 12) - { - desc = desc + " " + description.substring(0, 12) + ".."; - } - else - { - desc = desc + " " + description; - } - tooltip = tooltip + " " + description; + desc = desc + " " + description; } - if (sf.getFeatureGroup() != null) + tooltip = tooltip + " " + description; + } + if (sf.getFeatureGroup() != null) + { + tooltip = tooltip + (" (" + sf.getFeatureGroup() + ")"); + } + JMenuItem item = new JMenuItem(desc); + item.setToolTipText(tooltip); + item.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) { - tooltip = tooltip + (" (" + sf.getFeatureGroup() + ")"); + showFeatureDetails(seqName, sf); } - JMenuItem item = new JMenuItem(desc); - item.setToolTipText(tooltip); - item.addActionListener(new ActionListener() - { - @Override - public void actionPerformed(ActionEvent e) - { - showFeatureDetails(sf); - } - }); - details.add(item); - } + }); + details.add(item); } /** * Opens a panel showing a text report of feature dteails * + * @param seqName + * * @param sf */ - protected void showFeatureDetails(SequenceFeature sf) + protected void showFeatureDetails(String seqName, SequenceFeature sf) { CutAndPasteHtmlTransfer cap = new CutAndPasteHtmlTransfer(); - // it appears Java's CSS does not support border-collaps :-( + // it appears Java's CSS does not support border-collapse :-( cap.addStylesheetRule("table { border-collapse: collapse;}"); cap.addStylesheetRule("table, td, th {border: 1px solid black;}"); - cap.setText(sf.getDetailsReport()); + cap.setText(sf.getDetailsReport(seqName)); Desktop.addInternalFrame(cap, MessageManager.getString("label.feature_details"), 500, 500); @@ -806,12 +891,12 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener */ void addLinks(final SequenceI seq, List features) { - JMenu linkMenu = buildLinkMenu(seq, features); + JMenu linkMenu = buildLinkMenu(forIdPanel ? seq : null, features); // only add link menu if it has entries if (linkMenu.getItemCount() > 0) { - if (sequence != null) + if (forIdPanel) { sequenceMenu.add(linkMenu); } @@ -959,7 +1044,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener // menu appears asap // sequence only URLs // ID/regex match URLs - groupLinksMenu = new JMenu( + JMenu groupLinksMenu = new JMenu( MessageManager.getString("action.group_link")); // three types of url that might be created. JMenu[] linkMenus = new JMenu[] { null, @@ -1118,7 +1203,8 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener } }); sequenceMenu.setText(MessageManager.getString("label.sequence")); - sequenceName.setText( + + JMenuItem sequenceName = new JMenuItem( MessageManager.getString("label.edit_name_description")); sequenceName.addActionListener(new ActionListener() { @@ -1128,8 +1214,8 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener sequenceName_actionPerformed(); } }); - chooseAnnotations - .setText(MessageManager.getString("action.choose_annotations")); + JMenuItem chooseAnnotations = new JMenuItem( + MessageManager.getString("action.choose_annotations")); chooseAnnotations.addActionListener(new ActionListener() { @Override @@ -1138,24 +1224,24 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener chooseAnnotations_actionPerformed(e); } }); - sequenceDetails - .setText(MessageManager.getString("label.sequence_details")); + JMenuItem sequenceDetails = new JMenuItem( + MessageManager.getString("label.sequence_details")); sequenceDetails.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - sequenceDetails_actionPerformed(); + createSequenceDetailsReport(new SequenceI[] { sequence }); } }); - sequenceSelDetails - .setText(MessageManager.getString("label.sequence_details")); + JMenuItem sequenceSelDetails = new JMenuItem( + MessageManager.getString("label.sequence_details")); sequenceSelDetails.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - sequenceSelectionDetails_actionPerformed(); + createSequenceDetailsReport(ap.av.getSequenceSelection()); } }); @@ -1180,7 +1266,8 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener } }); - outline.setText(MessageManager.getString("action.border_colour")); + JMenuItem outline = new JMenuItem( + MessageManager.getString("action.border_colour")); outline.addActionListener(new ActionListener() { @Override @@ -1230,7 +1317,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener } }); editMenu.setText(MessageManager.getString("action.edit")); - cut.setText(MessageManager.getString("action.cut")); + JMenuItem cut = new JMenuItem(MessageManager.getString("action.cut")); cut.addActionListener(new ActionListener() { @Override @@ -1248,7 +1335,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener changeCase(e); } }); - copy.setText(MessageManager.getString("action.copy")); + JMenuItem copy = new JMenuItem(MessageManager.getString("action.copy")); copy.addActionListener(new ActionListener() { @Override @@ -1285,7 +1372,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener .setText(MessageManager.getString("label.show_annotations")); groupHideAnnotationsMenu .setText(MessageManager.getString("label.hide_annotations")); - sequenceFeature.setText( + JMenuItem sequenceFeature = new JMenuItem( MessageManager.getString("label.create_sequence_feature")); sequenceFeature.addActionListener(new ActionListener() { @@ -1295,10 +1382,10 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener sequenceFeature_actionPerformed(); } }); - jMenu1.setText(MessageManager.getString("label.group")); - pdbStructureDialog.setText( + editGroupMenu.setText(MessageManager.getString("label.group")); + chooseStructure.setText( MessageManager.getString("label.show_pdbstruct_dialog")); - pdbStructureDialog.addActionListener(new ActionListener() + chooseStructure.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent actionEvent) @@ -1316,7 +1403,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener .setText(MessageManager.getString("label.view_rna_structure")); // colStructureMenu.setText("Colour By Structure"); - editSequence.setText( + JMenuItem editSequence = new JMenuItem( MessageManager.getString("label.edit_sequence") + "..."); editSequence.addActionListener(new ActionListener() { @@ -1338,25 +1425,25 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener } }); - hideInsertions - .setText(MessageManager.getString("label.hide_insertions")); - hideInsertions.addActionListener(new ActionListener() - { - - @Override - public void actionPerformed(ActionEvent e) - { - hideInsertions_actionPerformed(e); - } - }); groupMenu.add(sequenceSelDetails); add(groupMenu); add(sequenceMenu); add(rnaStructureMenu); - add(pdbStructureDialog); - if (sequence != null) + add(chooseStructure); + if (forIdPanel) { + JMenuItem hideInsertions = new JMenuItem( + MessageManager.getString("label.hide_insertions")); + hideInsertions.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + hideInsertions_actionPerformed(e); + } + }); add(hideInsertions); } // annotations configuration panel suppressed for now @@ -1377,7 +1464,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener groupMenu.add(sequenceFeature); groupMenu.add(createGroupMenuItem); groupMenu.add(unGroupMenuItem); - groupMenu.add(jMenu1); + groupMenu.add(editGroupMenu); sequenceMenu.add(sequenceName); sequenceMenu.add(sequenceDetails); sequenceMenu.add(makeReferenceSeq); @@ -1391,17 +1478,13 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener editMenu.add(upperCase); editMenu.add(lowerCase); editMenu.add(toggle); - // JBPNote: These shouldn't be added here - should appear in a generic - // 'apply web service to this sequence menu' - // pdbMenu.add(RNAFold); - // pdbMenu.add(ContraFold); - jMenu1.add(groupName); - jMenu1.add(colourMenu); - jMenu1.add(showBoxes); - jMenu1.add(showText); - jMenu1.add(showColourText); - jMenu1.add(outline); - jMenu1.add(displayNonconserved); + editGroupMenu.add(groupName); + editGroupMenu.add(colourMenu); + editGroupMenu.add(showBoxes); + editGroupMenu.add(showText); + editGroupMenu.add(showColourText); + editGroupMenu.add(outline); + editGroupMenu.add(displayNonconserved); } /** @@ -1664,11 +1747,6 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener createSequenceDetailsReport(ap.av.getSequenceSelection()); } - protected void sequenceDetails_actionPerformed() - { - createSequenceDetailsReport(new SequenceI[] { sequence }); - } - public void createSequenceDetailsReport(SequenceI[] sequences) { CutAndPasteHtmlTransfer cap = new CutAndPasteHtmlTransfer(); @@ -1856,10 +1934,9 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener } /** - * DOCUMENT ME! - * - * @param e - * DOCUMENT ME! + * Shows a dialog where the sequence name and description may be edited. If a + * name containing spaces is entered, these are converted to underscores, with a + * warning message. */ void sequenceName_actionPerformed() { @@ -1877,9 +1954,10 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener return; } - if (dialog.getName() != null) + String name = dialog.getName(); + if (name != null) { - if (dialog.getName().indexOf(" ") > -1) + if (name.indexOf(" ") > -1) { JvOptionPane.showMessageDialog(ap, MessageManager @@ -1887,9 +1965,10 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener MessageManager .getString("label.no_spaces_allowed_sequence_name"), JvOptionPane.WARNING_MESSAGE); + name = name.replace(' ', '_'); } - sequence.setName(dialog.getName().replace(' ', '_')); + sequence.setName(name); ap.paintAlignment(false, false); } @@ -2105,38 +2184,20 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener } } - public void colourByStructure(String pdbid) - { - Annotation[] anots = ap.av.getStructureSelectionManager() - .colourSequenceFromStructure(sequence, pdbid); - - AlignmentAnnotation an = new AlignmentAnnotation("Structure", - "Coloured by " + pdbid, anots); - - ap.av.getAlignment().addAnnotation(an); - an.createSequenceMapping(sequence, 0, true); - // an.adjustForAlignment(); - ap.av.getAlignment().setAnnotationIndex(an, 0); - - ap.adjustAnnotationHeight(); - - sequence.addAlignmentAnnotation(an); - - } - public void editSequence_actionPerformed(ActionEvent actionEvent) { SequenceGroup sg = ap.av.getSelectionGroup(); + SequenceI seq = sequence; if (sg != null) { - if (sequence == null) + if (seq == null) { - sequence = sg.getSequenceAt(0); + seq = sg.getSequenceAt(0); } EditNameDialog dialog = new EditNameDialog( - sequence.getSequenceAsString(sg.getStartRes(), + seq.getSequenceAsString(sg.getStartRes(), sg.getEndRes() + 1), null, MessageManager.getString("label.edit_sequence"), null, MessageManager.getString("label.edit_sequence"), diff --git a/src/jalview/gui/SeqPanel.java b/src/jalview/gui/SeqPanel.java index 1176df5..75bf0cc 100644 --- a/src/jalview/gui/SeqPanel.java +++ b/src/jalview/gui/SeqPanel.java @@ -29,6 +29,7 @@ import jalview.datamodel.AlignmentAnnotation; import jalview.datamodel.AlignmentI; import jalview.datamodel.ColumnSelection; import jalview.datamodel.HiddenColumns; +import jalview.datamodel.MappedFeatures; import jalview.datamodel.SearchResultMatchI; import jalview.datamodel.SearchResults; import jalview.datamodel.SearchResultsI; @@ -50,6 +51,7 @@ import jalview.util.MessageManager; import jalview.util.Platform; import jalview.viewmodel.AlignmentViewport; import jalview.viewmodel.ViewportRanges; +import jalview.viewmodel.seqfeatures.FeatureRendererModel; import java.awt.BorderLayout; import java.awt.Color; @@ -61,6 +63,7 @@ import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -832,11 +835,11 @@ public class SeqPanel extends JPanel * the start of the highlighted region. */ @Override - public void highlightSequence(SearchResultsI results) + public String highlightSequence(SearchResultsI results) { if (results == null || results.equals(lastSearchResults)) { - return; + return null; } lastSearchResults = results; @@ -862,6 +865,77 @@ public class SeqPanel extends JPanel { setStatusMessage(results); } + return results.isEmpty() ? null : getHighlightInfo(results); + } + + /** + * temporary hack: answers a message suitable to show on structure hover + * label. This is normally null. It is a peptide variation description if + *
    + *
  • results are a single residue in a protein alignment
  • + *
  • there is a mapping to a coding sequence (codon)
  • + *
  • there are one or more SNP variant features on the codon
  • + *
+ * in which case the answer is of the format (e.g.) "p.Glu388Asp" + * + * @param results + * @return + */ + private String getHighlightInfo(SearchResultsI results) + { + /* + * ideally, just find mapped CDS (as we don't care about render style here); + * for now, go via split frame complement's FeatureRenderer + */ + AlignViewportI complement = ap.getAlignViewport().getCodingComplement(); + if (complement == null) + { + return null; + } + AlignFrame af = Desktop.getAlignFrameFor(complement); + FeatureRendererModel fr2 = af.getFeatureRenderer(); + + int j = results.getSize(); + List infos = new ArrayList<>(); + for (int i = 0; i < j; i++) + { + SearchResultMatchI match = results.getResults().get(i); + int pos = match.getStart(); + if (pos == match.getEnd()) + { + SequenceI seq = match.getSequence(); + SequenceI ds = seq.getDatasetSequence() == null ? seq + : seq.getDatasetSequence(); + MappedFeatures mf = fr2 + .findComplementFeaturesAtResidue(ds, pos); + if (mf != null) + { + for (SequenceFeature sf : mf.features) + { + String pv = mf.findProteinVariants(sf); + if (pv.length() > 0 && !infos.contains(pv)) + { + infos.add(pv); + } + } + } + } + } + + if (infos.isEmpty()) + { + return null; + } + StringBuilder sb = new StringBuilder(); + for (String info : infos) + { + if (sb.length() > 0) + { + sb.append("|"); + } + sb.append(info); + } + return sb.toString(); } @Override @@ -974,6 +1048,27 @@ public class SeqPanel extends JPanel .findFeaturesAtColumn(sequence, column + 1); seqARep.appendFeatures(tooltipText, pos, features, this.ap.getSeqPanel().seqCanvas.fr); + + /* + * add features in CDS/protein complement at the corresponding + * position if configured to do so + */ + if (av.isShowComplementFeatures()) + { + if (!Comparison.isGap(sequence.getCharAt(column))) + { + AlignViewportI complement = ap.getAlignViewport() + .getCodingComplement(); + AlignFrame af = Desktop.getAlignFrameFor(complement); + FeatureRendererModel fr2 = af.getFeatureRenderer(); + MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence, + pos); + if (mf != null) + { + seqARep.appendFeatures(tooltipText, pos, mf, fr2); + } + } + } } if (tooltipText.length() == 6) // { @@ -2159,11 +2254,11 @@ public class SeqPanel extends JPanel final int column = pos.column; final int seq = pos.seqIndex; SequenceI sequence = av.getAlignment().getSequenceAt(seq); - List features = ap.getFeatureRenderer() - .findFeaturesAtColumn(sequence, column + 1); - - PopupMenu pop = new PopupMenu(ap, null, features); - pop.show(this, evt.getX(), evt.getY()); + if (sequence != null) + { + PopupMenu pop = new PopupMenu(ap, sequence, column); + pop.show(this, evt.getX(), evt.getY()); + } } /** diff --git a/src/jalview/io/FeaturesFile.java b/src/jalview/io/FeaturesFile.java index 70f2ac7..a69788b 100755 --- a/src/jalview/io/FeaturesFile.java +++ b/src/jalview/io/FeaturesFile.java @@ -29,11 +29,13 @@ import jalview.api.FeaturesSourceI; import jalview.datamodel.AlignedCodonFrame; import jalview.datamodel.Alignment; import jalview.datamodel.AlignmentI; +import jalview.datamodel.MappedFeatures; import jalview.datamodel.SequenceDummy; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; import jalview.datamodel.features.FeatureMatcherSet; import jalview.datamodel.features.FeatureMatcherSetI; +import jalview.gui.Desktop; import jalview.io.gff.GffHelperBase; import jalview.io.gff.GffHelperFactory; import jalview.io.gff.GffHelperI; @@ -49,9 +51,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.TreeMap; /** * Parses and writes features files, which may be in Jalview, GFF2 or GFF3 @@ -563,32 +567,31 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI } /** - * Returns contents of a Jalview format features file, for visible features, - * as filtered by type and group. Features with a null group are displayed if - * their feature type is visible. Non-positional features may optionally be - * included (with no check on type or group). + * Returns contents of a Jalview format features file, for visible features, as + * filtered by type and group. Features with a null group are displayed if their + * feature type is visible. Non-positional features may optionally be included + * (with no check on type or group). * * @param sequences * @param fr * @param includeNonPositional - * if true, include non-positional features (regardless of group or - * type) + * if true, include non-positional features + * (regardless of group or type) + * @param includeComplement + * if true, include visible complementary + * (CDS/protein) positional features, with + * locations converted to local sequence + * coordinates * @return */ public String printJalviewFormat(SequenceI[] sequences, - FeatureRenderer fr, boolean includeNonPositional) + FeatureRenderer fr, boolean includeNonPositional, + boolean includeComplement) { Map visibleColours = fr .getDisplayedFeatureCols(); Map featureFilters = fr.getFeatureFilters(); - if (!includeNonPositional - && (visibleColours == null || visibleColours.isEmpty())) - { - // no point continuing. - return "No Features Visible"; - } - /* * write out feature colours (if we know them) */ @@ -620,10 +623,151 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI int count = outputFeaturesByGroup(out, fr, types, sequences, includeNonPositional); + if (includeComplement) + { + count += outputComplementFeatures(out, fr, sequences); + } + return count > 0 ? out.toString() : "No Features Visible"; } /** + * Outputs any visible complementary (CDS/peptide) positional features as + * Jalview format, within feature group. The coordinates of the linked features + * are converted to the corresponding positions of the local sequences. + * + * @param out + * @param fr + * @param sequences + * @return + */ + private int outputComplementFeatures(StringBuilder out, + FeatureRenderer fr, SequenceI[] sequences) + { + AlignViewportI comp = fr.getViewport().getCodingComplement(); + FeatureRenderer fr2 = Desktop.getAlignFrameFor(comp) + .getFeatureRenderer(); + + /* + * bin features by feature group and sequence + */ + Map>> map = new TreeMap<>( + String.CASE_INSENSITIVE_ORDER); + int count = 0; + + for (SequenceI seq : sequences) + { + /* + * find complementary features + */ + List complementary = findComplementaryFeatures(seq, + fr2); + String seqName = seq.getName(); + + for (SequenceFeature sf : complementary) + { + String group = sf.getFeatureGroup(); + if (!map.containsKey(group)) + { + map.put(group, new LinkedHashMap<>()); // preserves sequence order + } + Map> groupFeatures = map.get(group); + if (!groupFeatures.containsKey(seqName)) + { + groupFeatures.put(seqName, new ArrayList<>()); + } + List foundFeatures = groupFeatures.get(seqName); + foundFeatures.add(sf); + count++; + } + } + + /* + * output features by group + */ + for (Entry>> groupFeatures : map.entrySet()) + { + out.append(newline); + String group = groupFeatures.getKey(); + if (!"".equals(group)) + { + out.append(STARTGROUP).append(TAB).append(group).append(newline); + } + Map> seqFeaturesMap = groupFeatures + .getValue(); + for (Entry> seqFeatures : seqFeaturesMap + .entrySet()) + { + String sequenceName = seqFeatures.getKey(); + for (SequenceFeature sf : seqFeatures.getValue()) + { + formatJalviewFeature(out, sequenceName, sf); + } + } + if (!"".equals(group)) + { + out.append(ENDGROUP).append(TAB).append(group).append(newline); + } + } + + return count; + } + + /** + * Answers a list of mapped features visible in the (CDS/protein) complement, + * with feature positions translated to local sequence coordinates + * + * @param seq + * @param fr2 + * @return + */ + protected List findComplementaryFeatures(SequenceI seq, + FeatureRenderer fr2) + { + /* + * avoid duplication of features (e.g. peptide feature + * at all 3 mapped codon positions) + */ + List found = new ArrayList<>(); + List complementary = new ArrayList<>(); + + for (int pos = seq.getStart(); pos <= seq.getEnd(); pos++) + { + MappedFeatures mf = fr2.findComplementFeaturesAtResidue(seq, pos); + + if (mf != null) + { + MapList mapping = mf.mapping.getMap(); + for (SequenceFeature sf : mf.features) + { + /* + * make a virtual feature with local coordinates + */ + if (!found.contains(sf)) + { + String group = sf.getFeatureGroup(); + if (group == null) + { + group = ""; + } + found.add(sf); + int begin = sf.getBegin(); + int end = sf.getEnd(); + int[] range = mf.mapping.getTo() == seq.getDatasetSequence() + ? mapping.locateInTo(begin, end) + : mapping.locateInFrom(begin, end); + SequenceFeature sf2 = new SequenceFeature(sf, range[0], + range[1], group, sf.getScore()); + complementary.add(sf2); + } + } + } + } + + return complementary; + } + + /** * Outputs any feature filters defined for visible feature types, sandwiched by * STARTFILTERS and ENDFILTERS lines * @@ -745,7 +889,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI } } firstInGroup = false; - out.append(formatJalviewFeature(sequenceName, sf)); + formatJalviewFeature(out, sequenceName, sf); } } } @@ -759,14 +903,16 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI } /** + * Formats one feature in Jalview format and appends to the string buffer + * * @param out * @param sequenceName * @param sequenceFeature */ - protected String formatJalviewFeature( - String sequenceName, SequenceFeature sequenceFeature) + protected void formatJalviewFeature( + StringBuilder out, String sequenceName, + SequenceFeature sequenceFeature) { - StringBuilder out = new StringBuilder(64); if (sequenceFeature.description == null || sequenceFeature.description.equals("")) { @@ -791,7 +937,8 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI if (sequenceFeature.description.indexOf(href) == -1) { - out.append(" " + label + ""); + out.append(" ") + .append(label).append(""); } } @@ -816,8 +963,6 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI out.append(sequenceFeature.score); } out.append(newline); - - return out.toString(); } /** @@ -876,34 +1021,40 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI * Returns features output in GFF2 format * * @param sequences - * the sequences whose features are to be output + * the sequences whose features are to be + * output * @param visible - * a map whose keys are the type names of visible features + * a map whose keys are the type names of + * visible features * @param visibleFeatureGroups * @param includeNonPositionalFeatures + * @param includeComplement * @return */ public String printGffFormat(SequenceI[] sequences, - FeatureRenderer fr, boolean includeNonPositionalFeatures) + FeatureRenderer fr, boolean includeNonPositionalFeatures, + boolean includeComplement) { + FeatureRenderer fr2 = null; + if (includeComplement) + { + AlignViewportI comp = fr.getViewport().getCodingComplement(); + fr2 = Desktop.getAlignFrameFor(comp).getFeatureRenderer(); + } + Map visibleColours = fr.getDisplayedFeatureCols(); StringBuilder out = new StringBuilder(256); out.append(String.format("%s %d\n", GFF_VERSION, gffVersion == 0 ? 2 : gffVersion)); - if (!includeNonPositionalFeatures - && (visibleColours == null || visibleColours.isEmpty())) - { - return out.toString(); - } - String[] types = visibleColours == null ? new String[0] : visibleColours.keySet() .toArray(new String[visibleColours.keySet().size()]); for (SequenceI seq : sequences) { + List seqFeatures = new ArrayList<>(); List features = new ArrayList<>(); if (includeNonPositionalFeatures) { @@ -913,51 +1064,29 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI { features.addAll(seq.getFeatures().getPositionalFeatures(types)); } - for (SequenceFeature sf : features) { - if (!sf.isNonPositional() && !fr.isVisible(sf)) + if (sf.isNonPositional() || fr.isVisible(sf)) { /* - * feature hidden by group visibility, colour threshold, + * drop features hidden by group visibility, colour threshold, * or feature filter condition */ - continue; - } - - String source = sf.featureGroup; - if (source == null) - { - source = sf.getDescription(); + seqFeatures.add(sf); } + } - out.append(seq.getName()); - out.append(TAB); - out.append(source); - out.append(TAB); - out.append(sf.type); - out.append(TAB); - out.append(sf.begin); - out.append(TAB); - out.append(sf.end); - out.append(TAB); - out.append(sf.score); - out.append(TAB); - - int strand = sf.getStrand(); - out.append(strand == 1 ? "+" : (strand == -1 ? "-" : ".")); - out.append(TAB); - - String phase = sf.getPhase(); - out.append(phase == null ? "." : phase); - - // miscellaneous key-values (GFF column 9) - String attributes = sf.getAttributes(); - if (attributes != null) - { - out.append(TAB).append(attributes); - } + if (includeComplement) + { + seqFeatures.addAll(findComplementaryFeatures(seq, fr2)); + } + /* + * sort features here if wanted + */ + for (SequenceFeature sf : seqFeatures) + { + formatGffFeature(out, seq, sf); out.append(newline); } } @@ -966,6 +1095,46 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI } /** + * Formats one feature as GFF and appends to the string buffer + */ + private void formatGffFeature(StringBuilder out, SequenceI seq, + SequenceFeature sf) + { + String source = sf.featureGroup; + if (source == null) + { + source = sf.getDescription(); + } + + out.append(seq.getName()); + out.append(TAB); + out.append(source); + out.append(TAB); + out.append(sf.type); + out.append(TAB); + out.append(sf.begin); + out.append(TAB); + out.append(sf.end); + out.append(TAB); + out.append(sf.score); + out.append(TAB); + + int strand = sf.getStrand(); + out.append(strand == 1 ? "+" : (strand == -1 ? "-" : ".")); + out.append(TAB); + + String phase = sf.getPhase(); + out.append(phase == null ? "." : phase); + + // miscellaneous key-values (GFF column 9) + String attributes = sf.getAttributes(); + if (attributes != null) + { + out.append(TAB).append(attributes); + } + } + + /** * Returns a mapping given list of one or more Align descriptors (exonerate * format) * diff --git a/src/jalview/io/SequenceAnnotationReport.java b/src/jalview/io/SequenceAnnotationReport.java index dd09d03..f2f0657 100644 --- a/src/jalview/io/SequenceAnnotationReport.java +++ b/src/jalview/io/SequenceAnnotationReport.java @@ -24,6 +24,7 @@ import jalview.api.FeatureColourI; import jalview.datamodel.DBRefEntry; import jalview.datamodel.DBRefSource; import jalview.datamodel.GeneLociI; +import jalview.datamodel.MappedFeatures; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; import jalview.util.MessageManager; @@ -63,7 +64,7 @@ public class SequenceAnnotationReport * Comparator to order DBRefEntry by Source + accession id (case-insensitive), * with 'Primary' sources placed before others, and 'chromosome' first of all */ - private static Comparator comparator = new Comparator() + private static Comparator comparator = new Comparator<>() { @Override @@ -126,19 +127,33 @@ public class SequenceAnnotationReport * Append text for the list of features to the tooltip * * @param sb - * @param rpos + * @param residuePos * @param features * @param minmax */ - public void appendFeatures(final StringBuilder sb, int rpos, + public void appendFeatures(final StringBuilder sb, int residuePos, List features, FeatureRendererModel fr) { - if (features != null) + for (SequenceFeature feature : features) { - for (SequenceFeature feature : features) - { - appendFeature(sb, rpos, fr, feature); - } + appendFeature(sb, residuePos, fr, feature, null); + } + } + + /** + * Appends text for mapped features (e.g. CDS feature for peptide or vice versa) + * + * @param sb + * @param residuePos + * @param mf + * @param fr + */ + public void appendFeatures(StringBuilder sb, int residuePos, + MappedFeatures mf, FeatureRendererModel fr) + { + for (SequenceFeature feature : mf.features) + { + appendFeature(sb, residuePos, fr, feature, mf); } } @@ -151,7 +166,8 @@ public class SequenceAnnotationReport * @param feature */ void appendFeature(final StringBuilder sb, int rpos, - FeatureRendererModel fr, SequenceFeature feature) + FeatureRendererModel fr, SequenceFeature feature, + MappedFeatures mf) { if (feature.isContactFeature()) { @@ -220,6 +236,15 @@ public class SequenceAnnotationReport } } } + + if (mf != null) + { + String variants = mf.findProteinVariants(feature); + if (!variants.isEmpty()) + { + sb.append(" ").append(variants); + } + } } } @@ -374,7 +399,7 @@ public class SequenceAnnotationReport .getNonPositionalFeatures()) { int sz = -sb.length(); - appendFeature(sb, 0, fr, sf); + appendFeature(sb, 0, fr, sf, null); sz += sb.length(); maxWidth = Math.max(maxWidth, sz); } diff --git a/src/jalview/io/vcf/VCFLoader.java b/src/jalview/io/vcf/VCFLoader.java index 7bf7791..d461811 100644 --- a/src/jalview/io/vcf/VCFLoader.java +++ b/src/jalview/io/vcf/VCFLoader.java @@ -1,6 +1,5 @@ package jalview.io.vcf; -import jalview.analysis.AlignmentUtils; import jalview.analysis.Dna; import jalview.api.AlignViewControllerGuiI; import jalview.bin.Cache; @@ -23,6 +22,8 @@ import jalview.util.MessageManager; import java.io.File; import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -51,6 +52,8 @@ import htsjdk.variant.vcf.VCFInfoHeaderLine; */ public class VCFLoader { + private static final String UTF_8 = "UTF-8"; + private static final String DEFAULT_SPECIES = "homo_sapiens"; /** @@ -654,7 +657,8 @@ public class VCFLoader /* * dna-to-peptide product mapping */ - AlignmentUtils.computeProteinFeatures(seq, mapTo, map); + // JAL-3187 render on the fly instead + // AlignmentUtils.computeProteinFeatures(seq, mapTo, map); } else { @@ -1189,14 +1193,6 @@ public class VCFLoader } /* - * filter out fields we don't want to capture - */ - if (!vcfFieldsOfInterest.contains(key)) - { - continue; - } - - /* * we extract values for other data which are allele-specific; * these may be per alternate allele (INFO[key].Number = 'A') * or per allele including reference (INFO[key].Number = 'R') @@ -1235,6 +1231,16 @@ public class VCFLoader String value = getAttributeValue(variant, key, index); if (value != null) { + /* + * VCF spec requires encoding of special characters e.g. '=' + * so decode them here before storing + */ + try + { + value = URLDecoder.decode(value, UTF_8); + } catch (UnsupportedEncodingException e) + { + } sf.setValue(key, value); } } @@ -1288,6 +1294,16 @@ public class VCFLoader String id = vepFieldsOfInterest.get(i); if (id != null) { + /* + * VCF spec requires encoding of special characters e.g. '=' + * so decode them here before storing + */ + try + { + field = URLDecoder.decode(field, UTF_8); + } catch (UnsupportedEncodingException e) + { + } csqValues.put(id, field); } } diff --git a/src/jalview/project/Jalview2XML.java b/src/jalview/project/Jalview2XML.java index 0e17779..5b1c0a3 100644 --- a/src/jalview/project/Jalview2XML.java +++ b/src/jalview/project/Jalview2XML.java @@ -60,7 +60,6 @@ import jalview.gui.AlignmentPanel; import jalview.gui.AppVarna; import jalview.gui.ChimeraViewFrame; import jalview.gui.Desktop; -import jalview.gui.FeatureRenderer; import jalview.gui.JvOptionPane; import jalview.gui.OOMWarning; import jalview.gui.PCAPanel; @@ -94,6 +93,7 @@ import jalview.util.matcher.Condition; import jalview.viewmodel.AlignmentViewport; import jalview.viewmodel.PCAModel; import jalview.viewmodel.ViewportRanges; +import jalview.viewmodel.seqfeatures.FeatureRendererModel; import jalview.viewmodel.seqfeatures.FeatureRendererSettings; import jalview.viewmodel.seqfeatures.FeaturesDisplayed; import jalview.ws.jws2.Jws2Discoverer; @@ -1501,11 +1501,14 @@ public class Jalview2XML view.setFollowHighlight(av.isFollowHighlight()); view.setFollowSelection(av.followSelection); view.setIgnoreGapsinConsensus(av.isIgnoreGapsConsensus()); + view.setShowComplementFeatures(av.isShowComplementFeatures()); + view.setShowComplementFeaturesOnTop( + av.isShowComplementFeaturesOnTop()); if (av.getFeaturesDisplayed() != null) { FeatureSettings fs = new FeatureSettings(); - FeatureRenderer fr = ap.getSeqPanel().seqCanvas + FeatureRendererModel fr = ap.getSeqPanel().seqCanvas .getFeatureRenderer(); String[] renderOrder = fr.getRenderOrder().toArray(new String[0]); @@ -5019,11 +5022,14 @@ public class Jalview2XML viewport.setShowNPFeats(safeBoolean(view.isShowNPfeatureTooltip())); viewport.setShowGroupConsensus(view.isShowGroupConsensus()); viewport.setShowGroupConservation(view.isShowGroupConservation()); + viewport.setShowComplementFeatures(view.isShowComplementFeatures()); + viewport.setShowComplementFeaturesOnTop( + view.isShowComplementFeaturesOnTop()); // recover feature settings if (jm.getFeatureSettings() != null) { - FeatureRenderer fr = af.alignPanel.getSeqPanel().seqCanvas + FeatureRendererModel fr = af.alignPanel.getSeqPanel().seqCanvas .getFeatureRenderer(); FeaturesDisplayed fdi; viewport.setFeaturesDisplayed(fdi = new FeaturesDisplayed()); diff --git a/src/jalview/renderer/seqfeatures/FeatureColourFinder.java b/src/jalview/renderer/seqfeatures/FeatureColourFinder.java index cfe2735..d5784b0 100644 --- a/src/jalview/renderer/seqfeatures/FeatureColourFinder.java +++ b/src/jalview/renderer/seqfeatures/FeatureColourFinder.java @@ -20,6 +20,7 @@ */ package jalview.renderer.seqfeatures; +import jalview.api.AlignViewportI; import jalview.api.FeatureRenderer; import jalview.api.FeaturesDisplayedI; import jalview.datamodel.SequenceI; @@ -122,8 +123,16 @@ public class FeatureColourFinder */ boolean noFeaturesDisplayed() { - if (featureRenderer == null - || !featureRenderer.getViewport().isShowSequenceFeatures()) + if (featureRenderer == null) + { + return true; + } + AlignViewportI av = featureRenderer.getViewport(); + if (av.isShowComplementFeatures()) + { + return false; + } + if (!av.isShowSequenceFeatures()) { return true; } diff --git a/src/jalview/renderer/seqfeatures/FeatureRenderer.java b/src/jalview/renderer/seqfeatures/FeatureRenderer.java index 13885b4..a1980c7 100644 --- a/src/jalview/renderer/seqfeatures/FeatureRenderer.java +++ b/src/jalview/renderer/seqfeatures/FeatureRenderer.java @@ -23,9 +23,13 @@ package jalview.renderer.seqfeatures; import jalview.api.AlignViewportI; import jalview.api.FeatureColourI; import jalview.datamodel.ContiguousI; +import jalview.datamodel.MappedFeatures; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; +import jalview.gui.AlignFrame; +import jalview.gui.Desktop; import jalview.util.Comparison; +import jalview.util.ReverseListIterator; import jalview.viewmodel.seqfeatures.FeatureRendererModel; import java.awt.AlphaComposite; @@ -273,7 +277,8 @@ public class FeatureRenderer extends FeatureRendererModel * if columns are all gapped, or sequence has no features, nothing to do */ ContiguousI visiblePositions = seq.findPositions(start + 1, end + 1); - if (visiblePositions == null || !seq.getFeatures().hasFeatures()) + if (visiblePositions == null || !seq.getFeatures().hasFeatures() + && !av.isShowComplementFeatures()) { return null; } @@ -290,6 +295,16 @@ public class FeatureRenderer extends FeatureRendererModel Color drawnColour = null; /* + * draw 'complement' features below ours if configured to do so + */ + if (av.isShowComplementFeatures() + && !av.isShowComplementFeaturesOnTop()) + { + drawnColour = drawComplementFeatures(g, seq, start, end, y1, + colourOnly, visiblePositions, drawnColour); + } + + /* * iterate over features in ordering of their rendering (last is on top) */ for (int renderIndex = 0; renderIndex < renderOrder.length; renderIndex++) @@ -315,7 +330,7 @@ public class FeatureRenderer extends FeatureRendererModel if (featureColour == null) { /* - * feature excluded by visibility settings, filters, or colour threshold + * feature excluded by filters, or colour threshold */ continue; } @@ -394,6 +409,15 @@ public class FeatureRenderer extends FeatureRendererModel } } + /* + * draw 'complement' features above ours if configured to do so + */ + if (av.isShowComplementFeatures() && av.isShowComplementFeaturesOnTop()) + { + drawnColour = drawComplementFeatures(g, seq, start, end, y1, + colourOnly, visiblePositions, drawnColour); + } + if (transparency != 1.0f && g != null) { /* @@ -407,6 +431,52 @@ public class FeatureRenderer extends FeatureRendererModel } /** + * Find any features on the CDS/protein complement of the sequence region and + * draw them, with visibility and colouring as configured in the complementary + * viewport + * + * @param g + * @param seq + * @param start + * @param end + * @param y1 + * @param colourOnly + * @param visiblePositions + * @param drawnColour + * @return + */ + Color drawComplementFeatures(final Graphics g, final SequenceI seq, + int start, int end, int y1, boolean colourOnly, + ContiguousI visiblePositions, Color drawnColour) + { + AlignViewportI comp = av.getCodingComplement(); + FeatureRenderer fr2 = Desktop.getAlignFrameFor(comp) + .getFeatureRenderer(); + + final int visibleStart = visiblePositions.getBegin(); + final int visibleEnd = visiblePositions.getEnd(); + + for (int pos = visibleStart; pos <= visibleEnd; pos++) + { + int column = seq.findIndex(pos); + MappedFeatures mf = fr2 + .findComplementFeaturesAtResidue(seq, pos); + if (mf != null) + { + for (SequenceFeature sf : mf.features) + { + FeatureColourI fc = fr2.getFeatureStyle(sf.getType()); + Color featureColour = fr2.getColor(sf, fc); + renderFeature(g, seq, column - 1, column - 1, featureColour, + start, end, y1, colourOnly); + drawnColour = featureColour; + } + } + } + return drawnColour; + } + + /** * Called when alignment in associated view has new/modified features to * discover and display. * @@ -441,6 +511,18 @@ public class FeatureRenderer extends FeatureRendererModel updateFeatures(); /* + * show complement features on top (if configured to show them) + */ + if (av.isShowComplementFeatures() && av.isShowComplementFeaturesOnTop()) + { + Color col = findComplementFeatureColour(seq, column); + if (col != null) + { + return col; + } + } + + /* * inspect features in reverse renderOrder (the last in the array is * displayed on top) until we find one that is rendered at the position */ @@ -469,8 +551,43 @@ public class FeatureRenderer extends FeatureRendererModel } /* - * no displayed feature found at position + * show complement features underneath (if configured to show them) */ + Color col = null; + if (av.isShowComplementFeatures() + && !av.isShowComplementFeaturesOnTop()) + { + col = findComplementFeatureColour(seq, column); + } + + return col; + } + + Color findComplementFeatureColour(SequenceI seq, int column) + { + AlignViewportI complement = av.getCodingComplement(); + AlignFrame af = Desktop.getAlignFrameFor(complement); + FeatureRendererModel fr2 = af.getFeatureRenderer(); + MappedFeatures mf = fr2.findComplementFeaturesAtResidue( + seq, seq.findPosition(column - 1)); + if (mf == null) + { + return null; + } + ReverseListIterator it = new ReverseListIterator<>( + mf.features); + while (it.hasNext()) + { + SequenceFeature sf = it.next(); + if (!fr2.featureGroupNotShown(sf)) + { + Color col = fr2.getColour(sf); + if (col != null) + { + return col; + } + } + } return null; } } diff --git a/src/jalview/structure/SequenceListener.java b/src/jalview/structure/SequenceListener.java index 81ff739..f8c5bea 100644 --- a/src/jalview/structure/SequenceListener.java +++ b/src/jalview/structure/SequenceListener.java @@ -28,7 +28,15 @@ public interface SequenceListener // TODO remove this? never called on SequenceListener type public void mouseOverSequence(SequenceI sequence, int index, int pos); - public void highlightSequence(SearchResultsI results); + /** + * Highlights any position(s) represented by the search results and + * (optionally) returns an informative message about the position(s) + * higlighted + * + * @param results + * @return + */ + public String highlightSequence(SearchResultsI results); // TODO remove this? never called public void updateColours(SequenceI sequence, int index); diff --git a/src/jalview/structure/StructureSelectionManager.java b/src/jalview/structure/StructureSelectionManager.java index ff88bb7..054ed2f 100644 --- a/src/jalview/structure/StructureSelectionManager.java +++ b/src/jalview/structure/StructureSelectionManager.java @@ -859,13 +859,14 @@ public class StructureSelectionManager * @param pdbResNum * @param chain * @param pdbfile + * @return */ - public void mouseOverStructure(int pdbResNum, String chain, + public String mouseOverStructure(int pdbResNum, String chain, String pdbfile) { AtomSpec atomSpec = new AtomSpec(pdbfile, chain, pdbResNum, 0); List atoms = Collections.singletonList(atomSpec); - mouseOverStructure(atoms); + return mouseOverStructure(atoms); } /** @@ -873,12 +874,12 @@ public class StructureSelectionManager * * @param atoms */ - public void mouseOverStructure(List atoms) + public String mouseOverStructure(List atoms) { if (listeners == null) { // old or prematurely sent event - return; + return null; } boolean hasSequenceListener = false; for (int i = 0; i < listeners.size(); i++) @@ -890,18 +891,24 @@ public class StructureSelectionManager } if (!hasSequenceListener) { - return; + return null; } SearchResultsI results = findAlignmentPositionsForStructurePositions( atoms); + String result = null; for (Object li : listeners) { if (li instanceof SequenceListener) { - ((SequenceListener) li).highlightSequence(results); + String s = ((SequenceListener) li).highlightSequence(results); + if (s != null) + { + result = s; + } } } + return result; } /** diff --git a/src/jalview/util/MapList.java b/src/jalview/util/MapList.java index 9f28ee1..731e976 100644 --- a/src/jalview/util/MapList.java +++ b/src/jalview/util/MapList.java @@ -1236,4 +1236,14 @@ public class MapList return new MapList(getFromRanges(), toRanges, outFromRatio, outToRatio); } + /** + * Answers true if the mapping is from one contiguous range to another, else + * false + * + * @return + */ + public boolean isContiguous() + { + return fromShifts.size() == 1 && toShifts.size() == 1; + } } diff --git a/src/jalview/viewmodel/AlignmentViewport.java b/src/jalview/viewmodel/AlignmentViewport.java index 148ea16..8dcd1b3 100644 --- a/src/jalview/viewmodel/AlignmentViewport.java +++ b/src/jalview/viewmodel/AlignmentViewport.java @@ -2716,6 +2716,30 @@ public abstract class AlignmentViewport viewStyle.setProteinFontAsCdna(b); } + @Override + public void setShowComplementFeatures(boolean b) + { + viewStyle.setShowComplementFeatures(b); + } + + @Override + public boolean isShowComplementFeatures() + { + return viewStyle.isShowComplementFeatures(); + } + + @Override + public void setShowComplementFeaturesOnTop(boolean b) + { + viewStyle.setShowComplementFeaturesOnTop(b); + } + + @Override + public boolean isShowComplementFeaturesOnTop() + { + return viewStyle.isShowComplementFeaturesOnTop(); + } + /** * @return true if view should scroll to show the highlighted region of a * sequence diff --git a/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java b/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java index 17f9362..f9c9782 100644 --- a/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java +++ b/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java @@ -23,7 +23,13 @@ package jalview.viewmodel.seqfeatures; import jalview.api.AlignViewportI; import jalview.api.FeatureColourI; import jalview.api.FeaturesDisplayedI; +import jalview.datamodel.AlignedCodonFrame; import jalview.datamodel.AlignmentI; +import jalview.datamodel.MappedFeatures; +import jalview.datamodel.Mapping; +import jalview.datamodel.SearchResultMatchI; +import jalview.datamodel.SearchResults; +import jalview.datamodel.SearchResultsI; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; import jalview.datamodel.features.FeatureMatcherSetI; @@ -320,12 +326,12 @@ public abstract class FeatureRendererModel visibleTypes); /* - * include features unless their feature group is not displayed, or - * they are hidden (have no colour) based on a filter or colour threshold + * include features unless they are hidden (have no colour), based on + * feature group visibility, or a filter or colour threshold */ for (SequenceFeature sf : features) { - if (!featureGroupNotShown(sf) && getColour(sf) != null) + if (getColour(sf) != null) { result.add(sf); } @@ -983,7 +989,7 @@ public abstract class FeatureRendererModel * @param sequenceFeature * @return */ - protected boolean featureGroupNotShown(final SequenceFeature sequenceFeature) + public boolean featureGroupNotShown(final SequenceFeature sequenceFeature) { return featureGroups != null && sequenceFeature.featureGroup != null @@ -998,7 +1004,7 @@ public abstract class FeatureRendererModel */ @Override public List findFeaturesAtResidue(SequenceI sequence, - int resNo) + int fromResNo, int toResNo) { List result = new ArrayList<>(); if (!av.areFeaturesDisplayed() || getFeaturesDisplayed() == null) @@ -1011,12 +1017,11 @@ public abstract class FeatureRendererModel * displayed, and feature group is null or the empty string * or marked for display */ - Set visibleFeatures = getFeaturesDisplayed() - .getVisibleFeatures(); + List visibleFeatures = getDisplayedFeatureTypes(); String[] visibleTypes = visibleFeatures .toArray(new String[visibleFeatures.size()]); List features = sequence.getFeatures().findFeatures( - resNo, resNo, visibleTypes); + fromResNo, toResNo, visibleTypes); for (SequenceFeature sf : features) { @@ -1157,6 +1162,89 @@ public abstract class FeatureRendererModel } @Override + public MappedFeatures findComplementFeaturesAtResidue(SequenceI sequence, + int pos) + { + SequenceI ds = sequence.getDatasetSequence(); + if (ds == null) + { + ds = sequence; + } + final char residue = ds.getCharAt(pos - ds.getStart()); + + List found = new ArrayList<>(); + List mappings = this.av.getAlignment() + .getCodonFrame(sequence); + + /* + * todo: direct lookup of CDS for peptide and vice-versa; for now, + * have to search through an unordered list of mappings for a candidate + */ + Mapping mapping = null; + SequenceI mapFrom = null; + + for (AlignedCodonFrame acf : mappings) + { + mapping = acf.getMappingForSequence(sequence); + if (mapping == null || !mapping.getMap().isTripletMap()) + { + continue; // we are only looking for 3:1 or 1:3 mappings + } + SearchResultsI sr = new SearchResults(); + acf.markMappedRegion(ds, pos, sr); + for (SearchResultMatchI match : sr.getResults()) + { + int fromRes = match.getStart(); + int toRes = match.getEnd(); + mapFrom = match.getSequence(); + List fs = findFeaturesAtResidue( + match.getSequence(), fromRes, toRes); + for (SequenceFeature sf : fs) + { + if (!found.contains(sf)) + { + found.add(sf); + } + } + } + + /* + * just take the first mapped features we find + */ + if (!found.isEmpty()) + { + break; + } + } + if (found.isEmpty()) + { + return null; + } + + /* + * sort by renderorder, inefficiently + */ + List result = new ArrayList<>(); + for (String type : renderOrder) + { + for (SequenceFeature sf : found) + { + if (type.equals(sf.getType())) + { + result.add(sf); + if (result.size() == found.size()) + { + return new MappedFeatures(mapping, mapFrom, pos, residue, + result); + } + } + } + } + + return new MappedFeatures(mapping, mapFrom, pos, residue, result); + } + + @Override public boolean isVisible(SequenceFeature feature) { if (feature == null) diff --git a/src/jalview/viewmodel/styles/ViewStyle.java b/src/jalview/viewmodel/styles/ViewStyle.java index 16aa580..91f2f0c 100644 --- a/src/jalview/viewmodel/styles/ViewStyle.java +++ b/src/jalview/viewmodel/styles/ViewStyle.java @@ -213,6 +213,8 @@ public class ViewStyle implements ViewStyleI setShowNPFeats(vs.isShowNPFeats()); setShowSequenceFeaturesHeight(vs.isShowSequenceFeaturesHeight()); setShowSequenceFeatures(vs.isShowSequenceFeatures()); + setShowComplementFeatures(vs.isShowComplementFeatures()); + setShowComplementFeaturesOnTop(vs.isShowComplementFeaturesOnTop()); setShowText(vs.getShowText()); setShowUnconserved(vs.getShowUnconserved()); setTextColour(vs.getTextColour()); @@ -275,6 +277,9 @@ public class ViewStyle implements ViewStyleI && isShowSequenceFeaturesHeight() == vs .isShowSequenceFeaturesHeight() && isShowSequenceFeatures() == vs.isShowSequenceFeatures() + && isShowComplementFeatures() == vs.isShowComplementFeatures() + && isShowComplementFeaturesOnTop() == vs + .isShowComplementFeaturesOnTop() && getShowText() == vs.getShowText() && getShowUnconserved() == vs.getShowUnconserved() && getThreshold() == vs.getThreshold() @@ -365,6 +370,10 @@ public class ViewStyle implements ViewStyleI private int fontStyle; + private boolean showComplementFeatures; + + private boolean showComplementFeaturesOnTop; + /** * GUI state * @@ -1111,4 +1120,28 @@ public class ViewStyle implements ViewStyleI { proteinFontAsCdna = b; } + + @Override + public void setShowComplementFeatures(boolean b) + { + showComplementFeatures = b; + } + + @Override + public boolean isShowComplementFeatures() + { + return showComplementFeatures; + } + + @Override + public void setShowComplementFeaturesOnTop(boolean b) + { + showComplementFeaturesOnTop = b; + } + + @Override + public boolean isShowComplementFeaturesOnTop() + { + return showComplementFeaturesOnTop; + } } diff --git a/test/jalview/analysis/AlignmentUtilsTests.java b/test/jalview/analysis/AlignmentUtilsTests.java index 14559dd..dabd3ee 100644 --- a/test/jalview/analysis/AlignmentUtilsTests.java +++ b/test/jalview/analysis/AlignmentUtilsTests.java @@ -2044,41 +2044,48 @@ public class AlignmentUtilsTests * NB setting "id" (as returned by Ensembl for features in JSON format); * previously "ID" (as returned for GFF3 format) */ - SequenceFeature sf1 = new SequenceFeature("sequence_variant", "", 1, 1, + SequenceFeature sf1 = new SequenceFeature("sequence_variant", "", 10, + 10, 0f, ensembl); sf1.setValue("alleles", "A,G"); // AAA -> GAA -> K/E sf1.setValue("id", "var1.125A>G"); - SequenceFeature sf2 = new SequenceFeature("sequence_variant", "", 1, 1, + SequenceFeature sf2 = new SequenceFeature("sequence_variant", "", 10, + 10, 0f, dbSnp); sf2.setValue("alleles", "A,C"); // AAA -> CAA -> K/Q sf2.setValue("id", "var2"); sf2.setValue("clinical_significance", "Dodgy"); - SequenceFeature sf3 = new SequenceFeature("sequence_variant", "", 1, 1, + SequenceFeature sf3 = new SequenceFeature("sequence_variant", "", 11, + 11, 0f, dbSnp); sf3.setValue("alleles", "A,T"); // AAA -> TAA -> stop codon sf3.setValue("id", "var3"); sf3.setValue("clinical_significance", "Bad"); - SequenceFeature sf4 = new SequenceFeature("sequence_variant", "", 3, 3, + SequenceFeature sf4 = new SequenceFeature("sequence_variant", "", 12, + 12, 0f, cosmic); sf4.setValue("alleles", "A,G"); // AAA -> AAG synonymous sf4.setValue("id", "var4"); sf4.setValue("clinical_significance", "None"); - SequenceFeature sf5 = new SequenceFeature("sequence_variant", "", 3, 3, + SequenceFeature sf5 = new SequenceFeature("sequence_variant", "", 12, + 12, 0f, ensembl); sf5.setValue("alleles", "A,T"); // AAA -> AAT -> K/N sf5.setValue("id", "sequence_variant:var5"); // prefix gets stripped off sf5.setValue("clinical_significance", "Benign"); - SequenceFeature sf6 = new SequenceFeature("sequence_variant", "", 6, 6, + SequenceFeature sf6 = new SequenceFeature("sequence_variant", "", 15, + 15, 0f, dbSnp); sf6.setValue("alleles", "T,C"); // TTT -> TTC synonymous sf6.setValue("id", "var6"); - SequenceFeature sf7 = new SequenceFeature("sequence_variant", "", 8, 8, + SequenceFeature sf7 = new SequenceFeature("sequence_variant", "", 17, + 17, 0f, cosmic); sf7.setValue("alleles", "C,A,G"); // CCC -> CAC,CGC -> P/H/R sf7.setValue("id", "var7"); @@ -2103,7 +2110,7 @@ public class AlignmentUtilsTests // codon2Variants.add(new DnaVariant("A")); codon3Variants.add(new DnaVariant("A", sf4)); codon3Variants.add(new DnaVariant("A", sf5)); - AlignmentUtils.computePeptideVariants(peptide, 1, codonVariants); + AlignmentUtils.computePeptideVariants(peptide, 10, codonVariants); /* * compute variants for protein position 2 @@ -2114,7 +2121,7 @@ public class AlignmentUtilsTests codon1Variants.add(new DnaVariant("T")); codon2Variants.add(new DnaVariant("T")); codon3Variants.add(new DnaVariant("T", sf6)); - AlignmentUtils.computePeptideVariants(peptide, 2, codonVariants); + AlignmentUtils.computePeptideVariants(peptide, 11, codonVariants); /* * compute variants for protein position 3 @@ -2125,7 +2132,7 @@ public class AlignmentUtilsTests codon1Variants.add(new DnaVariant("C")); codon2Variants.add(new DnaVariant("C", sf7)); codon3Variants.add(new DnaVariant("C")); - AlignmentUtils.computePeptideVariants(peptide, 3, codonVariants); + AlignmentUtils.computePeptideVariants(peptide, 12, codonVariants); /* * verify added sequence features for @@ -2149,55 +2156,55 @@ public class AlignmentUtilsTests */ // AAA -> AAT -> K/N SequenceFeature sf = sfs.get(0); - assertEquals(1, sf.getBegin()); - assertEquals(1, sf.getEnd()); + assertEquals(10, sf.getBegin()); + assertEquals(10, sf.getEnd()); assertEquals("nonsynonymous_variant", sf.getType()); - assertEquals("p.Lys1Asn", sf.getDescription()); + assertEquals("p.Lys10Asn", sf.getDescription()); assertEquals("var5", sf.getValue("id")); assertEquals("Benign", sf.getValue("clinical_significance")); assertEquals("id=var5;clinical_significance=Benign", sf.getAttributes()); assertEquals(1, sf.links.size()); assertEquals( - "p.Lys1Asn var5|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var5", + "p.Lys10Asn var5|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var5", sf.links.get(0)); assertEquals(ensembl, sf.getFeatureGroup()); // AAA -> CAA -> K/Q sf = sfs.get(1); - assertEquals(1, sf.getBegin()); - assertEquals(1, sf.getEnd()); + assertEquals(10, sf.getBegin()); + assertEquals(10, sf.getEnd()); assertEquals("nonsynonymous_variant", sf.getType()); - assertEquals("p.Lys1Gln", sf.getDescription()); + assertEquals("p.Lys10Gln", sf.getDescription()); assertEquals("var2", sf.getValue("id")); assertEquals("Dodgy", sf.getValue("clinical_significance")); assertEquals("id=var2;clinical_significance=Dodgy", sf.getAttributes()); assertEquals(1, sf.links.size()); assertEquals( - "p.Lys1Gln var2|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var2", + "p.Lys10Gln var2|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var2", sf.links.get(0)); assertEquals(dbSnp, sf.getFeatureGroup()); // AAA -> GAA -> K/E sf = sfs.get(2); - assertEquals(1, sf.getBegin()); - assertEquals(1, sf.getEnd()); + assertEquals(10, sf.getBegin()); + assertEquals(10, sf.getEnd()); assertEquals("nonsynonymous_variant", sf.getType()); - assertEquals("p.Lys1Glu", sf.getDescription()); + assertEquals("p.Lys10Glu", sf.getDescription()); assertEquals("var1.125A>G", sf.getValue("id")); assertNull(sf.getValue("clinical_significance")); assertEquals("id=var1.125A>G", sf.getAttributes()); assertEquals(1, sf.links.size()); // link to variation is urlencoded assertEquals( - "p.Lys1Glu var1.125A>G|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var1.125A%3EG", + "p.Lys10Glu var1.125A>G|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var1.125A%3EG", sf.links.get(0)); assertEquals(ensembl, sf.getFeatureGroup()); // AAA -> TAA -> stop codon sf = sfs.get(3); - assertEquals(1, sf.getBegin()); - assertEquals(1, sf.getEnd()); + assertEquals(10, sf.getBegin()); + assertEquals(10, sf.getEnd()); assertEquals("stop_gained", sf.getType()); assertEquals("Aaa/Taa", sf.getDescription()); assertEquals("var3", sf.getValue("id")); @@ -2211,8 +2218,8 @@ public class AlignmentUtilsTests // AAA -> AAG synonymous sf = sfs.get(4); - assertEquals(1, sf.getBegin()); - assertEquals(1, sf.getEnd()); + assertEquals(10, sf.getBegin()); + assertEquals(10, sf.getEnd()); assertEquals("synonymous_variant", sf.getType()); assertEquals("aaA/aaG", sf.getDescription()); assertEquals("var4", sf.getValue("id")); @@ -2226,8 +2233,8 @@ public class AlignmentUtilsTests // TTT -> TTC synonymous sf = sfs.get(5); - assertEquals(2, sf.getBegin()); - assertEquals(2, sf.getEnd()); + assertEquals(11, sf.getBegin()); + assertEquals(11, sf.getEnd()); assertEquals("synonymous_variant", sf.getType()); assertEquals("ttT/ttC", sf.getDescription()); assertEquals("var6", sf.getValue("id")); @@ -2242,31 +2249,31 @@ public class AlignmentUtilsTests // var7 generates two distinct protein variant features (two alleles) // CCC -> CGC -> P/R sf = sfs.get(6); - assertEquals(3, sf.getBegin()); - assertEquals(3, sf.getEnd()); + assertEquals(12, sf.getBegin()); + assertEquals(12, sf.getEnd()); assertEquals("nonsynonymous_variant", sf.getType()); - assertEquals("p.Pro3Arg", sf.getDescription()); + assertEquals("p.Pro12Arg", sf.getDescription()); assertEquals("var7", sf.getValue("id")); assertEquals("Good", sf.getValue("clinical_significance")); assertEquals("id=var7;clinical_significance=Good", sf.getAttributes()); assertEquals(1, sf.links.size()); assertEquals( - "p.Pro3Arg var7|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var7", + "p.Pro12Arg var7|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var7", sf.links.get(0)); assertEquals(cosmic, sf.getFeatureGroup()); // CCC -> CAC -> P/H sf = sfs.get(7); - assertEquals(3, sf.getBegin()); - assertEquals(3, sf.getEnd()); + assertEquals(12, sf.getBegin()); + assertEquals(12, sf.getEnd()); assertEquals("nonsynonymous_variant", sf.getType()); - assertEquals("p.Pro3His", sf.getDescription()); + assertEquals("p.Pro12His", sf.getDescription()); assertEquals("var7", sf.getValue("id")); assertEquals("Good", sf.getValue("clinical_significance")); assertEquals("id=var7;clinical_significance=Good", sf.getAttributes()); assertEquals(1, sf.links.size()); assertEquals( - "p.Pro3His var7|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var7", + "p.Pro12His var7|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var7", sf.links.get(0)); assertEquals(cosmic, sf.getFeatureGroup()); } @@ -2591,9 +2598,16 @@ public class AlignmentUtilsTests AlignmentI al2 = new Alignment(new SequenceI[] { dna3, dna4 }); ((Alignment) al2).createDatasetAlignment(); + /* + * alignment removes gapped columns (two internal, two trailing) + */ assertTrue(AlignmentUtils.alignAsSameSequences(al1, al2)); - assertEquals(seq1, al1.getSequenceAt(0).getSequenceAsString()); - assertEquals(seq2, al1.getSequenceAt(1).getSequenceAsString()); + String aligned1 = "-cc-GG-GTTT-aaa"; + assertEquals(aligned1, + al1.getSequenceAt(0).getSequenceAsString()); + String aligned2 = "C--C-Cgg-gtttAAA"; + assertEquals(aligned2, + al1.getSequenceAt(1).getSequenceAsString()); /* * add another sequence to 'aligned' - should still succeed, since @@ -2603,8 +2617,8 @@ public class AlignmentUtilsTests dna5.createDatasetSequence(); al2.addSequence(dna5); assertTrue(AlignmentUtils.alignAsSameSequences(al1, al2)); - assertEquals(seq1, al1.getSequenceAt(0).getSequenceAsString()); - assertEquals(seq2, al1.getSequenceAt(1).getSequenceAsString()); + assertEquals(aligned1, al1.getSequenceAt(0).getSequenceAsString()); + assertEquals(aligned2, al1.getSequenceAt(1).getSequenceAsString()); /* * add another sequence to 'unaligned' - should fail, since now not @@ -2622,15 +2636,15 @@ public class AlignmentUtilsTests { SequenceI dna1 = new Sequence("dna1", "cccGGGTTTaaa"); SequenceI dna2 = new Sequence("dna2", "CCCgggtttAAA"); - SequenceI as1 = dna1.deriveSequence(); - SequenceI as2 = dna1.deriveSequence().getSubSequence(3, 7); - SequenceI as3 = dna2.deriveSequence(); + SequenceI as1 = dna1.deriveSequence(); // cccGGGTTTaaa/1-12 + SequenceI as2 = dna1.deriveSequence().getSubSequence(3, 7); // GGGT/4-7 + SequenceI as3 = dna2.deriveSequence(); // CCCgggtttAAA/1-12 as1.insertCharAt(6, 5, '-'); - String s_as1 = as1.getSequenceAsString(); + assertEquals("cccGGG-----TTTaaa", as1.getSequenceAsString()); as2.insertCharAt(6, 5, '-'); - String s_as2 = as2.getSequenceAsString(); - as3.insertCharAt(6, 5, '-'); - String s_as3 = as3.getSequenceAsString(); + assertEquals("GGGT-----", as2.getSequenceAsString()); + as3.insertCharAt(3, 5, '-'); + assertEquals("CCC-----gggtttAAA", as3.getSequenceAsString()); AlignmentI aligned = new Alignment(new SequenceI[] { as1, as2, as3 }); // why do we need to cast this still ? @@ -2642,10 +2656,13 @@ public class AlignmentUtilsTests uas3 }); ((Alignment) tobealigned).createDatasetAlignment(); + /* + * alignAs lines up dataset sequences and removes empty columns (two) + */ assertTrue(AlignmentUtils.alignAsSameSequences(tobealigned, aligned)); - assertEquals(s_as1, uas1.getSequenceAsString()); - assertEquals(s_as2, uas2.getSequenceAsString()); - assertEquals(s_as3, uas3.getSequenceAsString()); + assertEquals("cccGGG---TTTaaa", uas1.getSequenceAsString()); + assertEquals("GGGT", uas2.getSequenceAsString()); + assertEquals("CCC---gggtttAAA", uas3.getSequenceAsString()); } @Test(groups = { "Functional" }) diff --git a/test/jalview/datamodel/MappedFeaturesTest.java b/test/jalview/datamodel/MappedFeaturesTest.java new file mode 100644 index 0000000..e4caac3 --- /dev/null +++ b/test/jalview/datamodel/MappedFeaturesTest.java @@ -0,0 +1,113 @@ +package jalview.datamodel; + +import static org.testng.Assert.assertEquals; + +import jalview.util.MapList; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.testng.annotations.Test; + +public class MappedFeaturesTest +{ + @Test + public void testFindProteinVariants() + { + /* + * scenario: + * dna/10-20 aCGTaGctGAa (codons CGT=R, GGA = G) + * mapping: 3:1 from [11-13,15,18-19] to peptide/1-2 RG + */ + SequenceI from = new Sequence("dna/10-20", "ACGTAGCTGAA"); + SequenceI to = new Sequence("peptide", "RG"); + MapList map = new MapList(new int[] { 11, 13, 15, 15, 18, 19 }, + new int[] + { 1, 2 }, 3, 1); + Mapping mapping = new Mapping(to, map); + + /* + * variants + * C>T at dna11, consequence CGT>TGT=C + * T>C at dna13, consequence CGT>CGC synonymous + */ + List features = new ArrayList<>(); + SequenceFeature sf1 = new SequenceFeature("sequence_variant", "C,T", + 11, 11, null); + sf1.setValue("alleles", "C,T"); + features.add(sf1); + SequenceFeature sf2 = new SequenceFeature("sequence_variant", "T,C", 13, + 13, null); + sf2.setValue("alleles", "T,C"); + features.add(sf2); + + /* + * missense variant in first codon + */ + MappedFeatures mf = new MappedFeatures(mapping, from, 1, 'R', + features); + String variant = mf.findProteinVariants(sf1); + assertEquals(variant, "p.Arg1Cys"); + + /* + * more than one alternative allele + * C>G consequence is GGT=G + * peptide variants as a comma-separated list + */ + sf1.setValue("alleles", "C,T,G"); + variant = mf.findProteinVariants(sf1); + assertEquals(variant, "p.Arg1Cys,p.Arg1Gly"); + + /* + * synonymous variant in first codon + * shown in HGVS notation on peptide + */ + variant = mf.findProteinVariants(sf2); + assertEquals(variant, "c.13T>C(p.=)"); + + /* + * CSQ:HGVSp value is used if present + */ + Map csq = new HashMap<>(); + csq.put("HGVSp", "hello:world"); + sf2.setValue("CSQ", csq); + variant = mf.findProteinVariants(sf2); + assertEquals(variant, "world"); + + /* + * missense and indel variants in second codon + * - codon is GGA spliced from dna positions 15,18,19 + * - SNP G>T in second position mutates GGA>G to GTA>V + * - indel variants are not computed or reported + */ + mf = new MappedFeatures(mapping, from, 2, 'G', features); + features.clear(); + SequenceFeature sf3 = new SequenceFeature("sequence_variant", + "G,-,CG,T", 18, 18, null); + sf3.setValue("alleles", "G,-,CG,T"); + features.add(sf3); + variant = mf.findProteinVariants(sf3); + assertEquals(variant, "p.Gly2Val"); + + /* + * G>T in first position gives TGA Stop + * shown with HGVS notation as 'Ter' + */ + SequenceFeature sf4 = new SequenceFeature("sequence_variant", "G,T", 15, + 15, null); + sf4.setValue("alleles", "G,-,CG,T"); + features.add(sf4); + variant = mf.findProteinVariants(sf4); + assertEquals(variant, "p.Gly2Ter"); + + /* + * feature must be one of those in MappedFeatures + */ + SequenceFeature sf9 = new SequenceFeature("sequence_variant", "G,C", 15, + 15, null); + variant = mf.findProteinVariants(sf9); + assertEquals(variant, ""); + } +} diff --git a/test/jalview/datamodel/SequenceFeatureTest.java b/test/jalview/datamodel/SequenceFeatureTest.java index 9111e5a..cd8f9eb 100644 --- a/test/jalview/datamodel/SequenceFeatureTest.java +++ b/test/jalview/datamodel/SequenceFeatureTest.java @@ -277,43 +277,46 @@ public class SequenceFeatureTest @Test(groups = { "Functional" }) public void testGetDetailsReport() { + SequenceI seq = new Sequence("TestSeq", "PLRFQMD"); + String seqName = seq.getName(); + // single locus, no group, no score SequenceFeature sf = new SequenceFeature("variant", "G,C", 22, 22, null); - String expected = "
" - + "" + String expected = "
Typevariant
Start/end22
" + + "" + "
LocationTestSeq22
Typevariant
DescriptionG,C
"; - assertEquals(expected, sf.getDetailsReport()); + assertEquals(expected, sf.getDetailsReport(seqName)); // contact feature sf = new SequenceFeature("Disulphide Bond", "a description", 28, 31, null); - expected = "
" - + "" + expected = "
TypeDisulphide Bond
Start/end28:31
" + + "" + "
LocationTestSeq28:31
TypeDisulphide Bond
Descriptiona description
"; - assertEquals(expected, sf.getDetailsReport()); + assertEquals(expected, sf.getDetailsReport(seqName)); sf = new SequenceFeature("variant", "G,C", 22, 33, 12.5f, "group"); sf.setValue("Parent", "ENSG001"); sf.setValue("Child", "ENSP002"); - expected = "
" - + "" + expected = "
Typevariant
Start/end22-33
" + + "" + "" + "" + "" + "" + "
LocationTestSeq22-33
Typevariant
DescriptionG,C
Score12.5
Groupgroup
ChildENSP002
ParentENSG001
"; - assertEquals(expected, sf.getDetailsReport()); + assertEquals(expected, sf.getDetailsReport(seqName)); /* * feature with embedded html link in description */ String desc = "Fer2 Status: True Positive Pfam 8_8"; sf = new SequenceFeature("Pfam", desc, 8, 83, "Uniprot"); - expected = "
" - + "" + expected = "
TypePfam
Start/end8-83
" + + "" + "" + "
LocationTestSeq8-83
TypePfam
DescriptionFer2 Status: True Positive Pfam 8_8
GroupUniprot
"; - assertEquals(expected, sf.getDetailsReport()); + assertEquals(expected, sf.getDetailsReport(seqName)); } } diff --git a/test/jalview/datamodel/features/SequenceFeaturesTest.java b/test/jalview/datamodel/features/SequenceFeaturesTest.java index 29e76bb..4198a37 100644 --- a/test/jalview/datamodel/features/SequenceFeaturesTest.java +++ b/test/jalview/datamodel/features/SequenceFeaturesTest.java @@ -940,14 +940,14 @@ public class SequenceFeaturesTest assertFalse(iterator.hasNext()); /* - * two types specified - get sorted alphabetically + * two types specified - order is preserved */ types = sf.varargToTypes("Metal", "Cath"); iterator = types.iterator(); assertTrue(iterator.hasNext()); - assertSame(iterator.next(), featureStores.get("Cath")); - assertTrue(iterator.hasNext()); assertSame(iterator.next(), featureStores.get("Metal")); + assertTrue(iterator.hasNext()); + assertSame(iterator.next(), featureStores.get("Cath")); assertFalse(iterator.hasNext()); /* diff --git a/test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java b/test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java index 2c973ca..06a09df 100644 --- a/test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java +++ b/test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java @@ -60,15 +60,15 @@ public class ChimeraCommandsTest { Map map = new LinkedHashMap(); - ChimeraCommands.addColourRange(map, Color.blue, 0, 2, 5, "A"); - ChimeraCommands.addColourRange(map, Color.blue, 0, 7, 7, "B"); - ChimeraCommands.addColourRange(map, Color.blue, 0, 9, 23, "A"); - ChimeraCommands.addColourRange(map, Color.blue, 1, 1, 1, "A"); - ChimeraCommands.addColourRange(map, Color.blue, 1, 4, 7, "B"); - ChimeraCommands.addColourRange(map, Color.yellow, 1, 8, 8, "A"); - ChimeraCommands.addColourRange(map, Color.yellow, 1, 3, 5, "A"); - ChimeraCommands.addColourRange(map, Color.red, 0, 3, 5, "A"); - ChimeraCommands.addColourRange(map, Color.red, 0, 6, 9, "A"); + ChimeraCommands.addAtomSpecRange(map, Color.blue, 0, 2, 5, "A"); + ChimeraCommands.addAtomSpecRange(map, Color.blue, 0, 7, 7, "B"); + ChimeraCommands.addAtomSpecRange(map, Color.blue, 0, 9, 23, "A"); + ChimeraCommands.addAtomSpecRange(map, Color.blue, 1, 1, 1, "A"); + ChimeraCommands.addAtomSpecRange(map, Color.blue, 1, 4, 7, "B"); + ChimeraCommands.addAtomSpecRange(map, Color.yellow, 1, 8, 8, "A"); + ChimeraCommands.addAtomSpecRange(map, Color.yellow, 1, 3, 5, "A"); + ChimeraCommands.addAtomSpecRange(map, Color.red, 0, 3, 5, "A"); + ChimeraCommands.addAtomSpecRange(map, Color.red, 0, 6, 9, "A"); // Colours should appear in the Chimera command in the order in which // they were added; within colour, by model, by chain, ranges in start order @@ -91,7 +91,7 @@ public class ChimeraCommandsTest * start with just one feature/value... */ featuresMap.put("chain", featureValues); - ChimeraCommands.addColourRange(featureValues, "X", 0, 8, 20, "A"); + ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 8, 20, "A"); List commands = ChimeraCommands .buildSetAttributeCommands(featuresMap); @@ -104,24 +104,24 @@ public class ChimeraCommandsTest assertEquals(commands.get(0), "setattr r jv_chain 'X' #0:8-20.A"); // add same feature value, overlapping range - ChimeraCommands.addColourRange(featureValues, "X", 0, 3, 9, "A"); + ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 3, 9, "A"); // same feature value, contiguous range - ChimeraCommands.addColourRange(featureValues, "X", 0, 21, 25, "A"); + ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 21, 25, "A"); commands = ChimeraCommands.buildSetAttributeCommands(featuresMap); assertEquals(1, commands.size()); assertEquals(commands.get(0), "setattr r jv_chain 'X' #0:3-25.A"); // same feature value and model, different chain - ChimeraCommands.addColourRange(featureValues, "X", 0, 21, 25, "B"); + ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 21, 25, "B"); // same feature value and chain, different model - ChimeraCommands.addColourRange(featureValues, "X", 1, 26, 30, "A"); + ChimeraCommands.addAtomSpecRange(featureValues, "X", 1, 26, 30, "A"); commands = ChimeraCommands.buildSetAttributeCommands(featuresMap); assertEquals(1, commands.size()); assertEquals(commands.get(0), "setattr r jv_chain 'X' #0:3-25.A,21-25.B|#1:26-30.A"); // same feature, different value - ChimeraCommands.addColourRange(featureValues, "Y", 0, 40, 50, "A"); + ChimeraCommands.addAtomSpecRange(featureValues, "Y", 0, 40, 50, "A"); commands = ChimeraCommands.buildSetAttributeCommands(featuresMap); assertEquals(2, commands.size()); // commands are ordered by feature type but not by value @@ -133,7 +133,7 @@ public class ChimeraCommandsTest featuresMap.clear(); featureValues.clear(); featuresMap.put("side-chain binding!", featureValues); - ChimeraCommands.addColourRange(featureValues, + ChimeraCommands.addAtomSpecRange(featureValues, "metal 'ion!", 0, 7, 15, "A"); // feature names are sanitised to change non-alphanumeric to underscore diff --git a/test/jalview/gui/AlignFrameTest.java b/test/jalview/gui/AlignFrameTest.java index 3e82547..d2284f1 100644 --- a/test/jalview/gui/AlignFrameTest.java +++ b/test/jalview/gui/AlignFrameTest.java @@ -261,8 +261,11 @@ public class AlignFrameTest /* * apply 30% Conservation to group + * (notice menu action applies to selection group even if mouse click + * is at a sequence not in the group) */ - PopupMenu popupMenu = new PopupMenu(af.alignPanel, null, null); + PopupMenu popupMenu = new PopupMenu(af.alignPanel, al.getSequenceAt(2), + null); popupMenu.changeColour_actionPerformed(JalviewColourScheme.Strand .toString()); assertTrue(sg.getColourScheme() instanceof StrandColourScheme); @@ -540,7 +543,8 @@ public class AlignFrameTest sg.setStartRes(15); sg.setEndRes(25); av.setSelectionGroup(sg); - PopupMenu popupMenu = new PopupMenu(af.alignPanel, null, null); + PopupMenu popupMenu = new PopupMenu(af.alignPanel, al.getSequenceAt(0), + null); popupMenu.changeColour_actionPerformed(JalviewColourScheme.Strand .toString()); assertTrue(sg.getColourScheme() instanceof StrandColourScheme); diff --git a/test/jalview/gui/FeatureSettingsTest.java b/test/jalview/gui/FeatureSettingsTest.java index 6d8a47e..fd4bd4b 100644 --- a/test/jalview/gui/FeatureSettingsTest.java +++ b/test/jalview/gui/FeatureSettingsTest.java @@ -15,6 +15,7 @@ import jalview.io.FileLoader; import jalview.schemes.FeatureColour; import jalview.schemes.FeatureColourTest; import jalview.util.matcher.Condition; +import jalview.viewmodel.seqfeatures.FeatureRendererModel; import java.awt.Color; import java.io.File; @@ -50,7 +51,7 @@ public class FeatureSettingsTest /* * set colour schemes for features */ - FeatureRenderer fr = af.getFeatureRenderer(); + FeatureRendererModel fr = af.getFeatureRenderer(); // type1: red fr.setColour("type1", new FeatureColour(Color.red)); diff --git a/test/jalview/gui/PopupMenuTest.java b/test/jalview/gui/PopupMenuTest.java index df30935..bf961d8 100644 --- a/test/jalview/gui/PopupMenuTest.java +++ b/test/jalview/gui/PopupMenuTest.java @@ -91,6 +91,8 @@ public class PopupMenuTest public void setUp() throws IOException { Cache.loadProperties("test/jalview/io/testProps.jvprops"); + Cache.initLogger(); + String inMenuString = ("EMBL-EBI Search | http://www.ebi.ac.uk/ebisearch/search.ebi?db=allebi&query=$" + SEQUENCE_ID + "$" @@ -113,7 +115,7 @@ public class PopupMenuTest DataSourceType.PASTE, FileFormat.Fasta); AlignFrame af = new AlignFrame(alignment, 700, 500); parentPanel = new AlignmentPanel(af, af.getViewport()); - testee = new PopupMenu(parentPanel, null, null); + testee = new PopupMenu(parentPanel, alignment.getSequenceAt(0), null); int i = 0; for (SequenceI seq : alignment.getSequences()) { @@ -534,7 +536,6 @@ public class PopupMenuTest * note dbref GENE3D is matched to link Gene3D, the latter is displayed */ linkMenu = PopupMenu.buildLinkMenu(seq1, noFeatures); - assertEquals(linkText, linkMenu.getText()); linkItems = linkMenu.getMenuComponents(); assertEquals(3, linkItems.length); assertEquals("EMBL-EBI Search", ((JMenuItem) linkItems[0]).getText()); @@ -553,10 +554,31 @@ public class PopupMenuTest Preferences.sequenceUrlLinks = factory.createUrlProvider(); linkMenu = PopupMenu.buildLinkMenu(seq1, noFeatures); - assertEquals(linkText, linkMenu.getText()); linkItems = linkMenu.getMenuComponents(); assertEquals(1, linkItems.length); assertEquals("EMBL-EBI Search", ((JMenuItem) linkItems[0]).getText()); + + /* + * if sequence is null, only feature links are shown (alignment popup submenu) + */ + linkMenu = PopupMenu.buildLinkMenu(null, noFeatures); + linkItems = linkMenu.getMenuComponents(); + assertEquals(0, linkItems.length); + + List features = new ArrayList<>(); + SequenceFeature sf = new SequenceFeature("type", "desc", 1, 20, null); + features.add(sf); + linkMenu = PopupMenu.buildLinkMenu(null, features); + linkItems = linkMenu.getMenuComponents(); + assertEquals(0, linkItems.length); // feature has no links + + sf.addLink("Pfam family|http://pfam.xfam.org/family/PF00111"); + linkMenu = PopupMenu.buildLinkMenu(null, features); + linkItems = linkMenu.getMenuComponents(); + assertEquals(1, linkItems.length); + JMenuItem item = (JMenuItem) linkItems[0]; + assertEquals("Pfam family", item.getText()); + // ? no way to verify URL, compiled into link's actionListener } @Test(groups = { "Functional" }) diff --git a/test/jalview/io/FeaturesFileTest.java b/test/jalview/io/FeaturesFileTest.java index 77c18db..090de6f 100644 --- a/test/jalview/io/FeaturesFileTest.java +++ b/test/jalview/io/FeaturesFileTest.java @@ -476,7 +476,7 @@ public class FeaturesFileTest */ FeatureRenderer fr = af.alignPanel.getFeatureRenderer(); String exported = featuresFile - .printJalviewFormat(al.getSequencesArray(), fr, false); + .printJalviewFormat(al.getSequencesArray(), fr, false, false); String expected = "No Features Visible"; assertEquals(expected, exported); @@ -485,7 +485,7 @@ public class FeaturesFileTest */ fr.setGroupVisibility("uniprot", true); exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr, - true); + true, false); expected = "\nSTARTGROUP\tuniprot\n" + "Cath\tFER_CAPAA\t-1\t0\t0\tDomain\t0.0\n" + "ENDGROUP\tuniprot\n\n" @@ -499,12 +499,12 @@ public class FeaturesFileTest fr.setVisible("METAL"); fr.setVisible("GAMMA-TURN"); exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr, - false); + false, false); expected = "METAL\tcc9900\n" + "GAMMA-TURN\tscore|ff0000|00ffff|noValueMin|20.0|95.0|below|66.0\n" + "\nSTARTGROUP\tuniprot\n" - + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n" + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\t0.0\n" + + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n" + "ENDGROUP\tuniprot\n"; assertEquals(expected, exported); @@ -513,7 +513,7 @@ public class FeaturesFileTest */ fr.setVisible("Pfam"); exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr, - false); + false, false); /* * features are output within group, ordered by sequence and type */ @@ -521,9 +521,9 @@ public class FeaturesFileTest + "Pfam\tff0000\n" + "GAMMA-TURN\tscore|ff0000|00ffff|noValueMin|20.0|95.0|below|66.0\n" + "\nSTARTGROUP\tuniprot\n" - + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n" + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\t0.0\n" + "Pfam domainPfam_3_4\tFER_CAPAA\t-1\t20\t20\tPfam\t0.0\n" + + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n" + "ENDGROUP\tuniprot\n" // null / empty group features are output after named groups + "\ndesc2\tFER_CAPAN\t-1\t4\t9\tPfam\n" @@ -539,14 +539,14 @@ public class FeaturesFileTest + "\ndesc2\tFER_CAPAN\t-1\t4\t9\tPfam\n" + "\ndesc4\tFER1_SOLLC\t-1\t5\t8\tPfam\t-2.6\n"; exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr, - false); + false, false); assertEquals(expected, exported); /* * include non-positional (overrides group not shown) */ exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr, - true); + true, false); expected = "METAL\tcc9900\n" + "Pfam\tff0000\n" + "GAMMA-TURN\tscore|ff0000|00ffff|noValueMin|20.0|95.0|below|66.0\n" + "\nSTARTGROUP\tuniprot\n" @@ -573,11 +573,11 @@ public class FeaturesFileTest FeatureRendererModel fr = (FeatureRendererModel) af.alignPanel .getFeatureRenderer(); String exported = featuresFile.printGffFormat(al.getSequencesArray(), - fr, false); + fr, false, false); String gffHeader = "##gff-version 2\n"; assertEquals(gffHeader, exported); exported = featuresFile.printGffFormat(al.getSequencesArray(), fr, - true); + true, false); assertEquals(gffHeader, exported); /* @@ -614,7 +614,7 @@ public class FeaturesFileTest * with no features displayed, exclude non-positional features */ exported = featuresFile.printGffFormat(al.getSequencesArray(), fr, - false); + false, false); assertEquals(gffHeader, exported); /* @@ -623,7 +623,7 @@ public class FeaturesFileTest fr.setGroupVisibility("Uniprot", true); fr.setGroupVisibility("s3dm", false); exported = featuresFile.printGffFormat(al.getSequencesArray(), fr, - true); + true, false); String expected = gffHeader + "FER_CAPAA\tUniprot\tDomain\t0\t0\t0.0\t.\t.\n"; assertEquals(expected, exported); @@ -635,7 +635,7 @@ public class FeaturesFileTest fr.setVisible("METAL"); fr.setVisible("GAMMA-TURN"); exported = featuresFile.printGffFormat(al.getSequencesArray(), fr, - false); + false, false); // METAL feature has null group: description used for column 2 expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n"; assertEquals(expected, exported); @@ -645,7 +645,7 @@ public class FeaturesFileTest */ fr.setGroupVisibility("s3dm", true); exported = featuresFile.printGffFormat(al.getSequencesArray(), fr, - false); + false, false); // METAL feature has null group: description used for column 2 expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n" + "FER_CAPAN\ts3dm\tGAMMA-TURN\t36\t38\t2.1\t.\t.\n"; @@ -656,12 +656,12 @@ public class FeaturesFileTest */ fr.setVisible("Pfam"); exported = featuresFile.printGffFormat(al.getSequencesArray(), fr, - false); + false, false); // Pfam feature columns include strand(+), phase(2), attributes expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n" - + "FER_CAPAN\ts3dm\tGAMMA-TURN\t36\t38\t2.1\t.\t.\n" - + "FER_CAPAN\tUniprot\tPfam\t20\t20\t0.0\t+\t2\tx=y;black=white\n"; + + "FER_CAPAN\tUniprot\tPfam\t20\t20\t0.0\t+\t2\tx=y;black=white\n" + + "FER_CAPAN\ts3dm\tGAMMA-TURN\t36\t38\t2.1\t.\t.\n"; assertEquals(expected, exported); } @@ -770,7 +770,7 @@ public class FeaturesFileTest fr.setVisible("METAL"); fr.setColour("METAL", new FeatureColour(Color.PINK)); String exported = featuresFile.printGffFormat(al.getSequencesArray(), - fr, false); + fr, false, false); String expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n" + "FER_CAPAA\tCath\tMETAL\t41\t41\t0.6\t.\t.\n"; @@ -785,7 +785,7 @@ public class FeaturesFileTest fc.setThreshold(1.1f); fr.setColour("METAL", fc); exported = featuresFile.printGffFormat(al.getSequencesArray(), fr, - false); + false, false); expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n"; assertEquals(expected, exported); @@ -794,7 +794,7 @@ public class FeaturesFileTest */ fc.setAboveThreshold(false); exported = featuresFile.printGffFormat(al.getSequencesArray(), fr, - false); + false, false); expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n" + "FER_CAPAA\tCath\tMETAL\t41\t41\t0.6\t.\t.\n"; assertEquals(expected, exported); @@ -807,7 +807,7 @@ public class FeaturesFileTest "clin_sig")); fr.setFeatureFilter("METAL", filter); exported = featuresFile.printGffFormat(al.getSequencesArray(), fr, - false); + false, false); expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t41\t41\t0.6\t.\t.\n"; assertEquals(expected, exported); } @@ -843,7 +843,7 @@ public class FeaturesFileTest fr.setColour("METAL", new FeatureColour(Color.PINK)); String exported = featuresFile.printJalviewFormat( al.getSequencesArray(), - fr, false); + fr, false, false); String expected = "METAL\tffafaf\n\nSTARTGROUP\tgrp1\n" + "Cath\tFER_CAPAA\t-1\t39\t39\tMETAL\t1.2\n" + "ENDGROUP\tgrp1\n\nSTARTGROUP\tgrp2\n" @@ -861,7 +861,7 @@ public class FeaturesFileTest fc.setThreshold(1.1f); fr.setColour("METAL", fc); exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr, - false); + false, false); expected = "METAL\tscore|ffffff|000000|noValueMin|abso|0.0|2.0|above|1.1\n\n" + "STARTGROUP\tgrp1\n" + "Cath\tFER_CAPAA\t-1\t39\t39\tMETAL\t1.2\n" @@ -873,7 +873,7 @@ public class FeaturesFileTest */ fc.setAboveThreshold(false); exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr, - false); + false, false); expected = "METAL\tscore|ffffff|000000|noValueMin|abso|0.0|2.0|none\n\n" + "STARTGROUP\tgrp1\n" + "Cath\tFER_CAPAA\t-1\t39\t39\tMETAL\t1.2\n" @@ -890,7 +890,7 @@ public class FeaturesFileTest "clin_sig")); fr.setFeatureFilter("METAL", filter); exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr, - false); + false, false); expected = "FER_CAPAA\tCath\tMETAL\t41\t41\t0.6\t.\t.\n"; expected = "METAL\tscore|ffffff|000000|noValueMin|abso|0.0|2.0|none\n\n" + "STARTFILTERS\nMETAL\tclin_sig Contains benign\nENDFILTERS\n\n" diff --git a/test/jalview/io/SequenceAnnotationReportTest.java b/test/jalview/io/SequenceAnnotationReportTest.java index cf3c7e5..0b5dfdd 100644 --- a/test/jalview/io/SequenceAnnotationReportTest.java +++ b/test/jalview/io/SequenceAnnotationReportTest.java @@ -62,17 +62,17 @@ public class SequenceAnnotationReportTest 3, 1.2f, "group"); // residuePos == 2 does not match start or end of feature, nothing done: - sar.appendFeature(sb, 2, null, sf); + sar.appendFeature(sb, 2, null, sf, null); assertEquals("123456", sb.toString()); // residuePos == 1 matches start of feature, text appended (but no
) // feature score is not included - sar.appendFeature(sb, 1, null, sf); + sar.appendFeature(sb, 1, null, sf, null); assertEquals("123456disulfide bond 1:3", sb.toString()); // residuePos == 3 matches end of feature, text appended //
is prefixed once sb.length() > 6 - sar.appendFeature(sb, 3, null, sf); + sar.appendFeature(sb, 3, null, sf, null); assertEquals("123456disulfide bond 1:3
disulfide bond 1:3", sb.toString()); } @@ -86,7 +86,7 @@ public class SequenceAnnotationReportTest Float.NaN, "group"); sf.setStatus("Confirmed"); - sar.appendFeature(sb, 1, null, sf); + sar.appendFeature(sb, 1, null, sf, null); assertEquals("METAL 1 3; Fe2-S; (Confirmed)", sb.toString()); } @@ -100,7 +100,7 @@ public class SequenceAnnotationReportTest FeatureRendererModel fr = new FeatureRenderer(null); Map minmax = fr.getMinMax(); - sar.appendFeature(sb, 1, fr, sf); + sar.appendFeature(sb, 1, fr, sf, null); /* * map has no entry for this feature type - score is not shown: */ @@ -110,7 +110,7 @@ public class SequenceAnnotationReportTest * map has entry for this feature type - score is shown: */ minmax.put("METAL", new float[][] { { 0f, 1f }, null }); - sar.appendFeature(sb, 1, fr, sf); + sar.appendFeature(sb, 1, fr, sf, null); //
is appended to a buffer > 6 in length assertEquals("METAL 1 3; Fe2-S
METAL 1 3; Fe2-S Score=1.3", sb.toString()); @@ -120,7 +120,7 @@ public class SequenceAnnotationReportTest */ minmax.put("METAL", new float[][] { { 2f, 2f }, null }); sb.setLength(0); - sar.appendFeature(sb, 1, fr, sf); + sar.appendFeature(sb, 1, fr, sf, null); assertEquals("METAL 1 3; Fe2-S", sb.toString()); } @@ -132,7 +132,7 @@ public class SequenceAnnotationReportTest SequenceFeature sf = new SequenceFeature("METAL", "Fe2-S", 1, 3, Float.NaN, "group"); - sar.appendFeature(sb, 1, null, sf); + sar.appendFeature(sb, 1, null, sf, null); assertEquals("METAL 1 3; Fe2-S", sb.toString()); } @@ -152,7 +152,7 @@ public class SequenceAnnotationReportTest * first with no colour by attribute */ FeatureRendererModel fr = new FeatureRenderer(null); - sar.appendFeature(sb, 1, fr, sf); + sar.appendFeature(sb, 1, fr, sf, null); assertEquals("METAL 1 3; Fe2-S", sb.toString()); /* @@ -163,7 +163,7 @@ public class SequenceAnnotationReportTest fc.setAttributeName("Pfam"); fr.setColour("METAL", fc); sb.setLength(0); - sar.appendFeature(sb, 1, fr, sf); + sar.appendFeature(sb, 1, fr, sf, null); assertEquals("METAL 1 3; Fe2-S", sb.toString()); // no change /* @@ -171,7 +171,7 @@ public class SequenceAnnotationReportTest */ fc.setAttributeName("clinical_significance"); sb.setLength(0); - sar.appendFeature(sb, 1, fr, sf); + sar.appendFeature(sb, 1, fr, sf, null); assertEquals("METAL 1 3; Fe2-S; clinical_significance=Benign", sb.toString()); } @@ -193,7 +193,7 @@ public class SequenceAnnotationReportTest fc.setAttributeName("clinical_significance"); fr.setColour("METAL", fc); minmax.put("METAL", new float[][] { { 0f, 1f }, null }); - sar.appendFeature(sb, 1, fr, sf); + sar.appendFeature(sb, 1, fr, sf, null); assertEquals( "METAL 1 3; Fe2-S Score=1.3; (Confirmed); clinical_significance=Benign", @@ -209,13 +209,13 @@ public class SequenceAnnotationReportTest Float.NaN, "group"); // description is not included if it duplicates type: - sar.appendFeature(sb, 1, null, sf); + sar.appendFeature(sb, 1, null, sf, null); assertEquals("METAL 1 3", sb.toString()); sb.setLength(0); sf.setDescription("Metal"); // test is case-sensitive: - sar.appendFeature(sb, 1, null, sf); + sar.appendFeature(sb, 1, null, sf, null); assertEquals("METAL 1 3; Metal", sb.toString()); } @@ -228,13 +228,13 @@ public class SequenceAnnotationReportTest "helloworld", 1, 3, Float.NaN, "group"); - sar.appendFeature(sb, 1, null, sf); + sar.appendFeature(sb, 1, null, sf, null); // !! strips off but not ?? assertEquals("METAL 1 3; helloworld", sb.toString()); sb.setLength(0); sf.setDescription("
&kHD>6"); - sar.appendFeature(sb, 1, null, sf); + sar.appendFeature(sb, 1, null, sf, null); // if no tag, html-encodes > and < (only): assertEquals("METAL 1 3; <br>&kHD>6", sb.toString()); } diff --git a/test/jalview/io/vcf/VCFLoaderTest.java b/test/jalview/io/vcf/VCFLoaderTest.java index a87c160..fb7a4e4 100644 --- a/test/jalview/io/vcf/VCFLoaderTest.java +++ b/test/jalview/io/vcf/VCFLoaderTest.java @@ -1,6 +1,7 @@ package jalview.io.vcf; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; import jalview.bin.Cache; import jalview.datamodel.AlignmentI; @@ -178,13 +179,18 @@ public class VCFLoaderTest } } List proteinFeatures = peptide.getSequenceFeatures(); - assertEquals(proteinFeatures.size(), 1); - sf = proteinFeatures.get(0); - assertEquals(sf.getFeatureGroup(), "VCF"); - assertEquals(sf.getBegin(), 1); - assertEquals(sf.getEnd(), 1); - assertEquals(sf.getType(), SequenceOntologyI.NONSYNONYMOUS_VARIANT); - assertEquals(sf.getDescription(), "p.Ser1Thr"); + + /* + * JAL-3187 don't precompute protein features, do dynamically instead + */ + assertTrue(proteinFeatures.isEmpty()); + // assertEquals(proteinFeatures.size(), 1); + // sf = proteinFeatures.get(0); + // assertEquals(sf.getFeatureGroup(), "VCF"); + // assertEquals(sf.getBegin(), 1); + // assertEquals(sf.getEnd(), 1); + // assertEquals(sf.getType(), SequenceOntologyI.NONSYNONYMOUS_VARIANT); + // assertEquals(sf.getDescription(), "p.Ser1Thr"); } private File makeVcf() throws IOException @@ -445,13 +451,17 @@ public class VCFLoaderTest } } List proteinFeatures = peptide.getSequenceFeatures(); - assertEquals(proteinFeatures.size(), 1); - sf = proteinFeatures.get(0); - assertEquals(sf.getFeatureGroup(), "VCF"); - assertEquals(sf.getBegin(), 6); - assertEquals(sf.getEnd(), 6); - assertEquals(sf.getType(), SequenceOntologyI.NONSYNONYMOUS_VARIANT); - assertEquals(sf.getDescription(), "p.Ala6Gly"); + /* + * JAL-3187 don't precompute protein features, do dynamically instead + */ + assertTrue(proteinFeatures.isEmpty()); + // assertEquals(proteinFeatures.size(), 1); + // sf = proteinFeatures.get(0); + // assertEquals(sf.getFeatureGroup(), "VCF"); + // assertEquals(sf.getBegin(), 6); + // assertEquals(sf.getEnd(), 6); + // assertEquals(sf.getType(), SequenceOntologyI.NONSYNONYMOUS_VARIANT); + // assertEquals(sf.getDescription(), "p.Ala6Gly"); } /** @@ -494,6 +504,7 @@ public class VCFLoaderTest // gene features include Consequence for all transcripts Map map = (Map) sf.getValue("CSQ"); assertEquals(map.size(), 9); + assertEquals(map.get("PolyPhen"), "Bad"); sf = geneFeatures.get(1); assertEquals(sf.getBegin(), 5); @@ -503,6 +514,7 @@ public class VCFLoaderTest assertEquals(sf.getValue("alleles"), "C,T"); map = (Map) sf.getValue("CSQ"); assertEquals(map.size(), 9); + assertEquals(map.get("PolyPhen"), "Bad++"); // %3B%3B decoded sf = geneFeatures.get(2); assertEquals(sf.getBegin(), 9); @@ -605,20 +617,24 @@ public class VCFLoaderTest } } List proteinFeatures = peptide.getSequenceFeatures(); - SequenceFeatures.sortFeatures(proteinFeatures, true); - assertEquals(proteinFeatures.size(), 2); - sf = proteinFeatures.get(0); - assertEquals(sf.getFeatureGroup(), "VCF"); - assertEquals(sf.getBegin(), 1); - assertEquals(sf.getEnd(), 1); - assertEquals(sf.getType(), SequenceOntologyI.SYNONYMOUS_VARIANT); - assertEquals(sf.getDescription(), "agC/agT"); - sf = proteinFeatures.get(1); - assertEquals(sf.getFeatureGroup(), "VCF"); - assertEquals(sf.getBegin(), 4); - assertEquals(sf.getEnd(), 4); - assertEquals(sf.getType(), SequenceOntologyI.NONSYNONYMOUS_VARIANT); - assertEquals(sf.getDescription(), "p.Glu4Gly"); + /* + * JAL-3187 don't precompute protein features, do dynamically instead + */ + assertTrue(proteinFeatures.isEmpty()); + // SequenceFeatures.sortFeatures(proteinFeatures, true); + // assertEquals(proteinFeatures.size(), 2); + // sf = proteinFeatures.get(0); + // assertEquals(sf.getFeatureGroup(), "VCF"); + // assertEquals(sf.getBegin(), 1); + // assertEquals(sf.getEnd(), 1); + // assertEquals(sf.getType(), SequenceOntologyI.SYNONYMOUS_VARIANT); + // assertEquals(sf.getDescription(), "agC/agT"); + // sf = proteinFeatures.get(1); + // assertEquals(sf.getFeatureGroup(), "VCF"); + // assertEquals(sf.getBegin(), 4); + // assertEquals(sf.getEnd(), 4); + // assertEquals(sf.getType(), SequenceOntologyI.NONSYNONYMOUS_VARIANT); + // assertEquals(sf.getDescription(), "p.Glu4Gly"); /* * verify variant feature(s) added to transcript4 diff --git a/test/jalview/io/vcf/testVcf.dat b/test/jalview/io/vcf/testVcf.dat deleted file mode 100644 index 77e070c..0000000 --- a/test/jalview/io/vcf/testVcf.dat +++ /dev/null @@ -1,13 +0,0 @@ -##fileformat=VCFv4.2 -##INFO= -##INFO= -##INFO= -##INFO= -##INFO= -##reference=/Homo_sapiens/GRCh38 -#CHROM POS ID REF ALT QUAL FILTER INFO -5 45051610 . C A 81.96 RF;AC0 AC=1;AF=0.1;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=A|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,A|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad -5 45051614 . C T 1666.64 RF AC=1;AF=0.2;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=T|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,T|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad -5 45051618 . CGG C 41.94 AC0 AC=1;AF=0.3;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=C|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,C|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,CSQ=CGT|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,CGT|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad -5 45051622 . C G,T 224.23 RF;AC0 AC=1,2;AF=0.4,0.5;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=G|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,G|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,T|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,T|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad -5 45051626 . A AC,G 433.35 RF;AC0 AC=3,4;AF=0.6,0.7;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=G|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,G|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,AC|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,AC|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad diff --git a/test/jalview/io/vcf/testVcf.vcf b/test/jalview/io/vcf/testVcf.vcf index 77e070c..8a16a90 100644 --- a/test/jalview/io/vcf/testVcf.vcf +++ b/test/jalview/io/vcf/testVcf.vcf @@ -7,7 +7,7 @@ ##reference=/Homo_sapiens/GRCh38 #CHROM POS ID REF ALT QUAL FILTER INFO 5 45051610 . C A 81.96 RF;AC0 AC=1;AF=0.1;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=A|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,A|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad -5 45051614 . C T 1666.64 RF AC=1;AF=0.2;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=T|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,T|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad +5 45051614 . C T 1666.64 RF AC=1;AF=0.2;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=T|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,T|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad%2B%2B 5 45051618 . CGG C 41.94 AC0 AC=1;AF=0.3;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=C|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,C|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,CSQ=CGT|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,CGT|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad 5 45051622 . C G,T 224.23 RF;AC0 AC=1,2;AF=0.4,0.5;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=G|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,G|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,T|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,T|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad 5 45051626 . A AC,G 433.35 RF;AC0 AC=3,4;AF=0.6,0.7;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=G|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,G|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,AC|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,AC|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad diff --git a/test/jalview/project/Jalview2xmlTests.java b/test/jalview/project/Jalview2xmlTests.java index 5f1256c..5182ad4 100644 --- a/test/jalview/project/Jalview2xmlTests.java +++ b/test/jalview/project/Jalview2xmlTests.java @@ -51,7 +51,6 @@ import jalview.gui.AlignFrame; import jalview.gui.AlignViewport; import jalview.gui.AlignmentPanel; import jalview.gui.Desktop; -import jalview.gui.FeatureRenderer; import jalview.gui.JvOptionPane; import jalview.gui.PCAPanel; import jalview.gui.PopupMenu; @@ -74,6 +73,7 @@ import jalview.structure.StructureImportSettings; import jalview.util.MapList; import jalview.util.matcher.Condition; import jalview.viewmodel.AlignmentViewport; +import jalview.viewmodel.seqfeatures.FeatureRendererModel; import java.awt.Color; import java.io.File; @@ -842,13 +842,16 @@ public class Jalview2xmlTests extends Jalview2xmlBase /* * create a group with Strand colouring, 30% Conservation * and 40% PID threshold + * (notice menu action applies to selection group even if mouse click + * is at a sequence not in the group) */ SequenceGroup sg = new SequenceGroup(); sg.addSequence(al.getSequenceAt(0), false); sg.setStartRes(15); sg.setEndRes(25); av.setSelectionGroup(sg); - PopupMenu popupMenu = new PopupMenu(af.alignPanel, null, null); + PopupMenu popupMenu = new PopupMenu(af.alignPanel, al.getSequenceAt(2), + null); popupMenu.changeColour_actionPerformed( JalviewColourScheme.Strand.toString()); assertTrue(sg.getColourScheme() instanceof StrandColourScheme); @@ -924,7 +927,7 @@ public class Jalview2xmlTests extends Jalview2xmlBase /* * set colour schemes for features */ - FeatureRenderer fr = af.getFeatureRenderer(); + FeatureRendererModel fr = af.getFeatureRenderer(); fr.findAllFeatures(true); // type1: red diff --git a/test/jalview/renderer/seqfeatures/FeatureColourFinderTest.java b/test/jalview/renderer/seqfeatures/FeatureColourFinderTest.java index af7c2ed..6b1fbd2 100644 --- a/test/jalview/renderer/seqfeatures/FeatureColourFinderTest.java +++ b/test/jalview/renderer/seqfeatures/FeatureColourFinderTest.java @@ -11,10 +11,10 @@ import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; import jalview.gui.AlignFrame; import jalview.gui.AlignViewport; -import jalview.gui.FeatureRenderer; import jalview.io.DataSourceType; import jalview.io.FileLoader; import jalview.schemes.FeatureColour; +import jalview.viewmodel.seqfeatures.FeatureRendererModel; import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean; import java.awt.Color; @@ -51,7 +51,7 @@ public class FeatureColourFinderTest private AlignFrame af; - private FeatureRenderer fr; + private FeatureRendererModel fr; @BeforeTest(alwaysRun = true) public void setUp()