Merge branch 'develop' into features/JAL-2446NCList
[jalview.git] / src / jalview / io / FeaturesFile.java
index 4be8d0e..d2282b1 100755 (executable)
@@ -44,8 +44,8 @@ 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.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -94,14 +94,14 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
   /**
    * Constructor which does not parse the file immediately
    * 
-   * @param inFile
+   * @param file
    * @param paste
    * @throws IOException
    */
-  public FeaturesFile(String inFile, DataSourceType paste)
+  public FeaturesFile(String file, DataSourceType paste)
           throws IOException
   {
-    super(false, inFile, paste);
+    super(false, file, paste);
   }
 
   /**
@@ -117,14 +117,14 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
    * Constructor that optionally parses the file immediately
    * 
    * @param parseImmediately
-   * @param inFile
+   * @param file
    * @param type
    * @throws IOException
    */
-  public FeaturesFile(boolean parseImmediately, String inFile,
+  public FeaturesFile(boolean parseImmediately, String file,
           DataSourceType type) throws IOException
   {
-    super(parseImmediately, inFile, type);
+    super(parseImmediately, file, type);
   }
 
   /**
@@ -281,7 +281,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
      */
     for (SequenceI newseq : newseqs)
     {
-      if (newseq.getSequenceFeatures() != null)
+      if (newseq.getFeatures().hasFeatures())
       {
         align.addSequence(newseq);
       }
@@ -359,20 +359,23 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
       Color colour = ColorUtils.createColourFromName(ft);
       featureColours.put(ft, new FeatureColour(colour));
     }
-    SequenceFeature sf = new SequenceFeature(ft, desc, "", startPos, endPos,
-            featureGroup);
+    SequenceFeature sf = null;
     if (gffColumns.length > 6)
     {
       float score = Float.NaN;
       try
       {
         score = new Float(gffColumns[6]).floatValue();
-        // update colourgradient bounds if allowed to
       } catch (NumberFormatException ex)
       {
-        // leave as NaN
+        sf = new SequenceFeature(ft, desc, startPos, endPos, featureGroup);
       }
-      sf.setScore(score);
+      sf = new SequenceFeature(ft, desc, startPos, endPos, score,
+              featureGroup);
+    }
+    else
+    {
+      sf = new SequenceFeature(ft, desc, startPos, endPos, featureGroup);
     }
 
     parseDescriptionHTML(sf, removeHTML);
@@ -472,217 +475,191 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
     ParseHtmlBodyAndLinks parsed = new ParseHtmlBodyAndLinks(
             sf.getDescription(), removeHTML, newline);
 
-    sf.description = (removeHTML) ? parsed.getNonHtmlContent()
-            : sf.description;
+    if (removeHTML)
+    {
+      sf.setDescription(parsed.getNonHtmlContent());
+    }
+
     for (String link : parsed.getLinks())
     {
       sf.addLink(link);
     }
-
   }
 
   /**
-   * generate a features file for seqs includes non-pos features by default.
-   * 
-   * @param sequences
-   *          source of sequence features
-   * @param visible
-   *          hash of feature types and colours
-   * @return features file contents
-   */
-  public String printJalviewFormat(SequenceI[] sequences,
-          Map<String, FeatureColourI> visible)
-  {
-    return printJalviewFormat(sequences, visible, true, true);
-  }
-
-  /**
-   * generate a features file for seqs with colours from visible (if any)
+   * 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
-   *          hash of Colours for each feature type
-   * @param visOnly
-   *          when true only feature types in 'visible' will be output
-   * @param nonpos
-   *          indicates if non-positional features should be output (regardless
-   *          of group or type)
-   * @return features file contents
+   *          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 visOnly,
-          boolean nonpos)
+          Map<String, FeatureColourI> visible,
+          List<String> visibleFeatureGroups, boolean includeNonPositional)
   {
-    StringBuilder out = new StringBuilder(256);
-    boolean featuresGen = false;
-    if (visOnly && !nonpos && (visible == null || visible.size() < 1))
+    if (!includeNonPositional && (visible == null || visible.isEmpty()))
     {
       // no point continuing.
       return "No Features Visible";
     }
 
-    if (visible != null && visOnly)
+    /*
+     * write out feature colours (if we know them)
+     */
+    // TODO: decide if feature links should also be written here ?
+    StringBuilder out = new StringBuilder(256);
+    if (visible != null)
     {
-      // write feature colours only if we're given them and we are generating
-      // viewed features
-      // TODO: decide if feature links should also be written here ?
-      Iterator<String> en = visible.keySet().iterator();
-      while (en.hasNext())
+      for (Entry<String, FeatureColourI> featureColour : visible.entrySet())
       {
-        String featureType = en.next().toString();
-        FeatureColourI colour = visible.get(featureType);
-        out.append(colour.toJalviewFormat(featureType)).append(newline);
+        FeatureColourI colour = featureColour.getValue();
+        out.append(colour.toJalviewFormat(featureColour.getKey())).append(
+                newline);
       }
     }
 
-    // Work out which groups are both present and visible
-    List<String> groups = new ArrayList<String>();
-    int groupIndex = 0;
-    boolean isnonpos = false;
+    String[] types = visible == null ? new String[0] : visible.keySet()
+            .toArray(new String[visible.keySet().size()]);
 
-    SequenceFeature[] features;
-    for (int i = 0; i < sequences.length; i++)
+    /*
+     * sort groups alphabetically, and ensure that features with a
+     * null or empty group are output after those in named groups
+     */
+    List<String> sortedGroups = new ArrayList<String>(visibleFeatureGroups);
+    sortedGroups.remove(null);
+    sortedGroups.remove("");
+    Collections.sort(sortedGroups);
+    sortedGroups.add(null);
+    sortedGroups.add("");
+
+    boolean foundSome = false;
+
+    /*
+     * first output any non-positional features
+     */
+    if (includeNonPositional)
     {
-      features = sequences[i].getSequenceFeatures();
-      if (features != null)
+      for (int i = 0; i < sequences.length; i++)
       {
-        for (int j = 0; j < features.length; j++)
+        String sequenceName = sequences[i].getName();
+        for (SequenceFeature feature : sequences[i].getFeatures()
+                .getNonPositionalFeatures())
         {
-          isnonpos = features[j].begin == 0 && features[j].end == 0;
-          if ((!nonpos && isnonpos) || (!isnonpos && visOnly
-                  && !visible.containsKey(features[j].type)))
-          {
-            continue;
-          }
-
-          if (features[j].featureGroup != null
-                  && !groups.contains(features[j].featureGroup))
-          {
-            groups.add(features[j].featureGroup);
-          }
+          foundSome = true;
+          out.append(formatJalviewFeature(sequenceName, feature));
         }
       }
     }
 
-    String group = null;
-    do
+    for (String group : sortedGroups)
     {
-      if (groups.size() > 0 && groupIndex < groups.size())
+      boolean isNamedGroup = (group != null && !"".equals(group));
+      if (isNamedGroup)
       {
-        group = groups.get(groupIndex);
         out.append(newline);
         out.append("STARTGROUP").append(TAB);
         out.append(group);
         out.append(newline);
       }
-      else
-      {
-        group = null;
-      }
 
+      /*
+       * output positional features within groups
+       */
       for (int i = 0; i < sequences.length; i++)
       {
-        features = sequences[i].getSequenceFeatures();
-        if (features != null)
+        String sequenceName = sequences[i].getName();
+        List<SequenceFeature> features = new ArrayList<SequenceFeature>();
+        if (types.length > 0)
         {
-          for (SequenceFeature sequenceFeature : features)
-          {
-            isnonpos = sequenceFeature.begin == 0
-                    && sequenceFeature.end == 0;
-            if ((!nonpos && isnonpos) || (!isnonpos && visOnly
-                    && !visible.containsKey(sequenceFeature.type)))
-            {
-              // skip if feature is nonpos and we ignore them or if we only
-              // output visible and it isn't non-pos and it's not visible
-              continue;
-            }
-
-            if (group != null && (sequenceFeature.featureGroup == null
-                    || !sequenceFeature.featureGroup.equals(group)))
-            {
-              continue;
-            }
+          features.addAll(sequences[i].getFeatures().getFeaturesForGroup(
+                  true, group, types));
+        }
 
-            if (group == null && sequenceFeature.featureGroup != null)
-            {
-              continue;
-            }
-            // we have features to output
-            featuresGen = 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);
-          }
+        for (SequenceFeature sequenceFeature : features)
+        {
+          foundSome = true;
+          out.append(formatJalviewFeature(sequenceName, sequenceFeature));
         }
       }
 
-      if (group != null)
+      if (isNamedGroup)
       {
         out.append("ENDGROUP").append(TAB);
         out.append(group);
         out.append(newline);
-        groupIndex++;
       }
-      else
+    }
+
+    return foundSome ? out.toString() : "No Features Visible";
+  }
+
+  /**
+   * @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)
       {
-        break;
+        out.append("<html>");
       }
 
-    } while (groupIndex < groups.size() + 1);
+      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>");
+        }
+      }
 
-    if (!featuresGen)
+      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))
     {
-      return "No Features Visible";
+      out.append(TAB);
+      out.append(sequenceFeature.score);
     }
+    out.append(newline);
 
     return out.toString();
   }
@@ -740,102 +717,90 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
   }
 
   /**
-   * Returns features output in GFF2 format, including hidden and non-positional
-   * features
-   * 
-   * @param sequences
-   *          the sequences whose features are to be output
-   * @param visible
-   *          a map whose keys are the type names of visible features
-   * @return
-   */
-  public String printGffFormat(SequenceI[] sequences,
-          Map<String, FeatureColourI> visible)
-  {
-    return printGffFormat(sequences, visible, true, true);
-  }
-
-  /**
    * Returns features output in GFF2 format
    * 
    * @param sequences
    *          the sequences whose features are to be output
    * @param visible
    *          a map whose keys are the type names of visible features
-   * @param outputVisibleOnly
+   * @param visibleFeatureGroups
    * @param includeNonPositionalFeatures
    * @return
    */
   public String printGffFormat(SequenceI[] sequences,
-          Map<String, FeatureColourI> visible, boolean outputVisibleOnly,
+          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;
-          }
-          // TODO why the test !isnonpos here?
-          // what about not visible non-positional features?
-          if (!isnonpos && outputVisibleOnly
-                  && !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);
       }
     }
 
@@ -1096,10 +1061,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);