JAL-3304 option to export linked features in Jalview format
[jalview.git] / src / jalview / io / FeaturesFile.java
index 70f2ac7..ada4140 100755 (executable)
@@ -29,11 +29,13 @@ import jalview.api.FeaturesSourceI;
 import jalview.datamodel.AlignedCodonFrame;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.MappedFeatures;
 import jalview.datamodel.SequenceDummy;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.datamodel.features.FeatureMatcherSet;
 import jalview.datamodel.features.FeatureMatcherSetI;
+import jalview.gui.Desktop;
 import jalview.io.gff.GffHelperBase;
 import jalview.io.gff.GffHelperFactory;
 import jalview.io.gff.GffHelperI;
@@ -49,9 +51,11 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.TreeMap;
 
 /**
  * Parses and writes features files, which may be in Jalview, GFF2 or GFF3
@@ -563,32 +567,31 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
   }
 
   /**
-   * Returns contents of a Jalview format features file, for visible features,
-   * as filtered by type and group. Features with a null group are displayed if
-   * their feature type is visible. Non-positional features may optionally be
-   * included (with no check on type or group).
+   * Returns contents of a Jalview format features file, for visible features, as
+   * filtered by type and group. Features with a null group are displayed if their
+   * feature type is visible. Non-positional features may optionally be included
+   * (with no check on type or group).
    * 
    * @param sequences
    * @param fr
    * @param includeNonPositional
-   *          if true, include non-positional features (regardless of group or
-   *          type)
+   *                               if true, include non-positional features
+   *                               (regardless of group or type)
+   * @param includeComplement
+   *                               if true, include visible complementary
+   *                               (CDS/protein) positional features, with
+   *                               locations converted to local sequence
+   *                               coordinates
    * @return
    */
   public String printJalviewFormat(SequenceI[] sequences,
-          FeatureRenderer fr, boolean includeNonPositional)
+          FeatureRenderer fr, boolean includeNonPositional,
+          boolean includeComplement)
   {
     Map<String, FeatureColourI> visibleColours = fr
             .getDisplayedFeatureCols();
     Map<String, FeatureMatcherSetI> featureFilters = fr.getFeatureFilters();
 
-    if (!includeNonPositional
-            && (visibleColours == null || visibleColours.isEmpty()))
-    {
-      // no point continuing.
-      return "No Features Visible";
-    }
-
     /*
      * write out feature colours (if we know them)
      */
@@ -620,10 +623,125 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
     int count = outputFeaturesByGroup(out, fr, types, sequences,
             includeNonPositional);
 
+    if (includeComplement)
+    {
+      count += outputComplementFeatures(out, fr, sequences);
+    }
+
     return count > 0 ? out.toString() : "No Features Visible";
   }
 
   /**
+   * Outputs any visible complementary positional features, within feature group
+   * 
+   * @param out
+   * @param fr
+   * @param sequences
+   * @return
+   */
+  private int outputComplementFeatures(StringBuilder out,
+          FeatureRenderer fr, SequenceI[] sequences)
+  {
+    AlignViewportI comp = fr.getViewport().getCodingComplement();
+    FeatureRenderer fr2 = Desktop.getAlignFrameFor(comp)
+            .getFeatureRenderer();
+
+    /*
+     * build a map of {group, {seqName, List<SequenceFeature>}}
+     */
+    Map<String, Map<String, List<SequenceFeature>>> map = new TreeMap<>();
+    int count = 0;
+
+    for (SequenceI seq : sequences)
+    {
+      /*
+       * avoid duplication of features (e.g. peptide feature 
+       * at all 3 mapped codon positions)
+       */
+      List<SequenceFeature> found = new ArrayList<>();
+      String seqName = seq.getName();
+
+      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)
+          {
+            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++;
+            }
+          }
+        }
+      }
+    }
+
+    /*
+     * output features by group
+     */
+    for (Entry<String, Map<String, List<SequenceFeature>>> groupFeatures : map.entrySet())
+    {
+      out.append(newline);
+      String group = groupFeatures.getKey();
+      if (!"".equals(group))
+      {
+        out.append(STARTGROUP).append(TAB).append(group).append(newline);
+      }
+      Map<String, List<SequenceFeature>> seqFeaturesMap = groupFeatures
+              .getValue();
+      for (Entry<String, List<SequenceFeature>> seqFeatures : seqFeaturesMap
+              .entrySet())
+      {
+        String sequenceName = seqFeatures.getKey();
+        for (SequenceFeature sf : seqFeatures.getValue())
+        {
+          out.append(formatJalviewFeature(sequenceName, sf));
+        }
+      }
+      if (!"".equals(group))
+      {
+        out.append(ENDGROUP).append(TAB).append(group).append(newline);
+      }
+    }
+
+    return count;
+  }
+
+  /**
    * Outputs any feature filters defined for visible feature types, sandwiched by
    * STARTFILTERS and ENDFILTERS lines
    * 
@@ -876,15 +994,19 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
    * Returns features output in GFF2 format
    * 
    * @param sequences
-   *          the sequences whose features are to be output
+   *                                       the sequences whose features are to be
+   *                                       output
    * @param visible
-   *          a map whose keys are the type names of visible features
+   *                                       a map whose keys are the type names of
+   *                                       visible features
    * @param visibleFeatureGroups
    * @param includeNonPositionalFeatures
+   * @param includeComplement
    * @return
    */
   public String printGffFormat(SequenceI[] sequences,
-          FeatureRenderer fr, boolean includeNonPositionalFeatures)
+          FeatureRenderer fr, boolean includeNonPositionalFeatures,
+          boolean includeComplement)
   {
     Map<String, FeatureColourI> visibleColours = fr.getDisplayedFeatureCols();