From 5a631296dd1dcc1df7b50487a647c27333696c74 Mon Sep 17 00:00:00 2001 From: gmungoc Date: Thu, 16 Apr 2020 16:11:23 +0100 Subject: [PATCH] JAL-3567 report mapped location for virtual features in tooltip etc --- src/jalview/appletgui/APopupMenu.java | 40 +++--- src/jalview/datamodel/MappedFeatures.java | 84 +++++++++--- src/jalview/datamodel/SequenceFeature.java | 40 ++++-- src/jalview/gui/IdPanel.java | 27 ++-- src/jalview/gui/PopupMenu.java | 99 +++++++------- src/jalview/gui/SeqPanel.java | 16 +-- src/jalview/io/FeaturesFile.java | 29 ++--- src/jalview/io/SequenceAnnotationReport.java | 136 ++++++++++++-------- .../seqfeatures/FeatureRendererModel.java | 51 ++++---- test/jalview/datamodel/SequenceFeatureTest.java | 12 +- test/jalview/io/SequenceAnnotationReportTest.java | 75 ++++++----- 11 files changed, 361 insertions(+), 248 deletions(-) diff --git a/src/jalview/appletgui/APopupMenu.java b/src/jalview/appletgui/APopupMenu.java index 76f2705..6299c62 100644 --- a/src/jalview/appletgui/APopupMenu.java +++ b/src/jalview/appletgui/APopupMenu.java @@ -20,6 +20,25 @@ */ package jalview.appletgui; +import java.awt.CheckboxMenuItem; +import java.awt.Frame; +import java.awt.Menu; +import java.awt.MenuItem; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.Vector; + import jalview.analysis.AAFrequency; import jalview.analysis.AlignmentAnnotationUtils; import jalview.analysis.AlignmentUtils; @@ -57,25 +76,6 @@ import jalview.schemes.ZappoColourScheme; import jalview.util.MessageManager; import jalview.util.UrlLink; -import java.awt.CheckboxMenuItem; -import java.awt.Frame; -import java.awt.Menu; -import java.awt.MenuItem; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.ItemEvent; -import java.awt.event.ItemListener; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.SortedMap; -import java.util.TreeMap; -import java.util.Vector; - public class APopupMenu extends java.awt.PopupMenu implements ActionListener, ItemListener { @@ -900,7 +900,7 @@ public class APopupMenu extends java.awt.PopupMenu contents.append(MessageManager .formatMessage("label.annotation_for_displayid", new Object[] { seq.getDisplayId(true) })); - new SequenceAnnotationReport(null).createSequenceAnnotationReport( + new SequenceAnnotationReport(false).createSequenceAnnotationReport( contents, seq, true, true, ap.seqPanel.seqCanvas.fr); contents.append("

"); } diff --git a/src/jalview/datamodel/MappedFeatures.java b/src/jalview/datamodel/MappedFeatures.java index 0fa03cf..a42f34a 100644 --- a/src/jalview/datamodel/MappedFeatures.java +++ b/src/jalview/datamodel/MappedFeatures.java @@ -1,14 +1,15 @@ package jalview.datamodel; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + import jalview.io.gff.Gff3Helper; import jalview.schemes.ResidueProperties; +import jalview.util.MapList; 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 @@ -18,22 +19,31 @@ import java.util.Set; */ public class MappedFeatures { + /* + * VEP CSQ:HGVSp (if present) is a short-cut to the protein variant consequence + */ private static final String HGV_SP = "HGVSp"; private static final String CSQ = "CSQ"; /* - * the mapping from one sequence to another + * the sequence the mapped features are on */ - public final Mapping mapping; + private final SequenceI linkedSeq; - /** - * the sequence mapped from + /* + * the mapping between sequences; + * NB this could be in either sense */ - public final SequenceI fromSeq; + private final Mapping mapping; /* - * features on the sequence mapped to that overlap the mapped positions + * if true, mapping is from the linked sequence, else to the linked sequence + */ + private boolean mappingIsFromLinkedSequence; + + /* + * features on linkedSeq that overlap the mapped positions */ public final List features; @@ -74,7 +84,8 @@ public class MappedFeatures char res, List theFeatures) { mapping = theMapping; - fromSeq = from; + linkedSeq = from; + mappingIsFromLinkedSequence = mapping.to != linkedSeq; toPosition = pos; toResidue = res; features = theFeatures; @@ -90,13 +101,13 @@ public class MappedFeatures { codonPos = codonPositions; baseCodon = new char[3]; - int cdsStart = fromSeq.getStart(); + int cdsStart = linkedSeq.getStart(); baseCodon[0] = Character - .toUpperCase(fromSeq.getCharAt(codonPos[0] - cdsStart)); + .toUpperCase(linkedSeq.getCharAt(codonPos[0] - cdsStart)); baseCodon[1] = Character - .toUpperCase(fromSeq.getCharAt(codonPos[1] - cdsStart)); + .toUpperCase(linkedSeq.getCharAt(codonPos[1] - cdsStart)); baseCodon[2] = Character - .toUpperCase(fromSeq.getCharAt(codonPos[2] - cdsStart)); + .toUpperCase(linkedSeq.getCharAt(codonPos[2] - cdsStart)); } else { @@ -233,4 +244,47 @@ public class MappedFeatures return vars.toString(); } + + /** + * Answers the name of the linked sequence holding any mapped features + * + * @return + */ + public String getLinkedSequenceName() + { + return linkedSeq == null ? null : linkedSeq.getName(); + } + + /** + * Answers the mapped ranges (as one or more [start, end] positions) which + * correspond to the given [begin, end] range of the linked sequence. + * + *
+   * Example: MappedFeatures with CDS features mapped to peptide 
+   * CDS/200-220 gtc aac TGa acGt att AAC tta
+   * mapped to PEP/6-7 WN by mapping [206, 207, 210, 210, 215, 217] to [6, 7]
+   * getMappedPositions(206, 206) should return [6, 6]
+   * getMappedPositions(200, 214) should return [6, 6]
+   * getMappedPositions(210, 215) should return [6, 7]
+   * 
+ * + * @param begin + * @param end + * @return + */ + public int[] getMappedPositions(int begin, int end) + { + MapList map = mapping.getMap(); + return mappingIsFromLinkedSequence ? map.locateInTo(begin, end) + : map.locateInFrom(begin, end); + } + + public boolean isFromCds() + { + if (mapping.getMap().getFromRatio() == 3) + { + return mappingIsFromLinkedSequence; + } + return !mappingIsFromLinkedSequence; + } } diff --git a/src/jalview/datamodel/SequenceFeature.java b/src/jalview/datamodel/SequenceFeature.java index 2dd9cf0..df268f8 100755 --- a/src/jalview/datamodel/SequenceFeature.java +++ b/src/jalview/datamodel/SequenceFeature.java @@ -20,13 +20,6 @@ */ package jalview.datamodel; -import jalview.datamodel.features.FeatureAttributeType; -import jalview.datamodel.features.FeatureAttributes; -import jalview.datamodel.features.FeatureLocationI; -import jalview.datamodel.features.FeatureSourceI; -import jalview.datamodel.features.FeatureSources; -import jalview.util.StringUtils; - import java.util.Comparator; import java.util.LinkedHashMap; import java.util.Map; @@ -35,6 +28,13 @@ import java.util.SortedMap; import java.util.TreeMap; import java.util.Vector; +import jalview.datamodel.features.FeatureAttributeType; +import jalview.datamodel.features.FeatureAttributes; +import jalview.datamodel.features.FeatureLocationI; +import jalview.datamodel.features.FeatureSourceI; +import jalview.datamodel.features.FeatureSources; +import jalview.util.StringUtils; + /** * A class that models a single contiguous feature on a sequence. If flag * 'contactFeature' is true, the start and end positions are interpreted instead @@ -586,13 +586,17 @@ public class SequenceFeature implements FeatureLocationI } /** - * Answers an html-formatted report of feature details + * Answers an html-formatted report of feature details. If parameter + * {@code mf} is not null, the feature is a virtual linked feature, and + * details included both the original location and the mapped location + * (CDS/peptide). * * @param seqName + * @param mf * * @return */ - public String getDetailsReport(String seqName) + public String getDetailsReport(String seqName, MappedFeatures mf) { FeatureSourceI metadata = FeatureSources.getInstance() .getSource(source); @@ -600,9 +604,25 @@ public class SequenceFeature implements FeatureLocationI StringBuilder sb = new StringBuilder(128); sb.append("
"); sb.append(""); - sb.append(String.format(ROW_DATA, "Location", seqName, + String name = mf == null ? seqName : mf.getLinkedSequenceName(); + sb.append(String.format(ROW_DATA, "Location", name, begin == end ? begin : begin + (isContactFeature() ? ":" : "-") + end)); + if (mf != null) + { + int[] beginRange = mf.getMappedPositions(begin, begin); + int[] endRange = mf.getMappedPositions(end, end); + int from = beginRange[0]; + int to = endRange[endRange.length - 1]; + String s = mf.isFromCds() ? "Peptide Location" : "Coding location"; + sb.append(String.format(ROW_DATA, s, seqName, from == to ? from + : from + (isContactFeature() ? ":" : "-") + to)); + if (mf.isFromCds()) + { + sb.append(String.format(ROW_DATA, "Consequence", + mf.findProteinVariants(this), "")); + } + } sb.append(String.format(ROW_DATA, "Type", type, "")); String desc = StringUtils.stripHtmlTags(description); sb.append(String.format(ROW_DATA, "Description", desc, "")); diff --git a/src/jalview/gui/IdPanel.java b/src/jalview/gui/IdPanel.java index 2b1507a..3ce9d4d 100755 --- a/src/jalview/gui/IdPanel.java +++ b/src/jalview/gui/IdPanel.java @@ -20,17 +20,6 @@ */ package jalview.gui; -import jalview.datamodel.AlignmentAnnotation; -import jalview.datamodel.Sequence; -import jalview.datamodel.SequenceGroup; -import jalview.datamodel.SequenceI; -import jalview.gui.SeqPanel.MousePos; -import jalview.io.SequenceAnnotationReport; -import jalview.util.MessageManager; -import jalview.util.Platform; -import jalview.viewmodel.AlignmentViewport; -import jalview.viewmodel.ViewportRanges; - import java.awt.BorderLayout; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; @@ -44,6 +33,17 @@ import javax.swing.JPopupMenu; import javax.swing.SwingUtilities; import javax.swing.ToolTipManager; +import jalview.datamodel.AlignmentAnnotation; +import jalview.datamodel.Sequence; +import jalview.datamodel.SequenceGroup; +import jalview.datamodel.SequenceI; +import jalview.gui.SeqPanel.MousePos; +import jalview.io.SequenceAnnotationReport; +import jalview.util.MessageManager; +import jalview.util.Platform; +import jalview.viewmodel.AlignmentViewport; +import jalview.viewmodel.ViewportRanges; + /** * This panel hosts alignment sequence ids and responds to mouse clicks on them, * as well as highlighting ids matched by a search from the Find menu. @@ -62,8 +62,6 @@ public class IdPanel extends JPanel ScrollThread scrollThread = null; - String linkImageURL; - int offy; // int width; @@ -84,8 +82,7 @@ public class IdPanel extends JPanel this.av = av; alignPanel = parent; setIdCanvas(new IdCanvas(av)); - linkImageURL = getClass().getResource("/images/link.gif").toString(); - seqAnnotReport = new SequenceAnnotationReport(linkImageURL); + seqAnnotReport = new SequenceAnnotationReport(true); setLayout(new BorderLayout()); add(getIdCanvas(), BorderLayout.CENTER); addMouseListener(this); diff --git a/src/jalview/gui/PopupMenu.java b/src/jalview/gui/PopupMenu.java index 187090b..568f7f1 100644 --- a/src/jalview/gui/PopupMenu.java +++ b/src/jalview/gui/PopupMenu.java @@ -20,6 +20,31 @@ */ package jalview.gui; +import java.awt.Color; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Collection; +import java.util.Collections; +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; + +import javax.swing.ButtonGroup; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JColorChooser; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; +import javax.swing.JRadioButtonMenuItem; + import jalview.analysis.AAFrequency; import jalview.analysis.AlignmentAnnotationUtils; import jalview.analysis.AlignmentUtils; @@ -56,31 +81,6 @@ import jalview.util.StringUtils; import jalview.util.UrlLink; import jalview.viewmodel.seqfeatures.FeatureRendererModel; -import java.awt.Color; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.BitSet; -import java.util.Collection; -import java.util.Collections; -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; - -import javax.swing.ButtonGroup; -import javax.swing.JCheckBoxMenuItem; -import javax.swing.JColorChooser; -import javax.swing.JMenu; -import javax.swing.JMenuItem; -import javax.swing.JPopupMenu; -import javax.swing.JRadioButtonMenuItem; - /** * The popup menu that is displayed on right-click on a sequence id, or in the * sequence alignment. @@ -755,14 +755,16 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener } /** - * Add a link to show feature details for each sequence feature + * Add a menu item to show feature details for each sequence feature. Any + * linked 'virtual' features (CDS/protein) are also optionally found and + * included. * * @param features - * @param column * @param seq + * @param column */ protected void addFeatureDetails(List features, - SequenceI seq, int column) + final SequenceI seq, final int column) { /* * add features in CDS/protein complement at the corresponding @@ -797,39 +799,49 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener String name = seq.getName(); for (final SequenceFeature sf : features) { - addFeatureDetailsMenuItem(details, name, sf); + addFeatureDetailsMenuItem(details, name, sf, null); } if (mf != null) { - name = mf.fromSeq == seq ? mf.mapping.getTo().getName() - : mf.fromSeq.getName(); for (final SequenceFeature sf : mf.features) { - addFeatureDetailsMenuItem(details, name, sf); + addFeatureDetailsMenuItem(details, name, sf, mf); } } } /** - * A helper method to add one menu item whose action is to show details for one - * feature. The menu text includes feature description, but this may be + * A helper method to add one menu item whose action is to show details for + * one feature. The menu text includes feature description, but this may be * truncated. * * @param details * @param seqName * @param sf + * @param mf */ void addFeatureDetailsMenuItem(JMenu details, final String seqName, - final SequenceFeature sf) + final SequenceFeature sf, MappedFeatures mf) { int start = sf.getBegin(); int end = sf.getEnd(); + if (mf != null) + { + /* + * show local rather than linked feature coordinates + */ + int[] beginRange = mf.getMappedPositions(start, start); + start = beginRange[0]; + int[] endRange = mf.getMappedPositions(end, end); + end = endRange[endRange.length - 1]; + } StringBuilder desc = new StringBuilder(); desc.append(sf.getType()).append(" ").append(String.valueOf(start)); if (start != end) { - desc.append("-").append(String.valueOf(end)); + desc.append(sf.isContactFeature() ? ":" : "-"); + desc.append(String.valueOf(end)); } String description = sf.getDescription(); if (description != null) @@ -860,26 +872,27 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener @Override public void actionPerformed(ActionEvent e) { - showFeatureDetails(seqName, sf); + showFeatureDetails(sf, seqName, mf); } }); details.add(item); } /** - * Opens a panel showing a text report of feature dteails - * - * @param seqName + * Opens a panel showing a text report of feature details * * @param sf + * @param seqName + * @param mf */ - protected void showFeatureDetails(String seqName, SequenceFeature sf) + protected void showFeatureDetails(SequenceFeature sf, String seqName, + MappedFeatures mf) { CutAndPasteHtmlTransfer cap = new CutAndPasteHtmlTransfer(); // 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(seqName)); + cap.setText(sf.getDetailsReport(seqName, mf)); Desktop.addInternalFrame(cap, MessageManager.getString("label.feature_details"), 500, 500); @@ -1763,7 +1776,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener "label.create_sequence_details_report_annotation_for", new Object[] { seq.getDisplayId(true) }) + "

"); - new SequenceAnnotationReport(null).createSequenceAnnotationReport( + new SequenceAnnotationReport(false).createSequenceAnnotationReport( contents, seq, true, true, ap.getSeqPanel().seqCanvas.fr); contents.append("

"); } diff --git a/src/jalview/gui/SeqPanel.java b/src/jalview/gui/SeqPanel.java index 27ceb27..f28217d 100644 --- a/src/jalview/gui/SeqPanel.java +++ b/src/jalview/gui/SeqPanel.java @@ -207,8 +207,6 @@ public class SeqPanel extends JPanel StringBuffer keyboardNo2; - java.net.URL linkImageURL; - private final SequenceAnnotationReport seqARep; StringBuilder tooltipText = new StringBuilder(); @@ -229,8 +227,7 @@ public class SeqPanel extends JPanel */ public SeqPanel(AlignViewport viewport, AlignmentPanel alignPanel) { - linkImageURL = getClass().getResource("/images/link.gif"); - seqARep = new SequenceAnnotationReport(linkImageURL.toString()); + seqARep = new SequenceAnnotationReport(true); ToolTipManager.sharedInstance().registerComponent(this); ToolTipManager.sharedInstance().setInitialDelay(0); ToolTipManager.sharedInstance().setDismissDelay(10000); @@ -1047,9 +1044,9 @@ public class SeqPanel extends JPanel { List features = ap.getFeatureRenderer() .findFeaturesAtColumn(sequence, column + 1); - unshownFeatures = seqARep.appendFeaturesLengthLimit(tooltipText, pos, - features, - this.ap.getSeqPanel().seqCanvas.fr, MAX_TOOLTIP_LENGTH); + unshownFeatures = seqARep.appendFeatures(tooltipText, pos, + features, this.ap.getSeqPanel().seqCanvas.fr, + MAX_TOOLTIP_LENGTH); /* * add features in CDS/protein complement at the corresponding @@ -1067,9 +1064,8 @@ public class SeqPanel extends JPanel pos); if (mf != null) { - unshownFeatures = seqARep.appendFeaturesLengthLimit( - tooltipText, pos, mf, fr2, - MAX_TOOLTIP_LENGTH); + unshownFeatures = seqARep.appendFeatures(tooltipText, + pos, mf, fr2, MAX_TOOLTIP_LENGTH); } } } diff --git a/src/jalview/io/FeaturesFile.java b/src/jalview/io/FeaturesFile.java index a8a3746..92473ec 100755 --- a/src/jalview/io/FeaturesFile.java +++ b/src/jalview/io/FeaturesFile.java @@ -20,6 +20,18 @@ */ package jalview.io; +import java.awt.Color; +import java.io.IOException; +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; + import jalview.analysis.AlignmentUtils; import jalview.analysis.SequenceIdMatcher; import jalview.api.AlignViewportI; @@ -44,18 +56,6 @@ import jalview.util.MapList; import jalview.util.ParseHtmlBodyAndLinks; import jalview.util.StringUtils; -import java.awt.Color; -import java.io.IOException; -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 * format. These are tab-delimited formats but with differences in the use of @@ -736,7 +736,6 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI if (mf != null) { - MapList mapping = mf.mapping.getMap(); for (SequenceFeature sf : mf.features) { /* @@ -752,9 +751,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI 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); + int[] range = mf.getMappedPositions(begin, end); SequenceFeature sf2 = new SequenceFeature(sf, range[0], range[1], group, sf.getScore()); complementary.add(sf2); diff --git a/src/jalview/io/SequenceAnnotationReport.java b/src/jalview/io/SequenceAnnotationReport.java index 0125277..8328e7a 100644 --- a/src/jalview/io/SequenceAnnotationReport.java +++ b/src/jalview/io/SequenceAnnotationReport.java @@ -20,6 +20,13 @@ */ package jalview.io; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + import jalview.api.FeatureColourI; import jalview.datamodel.DBRefEntry; import jalview.datamodel.DBRefSource; @@ -32,13 +39,6 @@ import jalview.util.StringUtils; import jalview.util.UrlLink; import jalview.viewmodel.seqfeatures.FeatureRendererModel; -import java.util.Arrays; -import java.util.Collection; -import java.util.Comparator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - /** * generate HTML reports for a sequence * @@ -56,12 +56,12 @@ public class SequenceAnnotationReport private static final int MAX_SOURCES = 40; + private static String linkImageURL; + private static final String[][] PRIMARY_SOURCES = new String[][] { DBRefSource.CODINGDBS, DBRefSource.DNACODINGDBS, DBRefSource.PROTEINDBS }; - final String linkImageURL; - /* * Comparator to order DBRefEntry by Source + accession id (case-insensitive), * with 'Primary' sources placed before others, and 'chromosome' first of all @@ -120,14 +120,30 @@ public class SequenceAnnotationReport } }; - public SequenceAnnotationReport(String linkURL) + private boolean forTooltip; + + /** + * Constructor given a flag which affects behaviour + *
    + *
  • if true, generates feature details suitable to show in a tooltip
  • + *
  • if false, generates feature details in a form suitable for the sequence + * details report
  • + *
+ * + * @param isForTooltip + */ + public SequenceAnnotationReport(boolean isForTooltip) { - this.linkImageURL = linkURL; + this.forTooltip = isForTooltip; + if (linkImageURL == null) + { + linkImageURL = getClass().getResource("/images/link.gif").toString(); + } } /** - * Append text for the list of features to the tooltip Returns number of - * features left if maxlength limit is (or would have been) reached + * Append text for the list of features to the tooltip. Returns the number of + * features not added if maxlength limit is (or would have been) reached. * * @param sb * @param residuePos @@ -135,7 +151,7 @@ public class SequenceAnnotationReport * @param minmax * @param maxlength */ - public int appendFeaturesLengthLimit(final StringBuilder sb, + public int appendFeatures(final StringBuilder sb, int residuePos, List features, FeatureRendererModel fr, int maxlength) { @@ -150,16 +166,10 @@ public class SequenceAnnotationReport return 0; } - public void appendFeatures(final StringBuilder sb, int residuePos, - List features, FeatureRendererModel fr) - { - appendFeaturesLengthLimit(sb, residuePos, features, fr, 0); - } - /** - * Appends text for mapped features (e.g. CDS feature for peptide or vice versa) - * Returns number of features left if maxlength limit is (or would have been) - * reached + * Appends text for mapped features (e.g. CDS feature for peptide or vice + * versa) Returns number of features left if maxlength limit is (or would have + * been) reached. * * @param sb * @param residuePos @@ -167,7 +177,7 @@ public class SequenceAnnotationReport * @param fr * @param maxlength */ - public int appendFeaturesLengthLimit(StringBuilder sb, int residuePos, + public int appendFeatures(StringBuilder sb, int residuePos, MappedFeatures mf, FeatureRendererModel fr, int maxlength) { for (int i = 0; i < mf.features.size(); i++) @@ -181,12 +191,6 @@ public class SequenceAnnotationReport return 0; } - public void appendFeatures(StringBuilder sb, int residuePos, - MappedFeatures mf, FeatureRendererModel fr) - { - appendFeaturesLengthLimit(sb, residuePos, mf, fr, 0); - } - /** * Appends the feature at rpos to the given buffer * @@ -199,19 +203,44 @@ public class SequenceAnnotationReport FeatureRendererModel fr, SequenceFeature feature, MappedFeatures mf, int maxlength) { + int begin = feature.getBegin(); + int end = feature.getEnd(); + + /* + * if this is a virtual features, convert begin/end to the + * coordinates of the sequence it is mapped to + */ + int[] beginRange = null; + int[] endRange = null; + if (mf != null) + { + beginRange = mf.getMappedPositions(begin, begin); + endRange = mf.getMappedPositions(end, end); + begin = beginRange[0]; + end = endRange[endRange.length - 1]; + } + StringBuilder sb = new StringBuilder(); if (feature.isContactFeature()) { - if (feature.getBegin() == rpos || feature.getEnd() == rpos) + /* + * include if rpos is at start or end position of [mapped] feature + */ + boolean showContact = (mf == null) && (rpos == begin || rpos == end); + boolean showMappedContact = (mf != null) && ((rpos >= beginRange[0] + && rpos <= beginRange[beginRange.length - 1]) + || (rpos >= endRange[0] + && rpos <= endRange[endRange.length - 1])); + if (showContact || showMappedContact) { if (sb0.length() > 6) { sb.append("
"); } - sb.append(feature.getType()).append(" ").append(feature.getBegin()) - .append(":").append(feature.getEnd()); + sb.append(feature.getType()).append(" ").append(begin).append(":") + .append(end); } - return appendTextMaxLengthReached(sb0, sb, maxlength); + return appendText(sb0, sb, maxlength); } if (sb0.length() > 6) @@ -226,11 +255,11 @@ public class SequenceAnnotationReport if (rpos != 0) { // we are marking a positional feature - sb.append(feature.begin); - } - if (feature.begin != feature.end) - { - sb.append(" ").append(feature.end); + sb.append(begin); + if (begin != end) + { + sb.append(" ").append(end); + } } String description = feature.getDescription(); @@ -291,27 +320,28 @@ public class SequenceAnnotationReport } } } - return appendTextMaxLengthReached(sb0, sb, maxlength); + return appendText(sb0, sb, maxlength); } - void appendFeature(final StringBuilder sb, int rpos, - FeatureRendererModel fr, SequenceFeature feature, - MappedFeatures mf) - { - appendFeature(sb, rpos, fr, feature, mf, 0); - } - - private static boolean appendTextMaxLengthReached(StringBuilder sb0, - StringBuilder sb, int maxlength) + /** + * Appends sb to sb0, and returns false, unless maxlength is not zero and + * appending would make the result longer than or equal to maxlength, in which + * case the append is not done and returns true + * + * @param sb0 + * @param sb + * @param maxlength + * @return + */ + private static boolean appendText(StringBuilder sb0, StringBuilder sb, + int maxlength) { - boolean ret = false; if (maxlength == 0 || sb0.length() + sb.length() < maxlength) { sb0.append(sb); return false; - } else { - return true; } + return true; } /** @@ -466,7 +496,7 @@ public class SequenceAnnotationReport .getNonPositionalFeatures()) { int sz = -sb.length(); - appendFeature(sb, 0, fr, sf, null); + appendFeature(sb, 0, fr, sf, null, 0); sz += sb.length(); maxWidth = Math.max(maxWidth, sz); } diff --git a/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java b/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java index 9a8a086..0a667aa 100644 --- a/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java +++ b/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java @@ -20,6 +20,20 @@ */ package jalview.viewmodel.seqfeatures; +import java.awt.Color; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + import jalview.api.AlignViewportI; import jalview.api.FeatureColourI; import jalview.api.FeaturesDisplayedI; @@ -38,20 +52,6 @@ import jalview.renderer.seqfeatures.FeatureRenderer; import jalview.schemes.FeatureColour; import jalview.util.ColorUtils; -import java.awt.Color; -import java.beans.PropertyChangeListener; -import java.beans.PropertyChangeSupport; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - public abstract class FeatureRendererModel implements jalview.api.FeatureRenderer { @@ -1162,8 +1162,8 @@ public abstract class FeatureRendererModel } @Override - public MappedFeatures findComplementFeaturesAtResidue(SequenceI sequence, - int pos) + public MappedFeatures findComplementFeaturesAtResidue( + final SequenceI sequence, final int pos) { SequenceI ds = sequence.getDatasetSequence(); if (ds == null) @@ -1232,9 +1232,12 @@ public abstract class FeatureRendererModel } /* - * sort by renderorder, inefficiently + * sort by renderorder (inefficiently but ok for small scale); + * NB this sorts 'on top' feature to end, for rendering */ List result = new ArrayList<>(); + final int toAdd = found.size(); + int added = 0; for (String type : renderOrder) { for (SequenceFeature sf : found) @@ -1242,11 +1245,15 @@ public abstract class FeatureRendererModel if (type.equals(sf.getType())) { result.add(sf); - if (result.size() == found.size()) - { - return new MappedFeatures(mapping, mapFrom, pos, residue, - result); - } + added++; + } + if (added == toAdd) + { + break; + } + if (added == toAdd) + { + break; } } } diff --git a/test/jalview/datamodel/SequenceFeatureTest.java b/test/jalview/datamodel/SequenceFeatureTest.java index cd8f9eb..673ea29 100644 --- a/test/jalview/datamodel/SequenceFeatureTest.java +++ b/test/jalview/datamodel/SequenceFeatureTest.java @@ -26,11 +26,11 @@ import static org.testng.AssertJUnit.assertNull; import static org.testng.AssertJUnit.assertSame; import static org.testng.AssertJUnit.assertTrue; -import jalview.gui.JvOptionPane; - import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; +import jalview.gui.JvOptionPane; + public class SequenceFeatureTest { @@ -285,7 +285,7 @@ public class SequenceFeatureTest String expected = "
" + "" + "
LocationTestSeq22
Typevariant
DescriptionG,C
"; - assertEquals(expected, sf.getDetailsReport(seqName)); + assertEquals(expected, sf.getDetailsReport(seqName, null)); // contact feature sf = new SequenceFeature("Disulphide Bond", "a description", 28, 31, @@ -293,7 +293,7 @@ public class SequenceFeatureTest expected = "
" + "" + "
LocationTestSeq28:31
TypeDisulphide Bond
Descriptiona description
"; - assertEquals(expected, sf.getDetailsReport(seqName)); + assertEquals(expected, sf.getDetailsReport(seqName, null)); sf = new SequenceFeature("variant", "G,C", 22, 33, 12.5f, "group"); @@ -306,7 +306,7 @@ public class SequenceFeatureTest + "Groupgroup" + "ChildENSP002" + "ParentENSG001"; - assertEquals(expected, sf.getDetailsReport(seqName)); + assertEquals(expected, sf.getDetailsReport(seqName, null)); /* * feature with embedded html link in description @@ -317,6 +317,6 @@ public class SequenceFeatureTest + "TypePfam" + "DescriptionFer2 Status: True Positive Pfam 8_8" + "GroupUniprot"; - assertEquals(expected, sf.getDetailsReport(seqName)); + assertEquals(expected, sf.getDetailsReport(seqName, null)); } } diff --git a/test/jalview/io/SequenceAnnotationReportTest.java b/test/jalview/io/SequenceAnnotationReportTest.java index 42183ca..7e00caa 100644 --- a/test/jalview/io/SequenceAnnotationReportTest.java +++ b/test/jalview/io/SequenceAnnotationReportTest.java @@ -23,6 +23,14 @@ package jalview.io; import static org.testng.AssertJUnit.assertEquals; import static org.testng.AssertJUnit.assertTrue; +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + import jalview.api.FeatureColourI; import jalview.datamodel.DBRefEntry; import jalview.datamodel.Sequence; @@ -33,15 +41,6 @@ import jalview.io.gff.GffConstants; import jalview.renderer.seqfeatures.FeatureRenderer; import jalview.schemes.FeatureColour; import jalview.viewmodel.seqfeatures.FeatureRendererModel; - -import java.awt.Color; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import org.testng.annotations.BeforeClass; -import org.testng.annotations.Test; - import junit.extensions.PA; public class SequenceAnnotationReportTest @@ -57,24 +56,24 @@ public class SequenceAnnotationReportTest @Test(groups = "Functional") public void testAppendFeature_disulfideBond() { - SequenceAnnotationReport sar = new SequenceAnnotationReport(null); + SequenceAnnotationReport sar = new SequenceAnnotationReport(false); StringBuilder sb = new StringBuilder(); sb.append("123456"); SequenceFeature sf = new SequenceFeature("disulfide bond", "desc", 1, 3, 1.2f, "group"); // residuePos == 2 does not match start or end of feature, nothing done: - sar.appendFeature(sb, 2, null, sf, null); + sar.appendFeature(sb, 2, null, sf, null, 0); 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, null); + sar.appendFeature(sb, 1, null, sf, null, 0); 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, null); + sar.appendFeature(sb, 3, null, sf, null, 0); assertEquals("123456disulfide bond 1:3
disulfide bond 1:3", sb.toString()); } @@ -82,13 +81,13 @@ public class SequenceAnnotationReportTest @Test(groups = "Functional") public void testAppendFeatures_longText() { - SequenceAnnotationReport sar = new SequenceAnnotationReport(null); + SequenceAnnotationReport sar = new SequenceAnnotationReport(false); StringBuilder sb = new StringBuilder(); String longString = "Abcd".repeat(50); SequenceFeature sf = new SequenceFeature("sequence", longString, 1, 3, "group"); - sar.appendFeature(sb, 1, null, sf, null); + sar.appendFeature(sb, 1, null, sf, null, 0); assertTrue(sb.length() < 100); List sfl = new ArrayList<>(); @@ -103,7 +102,7 @@ public class SequenceAnnotationReportTest sfl.add(sf); sfl.add(sf); sfl.add(sf); - int n = sar.appendFeaturesLengthLimit(sb, 1, sfl, + int n = sar.appendFeatures(sb, 1, sfl, new FeatureRenderer(null), 200); // text should terminate before 200 characters String s = sb.toString(); assertTrue(s.length() < 200); @@ -114,27 +113,27 @@ public class SequenceAnnotationReportTest @Test(groups = "Functional") public void testAppendFeature_status() { - SequenceAnnotationReport sar = new SequenceAnnotationReport(null); + SequenceAnnotationReport sar = new SequenceAnnotationReport(false); StringBuilder sb = new StringBuilder(); SequenceFeature sf = new SequenceFeature("METAL", "Fe2-S", 1, 3, Float.NaN, "group"); sf.setStatus("Confirmed"); - sar.appendFeature(sb, 1, null, sf, null); + sar.appendFeature(sb, 1, null, sf, null, 0); assertEquals("METAL 1 3; Fe2-S; (Confirmed)", sb.toString()); } @Test(groups = "Functional") public void testAppendFeature_withScore() { - SequenceAnnotationReport sar = new SequenceAnnotationReport(null); + SequenceAnnotationReport sar = new SequenceAnnotationReport(false); StringBuilder sb = new StringBuilder(); SequenceFeature sf = new SequenceFeature("METAL", "Fe2-S", 1, 3, 1.3f, "group"); FeatureRendererModel fr = new FeatureRenderer(null); Map minmax = fr.getMinMax(); - sar.appendFeature(sb, 1, fr, sf, null); + sar.appendFeature(sb, 1, fr, sf, null, 0); /* * map has no entry for this feature type - score is not shown: */ @@ -144,7 +143,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, null); + sar.appendFeature(sb, 1, fr, sf, null, 0); //
is appended to a buffer > 6 in length assertEquals("METAL 1 3; Fe2-S
METAL 1 3; Fe2-S Score=1.3", sb.toString()); @@ -154,19 +153,19 @@ public class SequenceAnnotationReportTest */ minmax.put("METAL", new float[][] { { 2f, 2f }, null }); sb.setLength(0); - sar.appendFeature(sb, 1, fr, sf, null); + sar.appendFeature(sb, 1, fr, sf, null, 0); assertEquals("METAL 1 3; Fe2-S", sb.toString()); } @Test(groups = "Functional") public void testAppendFeature_noScore() { - SequenceAnnotationReport sar = new SequenceAnnotationReport(null); + SequenceAnnotationReport sar = new SequenceAnnotationReport(false); StringBuilder sb = new StringBuilder(); SequenceFeature sf = new SequenceFeature("METAL", "Fe2-S", 1, 3, Float.NaN, "group"); - sar.appendFeature(sb, 1, null, sf, null); + sar.appendFeature(sb, 1, null, sf, null, 0); assertEquals("METAL 1 3; Fe2-S", sb.toString()); } @@ -176,7 +175,7 @@ public class SequenceAnnotationReportTest @Test(groups = "Functional") public void testAppendFeature_colouredByAttribute() { - SequenceAnnotationReport sar = new SequenceAnnotationReport(null); + SequenceAnnotationReport sar = new SequenceAnnotationReport(false); StringBuilder sb = new StringBuilder(); SequenceFeature sf = new SequenceFeature("METAL", "Fe2-S", 1, 3, Float.NaN, "group"); @@ -186,7 +185,7 @@ public class SequenceAnnotationReportTest * first with no colour by attribute */ FeatureRendererModel fr = new FeatureRenderer(null); - sar.appendFeature(sb, 1, fr, sf, null); + sar.appendFeature(sb, 1, fr, sf, null, 0); assertEquals("METAL 1 3; Fe2-S", sb.toString()); /* @@ -197,7 +196,7 @@ public class SequenceAnnotationReportTest fc.setAttributeName("Pfam"); fr.setColour("METAL", fc); sb.setLength(0); - sar.appendFeature(sb, 1, fr, sf, null); + sar.appendFeature(sb, 1, fr, sf, null, 0); assertEquals("METAL 1 3; Fe2-S", sb.toString()); // no change /* @@ -205,7 +204,7 @@ public class SequenceAnnotationReportTest */ fc.setAttributeName("clinical_significance"); sb.setLength(0); - sar.appendFeature(sb, 1, fr, sf, null); + sar.appendFeature(sb, 1, fr, sf, null, 0); assertEquals("METAL 1 3; Fe2-S; clinical_significance=Benign", sb.toString()); } @@ -213,7 +212,7 @@ public class SequenceAnnotationReportTest @Test(groups = "Functional") public void testAppendFeature_withScoreStatusAttribute() { - SequenceAnnotationReport sar = new SequenceAnnotationReport(null); + SequenceAnnotationReport sar = new SequenceAnnotationReport(false); StringBuilder sb = new StringBuilder(); SequenceFeature sf = new SequenceFeature("METAL", "Fe2-S", 1, 3, 1.3f, "group"); @@ -227,7 +226,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, null); + sar.appendFeature(sb, 1, fr, sf, null, 0); assertEquals( "METAL 1 3; Fe2-S Score=1.3; (Confirmed); clinical_significance=Benign", @@ -237,38 +236,38 @@ public class SequenceAnnotationReportTest @Test(groups = "Functional") public void testAppendFeature_DescEqualsType() { - SequenceAnnotationReport sar = new SequenceAnnotationReport(null); + SequenceAnnotationReport sar = new SequenceAnnotationReport(false); StringBuilder sb = new StringBuilder(); SequenceFeature sf = new SequenceFeature("METAL", "METAL", 1, 3, Float.NaN, "group"); // description is not included if it duplicates type: - sar.appendFeature(sb, 1, null, sf, null); + sar.appendFeature(sb, 1, null, sf, null, 0); assertEquals("METAL 1 3", sb.toString()); sb.setLength(0); sf.setDescription("Metal"); // test is case-sensitive: - sar.appendFeature(sb, 1, null, sf, null); + sar.appendFeature(sb, 1, null, sf, null, 0); assertEquals("METAL 1 3; Metal", sb.toString()); } @Test(groups = "Functional") public void testAppendFeature_stripHtml() { - SequenceAnnotationReport sar = new SequenceAnnotationReport(null); + SequenceAnnotationReport sar = new SequenceAnnotationReport(false); StringBuilder sb = new StringBuilder(); SequenceFeature sf = new SequenceFeature("METAL", "helloworld", 1, 3, Float.NaN, "group"); - sar.appendFeature(sb, 1, null, sf, null); + sar.appendFeature(sb, 1, null, sf, null, 0); // !! strips off but not ?? assertEquals("METAL 1 3; helloworld", sb.toString()); sb.setLength(0); sf.setDescription("
&kHD>6"); - sar.appendFeature(sb, 1, null, sf, null); + sar.appendFeature(sb, 1, null, sf, null, 0); // if no tag, html-encodes > and < (only): assertEquals("METAL 1 3; <br>&kHD>6", sb.toString()); } @@ -276,7 +275,7 @@ public class SequenceAnnotationReportTest @Test(groups = "Functional") public void testCreateSequenceAnnotationReport() { - SequenceAnnotationReport sar = new SequenceAnnotationReport(null); + SequenceAnnotationReport sar = new SequenceAnnotationReport(false); StringBuilder sb = new StringBuilder(); SequenceI seq = new Sequence("s1", "MAKLKRFQSSTLL"); @@ -398,7 +397,7 @@ public class SequenceAnnotationReportTest @Test(groups = "Functional") public void testCreateSequenceAnnotationReport_withEllipsis() { - SequenceAnnotationReport sar = new SequenceAnnotationReport(null); + SequenceAnnotationReport sar = new SequenceAnnotationReport(false); StringBuilder sb = new StringBuilder(); SequenceI seq = new Sequence("s1", "ABC"); -- 1.7.10.2