JAL-3304 option to export linked features also for GFF format
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 18 Jun 2019 13:23:50 +0000 (14:23 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 18 Jun 2019 13:23:50 +0000 (14:23 +0100)
src/jalview/io/FeaturesFile.java

index ada4140..fffa650 100755 (executable)
@@ -632,7 +632,9 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
   }
 
   /**
-   * Outputs any visible complementary positional features, within feature group
+   * 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
@@ -647,66 +649,36 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
             .getFeatureRenderer();
 
     /*
-     * build a map of {group, {seqName, List<SequenceFeature>}}
+     * bin features by feature group and sequence
      */
-    Map<String, Map<String, List<SequenceFeature>>> map = new TreeMap<>();
+    Map<String, Map<String, List<SequenceFeature>>> map = new TreeMap<>(
+            String.CASE_INSENSITIVE_ORDER);
     int count = 0;
 
     for (SequenceI seq : sequences)
     {
       /*
-       * avoid duplication of features (e.g. peptide feature 
-       * at all 3 mapped codon positions)
+       * find complementary features
        */
-      List<SequenceFeature> found = new ArrayList<>();
+      List<SequenceFeature> complementary = findComplementaryFeatures(seq,
+              fr2);
       String seqName = seq.getName();
 
-      for (int pos = seq.getStart(); pos <= seq.getEnd(); pos++)
+      for (SequenceFeature sf : complementary)
       {
-        MappedFeatures mf = fr2.findComplementFeaturesAtResidue(seq, pos);
-
-        if (mf != null)
+        String group = sf.getFeatureGroup();
+        if (!map.containsKey(group))
         {
-          MapList mapping = mf.mapping.getMap();
-          for (SequenceFeature sf : mf.features)
-          {
-            String group = sf.getFeatureGroup();
-            if (group == null)
-            {
-              group = "";
-            }
-            if (!map.containsKey(group))
-            {
-              map.put(group, new LinkedHashMap<>());
-            }
-            Map<String, List<SequenceFeature>> groupFeatures = map
-                    .get(group);
-            if (!groupFeatures.containsKey(seqName))
-            {
-              groupFeatures.put(seqName, new ArrayList<>());
-            }
-            List<SequenceFeature> foundFeatures = groupFeatures
-                    .get(seqName);
-
-            /*
-             * make a virtual feature with local coordinates
-             */
-            if (!found.contains(sf))
-            {
-              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());
-              foundFeatures.add(sf2);
-              count++;
-            }
-          }
+          map.put(group, new LinkedHashMap<>()); // preserves sequence order
         }
+        Map<String, List<SequenceFeature>> groupFeatures = map.get(group);
+        if (!groupFeatures.containsKey(seqName))
+        {
+          groupFeatures.put(seqName, new ArrayList<>());
+        }
+        List<SequenceFeature> foundFeatures = groupFeatures.get(seqName);
+        foundFeatures.add(sf);
+        count++;
       }
     }
 
@@ -729,7 +701,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
         String sequenceName = seqFeatures.getKey();
         for (SequenceFeature sf : seqFeatures.getValue())
         {
-          out.append(formatJalviewFeature(sequenceName, sf));
+          formatJalviewFeature(out, sequenceName, sf);
         }
       }
       if (!"".equals(group))
@@ -741,6 +713,52 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
     return count;
   }
 
+  protected List<SequenceFeature> findComplementaryFeatures(SequenceI seq,
+          FeatureRenderer fr2)
+  {
+    /*
+     * avoid duplication of features (e.g. peptide feature 
+     * at all 3 mapped codon positions)
+     */
+    List<SequenceFeature> found = new ArrayList<>();
+    List<SequenceFeature> 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
@@ -863,7 +881,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
               }
             }
             firstInGroup = false;
-            out.append(formatJalviewFeature(sequenceName, sf));
+            formatJalviewFeature(out, sequenceName, sf);
           }
         }
       }
@@ -877,14 +895,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(""))
     {
@@ -909,7 +929,8 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
 
           if (sequenceFeature.description.indexOf(href) == -1)
           {
-            out.append(" <a href=\"" + href + "\">" + label + "</a>");
+            out.append(" <a href=\"").append(href).append("\">")
+                    .append(label).append("</a>");
           }
         }
 
@@ -934,8 +955,6 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
       out.append(sequenceFeature.score);
     }
     out.append(newline);
-
-    return out.toString();
   }
 
   /**
@@ -1008,24 +1027,26 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
           FeatureRenderer fr, boolean includeNonPositionalFeatures,
           boolean includeComplement)
   {
+    FeatureRenderer fr2 = null;
+    if (includeComplement)
+    {
+      AlignViewportI comp = fr.getViewport().getCodingComplement();
+      fr2 = Desktop.getAlignFrameFor(comp).getFeatureRenderer();
+    }
+
     Map<String, FeatureColourI> 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<SequenceFeature> seqFeatures = new ArrayList<>();
       List<SequenceFeature> features = new ArrayList<>();
       if (includeNonPositionalFeatures)
       {
@@ -1035,51 +1056,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);
       }
     }
@@ -1088,6 +1087,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)
    *