JAL-2490 find features for export as GFF, JAL-2548 respect group
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 23 May 2017 16:10:23 +0000 (17:10 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 23 May 2017 16:10:23 +0000 (17:10 +0100)
visibility

src/jalview/analysis/AlignmentSorter.java
src/jalview/api/FeatureRenderer.java
src/jalview/appletgui/AlignFrame.java
src/jalview/gui/AnnotationExporter.java
src/jalview/io/FeaturesFile.java
src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java
test/jalview/io/FeaturesFileTest.java

index f228350..e7733e9 100755 (executable)
@@ -783,8 +783,13 @@ public class AlignmentSorter
           continue;
         }
 
+        /*
+         * accept all features with null or empty group, otherwise
+         * check group is one of the currently visible groups
+         */
         String featureGroup = sf.getFeatureGroup();
         if (groups != null && featureGroup != null
+                && !"".equals(featureGroup)
                 && !groups.contains(featureGroup))
         {
           it.remove();
index 7123b8c..edd236b 100644 (file)
@@ -165,9 +165,9 @@ public interface FeatureRenderer
   List<String> getDisplayedFeatureTypes();
 
   /**
-   * get current displayed groups
+   * Returns a (possibly empty) list of currently visible feature groups
    * 
-   * @return a (possibly empty) list of feature groups
+   * @return
    */
   List<String> getDisplayedFeatureGroups();
 
index b8700e1..2dde2ab 100644 (file)
@@ -1440,6 +1440,17 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     return null;
   }
 
+  private List<String> getDisplayedFeatureGroups()
+  {
+    if (alignPanel.getFeatureRenderer() != null
+            && viewport.getFeaturesDisplayed() != null)
+    {
+      return alignPanel.getFeatureRenderer().getDisplayedFeatureGroups();
+
+    }
+    return null;
+  }
+
   public String outputFeatures(boolean displayTextbox, String format)
   {
     String features;
@@ -1447,12 +1458,14 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     if (format.equalsIgnoreCase("Jalview"))
     {
       features = formatter.printJalviewFormat(viewport.getAlignment()
-              .getSequencesArray(), getDisplayedFeatureCols(), true);
+              .getSequencesArray(), getDisplayedFeatureCols(),
+              getDisplayedFeatureGroups(), true);
     }
     else
     {
       features = formatter.printGffFormat(viewport.getAlignment()
-              .getSequencesArray(), getDisplayedFeatureCols(), true);
+              .getSequencesArray(), getDisplayedFeatureCols(),
+              getDisplayedFeatureGroups(), true);
     }
 
     if (displayTextbox)
index a48c030..42913de 100644 (file)
@@ -34,6 +34,7 @@ import java.awt.Color;
 import java.awt.FlowLayout;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
+import java.util.List;
 import java.util.Map;
 
 import javax.swing.BorderFactory;
@@ -160,16 +161,18 @@ public class AnnotationExporter extends JPanel
       SequenceI[] sequences = ap.av.getAlignment().getSequencesArray();
       Map<String, FeatureColourI> featureColours = ap.getFeatureRenderer()
               .getDisplayedFeatureCols();
+      List<String> featureGroups = ap.getFeatureRenderer()
+              .getDisplayedFeatureGroups();
       boolean includeNonPositional = ap.av.isShowNPFeats();
       if (GFFFormat.isSelected())
       {
         text = formatter.printGffFormat(sequences, featureColours,
-                includeNonPositional);
+                featureGroups, includeNonPositional);
       }
       else
       {
         text = formatter.printJalviewFormat(sequences, featureColours,
-                includeNonPositional);
+                featureGroups, includeNonPositional);
       }
     }
     else
index 869b18b..afc00ee 100755 (executable)
@@ -47,11 +47,9 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
-import java.util.Set;
 
 /**
  * Parses and writes features files, which may be in Jalview, GFF2 or GFF3
@@ -500,19 +498,24 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
   }
 
   /**
-   * Returns contents of a Jalview format features file
+   * 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
    *          source of features
    * @param visible
    *          map of colour for each visible feature type
+   * @param visibleFeatureGroups
    * @param includeNonPositional
    *          if true, include non-positional features (regardless of group or
    *          type)
    * @return
    */
   public String printJalviewFormat(SequenceI[] sequences,
-          Map<String, FeatureColourI> visible, boolean includeNonPositional)
+          Map<String, FeatureColourI> visible,
+          List<String> visibleFeatureGroups, boolean includeNonPositional)
   {
     if (!includeNonPositional && (visible == null || visible.isEmpty()))
     {
@@ -535,30 +538,36 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
       }
     }
 
-    // Work out which groups are both present and visible
-    Set<String> groups = new HashSet<String>();
     String[] types = visible == null ? new String[0] : visible.keySet()
             .toArray(new String[visible.keySet().size()]);
 
-    for (int i = 0; i < sequences.length; i++)
+    /*
+     * sort groups alphabetically, and ensure that null group is output last
+     */
+    List<String> sortedGroups = new ArrayList<String>(visibleFeatureGroups);
+    sortedGroups.remove(null);
+    Collections.sort(sortedGroups);
+    sortedGroups.add(null);
+
+    boolean foundSome = false;
+
+    /*
+     * first output any non-positional features
+     */
+    if (includeNonPositional)
     {
-      groups.addAll(sequences[i].getFeatures()
-              .getFeatureGroups(true, types));
-      if (includeNonPositional)
+      for (int i = 0; i < sequences.length; i++)
       {
-        groups.addAll(sequences[i].getFeatures().getFeatureGroups(false,
-                types));
+        String sequenceName = sequences[i].getName();
+        for (SequenceFeature feature : sequences[i].getFeatures()
+                .getNonPositionalFeatures())
+        {
+          foundSome = true;
+          out.append(formatJalviewFeature(sequenceName, feature));
+        }
       }
     }
 
-    /*
-     * sort distinct groups so null group is output last
-     */
-    List<String> sortedGroups = new ArrayList<String>(groups);
-    Collections.sort(sortedGroups, SORT_NULL_LAST);
-
-    // TODO check where null group should be output
-    boolean foundSome = false;
     for (String group : sortedGroups)
     {
       if (group != null)
@@ -570,16 +579,12 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
       }
 
       /*
-       * output features within groups (non-positional first if wanted)
+       * output positional features within groups
        */
       for (int i = 0; i < sequences.length; i++)
       {
+        String sequenceName = sequences[i].getName();
         List<SequenceFeature> features = new ArrayList<SequenceFeature>();
-        if (includeNonPositional)
-        {
-          features.addAll(sequences[i].getFeatures().getFeaturesForGroup(
-                  false, group, types));
-        }
         if (types.length > 0)
         {
           features.addAll(sequences[i].getFeatures().getFeaturesForGroup(
@@ -588,57 +593,8 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
 
         for (SequenceFeature sequenceFeature : features)
         {
-          // we have features to output
           foundSome = true;
-          if (sequenceFeature.description == null
-                  || sequenceFeature.description.equals(""))
-          {
-            out.append(sequenceFeature.type).append(TAB);
-          }
-          else
-          {
-            if (sequenceFeature.links != null
-                    && sequenceFeature.getDescription().indexOf("<html>") == -1)
-            {
-              out.append("<html>");
-            }
-
-            out.append(sequenceFeature.description);
-            if (sequenceFeature.links != null)
-            {
-              for (int l = 0; l < sequenceFeature.links.size(); l++)
-              {
-                String label = sequenceFeature.links.elementAt(l);
-                String href = label.substring(label.indexOf("|") + 1);
-                label = label.substring(0, label.indexOf("|"));
-
-                if (sequenceFeature.description.indexOf(href) == -1)
-                {
-                  out.append(" <a href=\"" + href + "\">" + label + "</a>");
-                }
-              }
-
-              if (sequenceFeature.getDescription().indexOf("</html>") == -1)
-              {
-                out.append("</html>");
-              }
-            }
-
-            out.append(TAB);
-          }
-          out.append(sequences[i].getName());
-          out.append("\t-1\t");
-          out.append(sequenceFeature.begin);
-          out.append(TAB);
-          out.append(sequenceFeature.end);
-          out.append(TAB);
-          out.append(sequenceFeature.type);
-          if (!Float.isNaN(sequenceFeature.score))
-          {
-            out.append(TAB);
-            out.append(sequenceFeature.score);
-          }
-          out.append(newline);
+          out.append(formatJalviewFeature(sequenceName, sequenceFeature));
         }
       }
 
@@ -654,6 +610,68 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
   }
 
   /**
+   * @param out
+   * @param sequenceName
+   * @param sequenceFeature
+   */
+  protected String formatJalviewFeature(
+          String sequenceName, SequenceFeature sequenceFeature)
+  {
+    StringBuilder out = new StringBuilder(64);
+    if (sequenceFeature.description == null
+            || sequenceFeature.description.equals(""))
+    {
+      out.append(sequenceFeature.type).append(TAB);
+    }
+    else
+    {
+      if (sequenceFeature.links != null
+              && sequenceFeature.getDescription().indexOf("<html>") == -1)
+      {
+        out.append("<html>");
+      }
+
+      out.append(sequenceFeature.description);
+      if (sequenceFeature.links != null)
+      {
+        for (int l = 0; l < sequenceFeature.links.size(); l++)
+        {
+          String label = sequenceFeature.links.elementAt(l);
+          String href = label.substring(label.indexOf("|") + 1);
+          label = label.substring(0, label.indexOf("|"));
+
+          if (sequenceFeature.description.indexOf(href) == -1)
+          {
+            out.append(" <a href=\"" + href + "\">" + label + "</a>");
+          }
+        }
+
+        if (sequenceFeature.getDescription().indexOf("</html>") == -1)
+        {
+          out.append("</html>");
+        }
+      }
+
+      out.append(TAB);
+    }
+    out.append(sequenceName);
+    out.append("\t-1\t");
+    out.append(sequenceFeature.begin);
+    out.append(TAB);
+    out.append(sequenceFeature.end);
+    out.append(TAB);
+    out.append(sequenceFeature.type);
+    if (!Float.isNaN(sequenceFeature.score))
+    {
+      out.append(TAB);
+      out.append(sequenceFeature.score);
+    }
+    out.append(newline);
+
+    return out.toString();
+  }
+
+  /**
    * Parse method that is called when a GFF file is dragged to the desktop
    */
   @Override
@@ -712,76 +730,84 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
    *          the sequences whose features are to be output
    * @param visible
    *          a map whose keys are the type names of visible features
+   * @param visibleFeatureGroups
    * @param includeNonPositionalFeatures
    * @return
    */
   public String printGffFormat(SequenceI[] sequences,
           Map<String, FeatureColourI> visible,
+          List<String> visibleFeatureGroups,
           boolean includeNonPositionalFeatures)
   {
     StringBuilder out = new StringBuilder(256);
-    int version = gffVersion == 0 ? 2 : gffVersion;
-    out.append(String.format("%s %d\n", GFF_VERSION, version));
-    String source;
-    boolean isnonpos;
+
+    out.append(String.format("%s %d\n", GFF_VERSION, gffVersion == 0 ? 2 : gffVersion));
+
+    if (!includeNonPositionalFeatures
+            && (visible == null || visible.isEmpty()))
+    {
+      return out.toString();
+    }
+
+    String[] types = visible == null ? new String[0] : visible.keySet()
+            .toArray(
+            new String[visible.keySet().size()]);
+
     for (SequenceI seq : sequences)
     {
-      SequenceFeature[] features = seq.getSequenceFeatures();
-      if (features != null)
+      List<SequenceFeature> features = new ArrayList<SequenceFeature>();
+      if (includeNonPositionalFeatures)
       {
-        for (SequenceFeature sf : features)
-        {
-          isnonpos = sf.begin == 0 && sf.end == 0;
-          if (!includeNonPositionalFeatures && isnonpos)
-          {
-            /*
-             * ignore non-positional features if not wanted
-             */
-            continue;
-          }
-          if (!isnonpos && !visible.containsKey(sf.type))
-          {
-            /*
-             * ignore not visible features if not wanted
-             */
-            continue;
-          }
+        features.addAll(seq.getFeatures().getNonPositionalFeatures());
+      }
+      if (visible != null && !visible.isEmpty())
+      {
+        features.addAll(seq.getFeatures().getPositionalFeatures(types));
+      }
 
-          source = sf.featureGroup;
-          if (source == null)
-          {
-            source = sf.getDescription();
-          }
+      for (SequenceFeature sf : features)
+      {
+        String source = sf.featureGroup;
+        if (!sf.isNonPositional() && source != null
+                && !visibleFeatureGroups.contains(source))
+        {
+          // group is not visible
+          continue;
+        }
 
-          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 (source == null)
+        {
+          source = sf.getDescription();
+        }
 
-          out.append(newline);
+        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);
         }
+
+        out.append(newline);
       }
     }
 
@@ -1042,10 +1068,11 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
 
       // rename sequences if GFF handler requested this
       // TODO a more elegant way e.g. gffHelper.postProcess(newseqs) ?
-      SequenceFeature[] sfs = seq.getSequenceFeatures();
-      if (sfs != null)
+      List<SequenceFeature> sfs = seq.getFeatures().getPositionalFeatures();
+      if (!sfs.isEmpty())
       {
-        String newName = (String) sfs[0].getValue(GffHelperI.RENAME_TOKEN);
+        String newName = (String) sfs.get(0).getValue(
+                GffHelperI.RENAME_TOKEN);
         if (newName != null)
         {
           seq.setName(newName);
index 284ee4f..a8e8989 100644 (file)
@@ -980,23 +980,12 @@ public abstract class FeatureRendererModel implements
   public List<String> getDisplayedFeatureGroups()
   {
     List<String> _gps = new ArrayList<String>();
-    boolean valid = false;
     for (String gp : getFeatureGroups())
     {
       if (checkGroupVisibility(gp, false))
       {
-        valid = true;
         _gps.add(gp);
       }
-      if (!valid)
-      {
-        return null;
-      }
-      else
-      {
-        // gps = new String[_gps.size()];
-        // _gps.toArray(gps);
-      }
     }
     return _gps;
   }
index 6429e16..d59c6bb 100644 (file)
@@ -39,7 +39,10 @@ import jalview.gui.JvOptionPane;
 import java.awt.Color;
 import java.io.File;
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import org.testng.annotations.BeforeClass;
@@ -421,17 +424,20 @@ public class FeaturesFileTest
      */
     FeatureRenderer fr = af.alignPanel.getFeatureRenderer();
     Map<String, FeatureColourI> visible = fr.getDisplayedFeatureCols();
+    List<String> visibleGroups = new ArrayList<String>(
+            Arrays.asList(new String[] {}));
     String exported = featuresFile.printJalviewFormat(
-            al.getSequencesArray(), visible, false);
+            al.getSequencesArray(), visible, visibleGroups, false);
     String expected = "No Features Visible";
     assertEquals(expected, exported);
 
     /*
      * include non-positional features
      */
+    visibleGroups.add("uniprot");
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(),
-            visible, true);
-    expected = "\nSTARTGROUP\tuniprot\nCath\tFER_CAPAA\t-1\t0\t0\tDomain\t0.0\nENDGROUP\tuniprot\n";
+            visible, visibleGroups, true);
+    expected = "Cath\tFER_CAPAA\t-1\t0\t0\tDomain\t0.0\n\nSTARTGROUP\tuniprot\nENDGROUP\tuniprot\n";
     assertEquals(expected, exported);
 
     /*
@@ -441,7 +447,7 @@ public class FeaturesFileTest
     fr.setVisible("GAMMA-TURN");
     visible = fr.getDisplayedFeatureCols();
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(),
-            visible, false);
+            visible, visibleGroups, false);
     expected = "METAL\tcc9900\n"
             + "GAMMA-TURN\tff0000|00ffff|20.0|95.0|below|66.0\n"
             + "\nSTARTGROUP\tuniprot\n"
@@ -456,7 +462,7 @@ public class FeaturesFileTest
     fr.setVisible("Pfam");
     visible = fr.getDisplayedFeatureCols();
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(),
-            visible, false);
+            visible, visibleGroups, false);
     /*
      * features are output within group, ordered by sequence and by type
      */
@@ -484,12 +490,14 @@ public class FeaturesFileTest
     FeaturesFile featuresFile = new FeaturesFile();
     FeatureRenderer fr = af.alignPanel.getFeatureRenderer();
     Map<String, FeatureColourI> visible = new HashMap<String, FeatureColourI>();
-    String exported = featuresFile.printGffFormat(
-            al.getSequencesArray(), visible, false);
+    List<String> visibleGroups = new ArrayList<String>(
+            Arrays.asList(new String[] {}));
+    String exported = featuresFile.printGffFormat(al.getSequencesArray(),
+            visible, visibleGroups, false);
     String gffHeader = "##gff-version 2\n";
     assertEquals(gffHeader, exported);
     exported = featuresFile.printGffFormat(al.getSequencesArray(), visible,
-            true);
+            visibleGroups, true);
     assertEquals(gffHeader, exported);
 
     /*
@@ -503,7 +511,8 @@ public class FeaturesFileTest
             .addSequenceFeature(
                     new SequenceFeature("GAMMA-TURN", "Turn", 36, 38, 2.1f,
                             "s3dm"));
-    SequenceFeature sf = new SequenceFeature("Pfam", "", 20, 20, 0f, "Uniprot");
+    SequenceFeature sf = new SequenceFeature("Pfam", "", 20, 20, 0f,
+            "Uniprot");
     sf.setAttributes("x=y;black=white");
     sf.setStrand("+");
     sf.setPhase("2");
@@ -513,40 +522,53 @@ public class FeaturesFileTest
      * with no features displayed, exclude non-positional features
      */
     exported = featuresFile.printGffFormat(al.getSequencesArray(), visible,
-            false);
+            visibleGroups, false);
     assertEquals(gffHeader, exported);
-  
+
     /*
      * include non-positional features
      */
-    exported = featuresFile.printGffFormat(al.getSequencesArray(),
-            visible, true);
+    visibleGroups.add("Uniprot");
+    exported = featuresFile.printGffFormat(al.getSequencesArray(), visible,
+            visibleGroups, true);
     String expected = gffHeader
             + "FER_CAPAA\tUniprot\tDomain\t0\t0\t0.0\t.\t.\n";
     assertEquals(expected, exported);
-  
+
     /*
      * set METAL (in uniprot group) and GAMMA-TURN visible, but not Pfam
+     * only Uniprot group visible here...
      */
     fr.setVisible("METAL");
     fr.setVisible("GAMMA-TURN");
     visible = fr.getDisplayedFeatureCols();
-    exported = featuresFile.printGffFormat(al.getSequencesArray(),
-            visible, false);
+    exported = featuresFile.printGffFormat(al.getSequencesArray(), visible,
+            visibleGroups, 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);
+
+    /*
+     * set s3dm group visible
+     */
+    visibleGroups.add("s3dm");
+    exported = featuresFile.printGffFormat(al.getSequencesArray(), visible,
+            visibleGroups, 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";
     assertEquals(expected, exported);
-  
+
     /*
      * now set Pfam visible
      */
     fr.setVisible("Pfam");
     visible = fr.getDisplayedFeatureCols();
-    exported = featuresFile.printGffFormat(al.getSequencesArray(),
-            visible, false);
+    exported = featuresFile.printGffFormat(al.getSequencesArray(), visible,
+            visibleGroups, false);
     // Pfam feature columns include strand(+), phase(2), attributes
-    expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n"
+    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";
     assertEquals(expected, exported);