12ad0d4891892bf13e58f25414d1b6e4a31b6374
[jalview.git] / src / jalview / io / FeaturesFile.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.io;
22
23 import jalview.analysis.AlignmentUtils;
24 import jalview.analysis.SequenceIdMatcher;
25 import jalview.api.AlignViewportI;
26 import jalview.api.FeatureColourI;
27 import jalview.api.FeatureRenderer;
28 import jalview.api.FeaturesSourceI;
29 import jalview.datamodel.AlignedCodonFrame;
30 import jalview.datamodel.Alignment;
31 import jalview.datamodel.AlignmentI;
32 import jalview.datamodel.SequenceDummy;
33 import jalview.datamodel.SequenceFeature;
34 import jalview.datamodel.SequenceI;
35 import jalview.datamodel.features.FeatureMatcherSet;
36 import jalview.datamodel.features.FeatureMatcherSetI;
37 import jalview.io.gff.GffHelperBase;
38 import jalview.io.gff.GffHelperFactory;
39 import jalview.io.gff.GffHelperI;
40 import jalview.schemes.FeatureColour;
41 import jalview.util.ColorUtils;
42 import jalview.util.MapList;
43 import jalview.util.ParseHtmlBodyAndLinks;
44 import jalview.util.StringUtils;
45
46 import java.awt.Color;
47 import java.io.IOException;
48 import java.util.ArrayList;
49 import java.util.Arrays;
50 import java.util.Collections;
51 import java.util.HashMap;
52 import java.util.List;
53 import java.util.Map;
54 import java.util.Map.Entry;
55
56 /**
57  * Parses and writes features files, which may be in Jalview, GFF2 or GFF3
58  * format. These are tab-delimited formats but with differences in the use of
59  * columns.
60  * 
61  * A Jalview feature file may define feature colours and then declare that the
62  * remainder of the file is in GFF format with the line 'GFF'.
63  * 
64  * GFF3 files may include alignment mappings for features, which Jalview will
65  * attempt to model, and may include sequence data following a ##FASTA line.
66  * 
67  * 
68  * @author AMW
69  * @author jbprocter
70  * @author gmcarstairs
71  */
72 public class FeaturesFile extends AlignFile implements FeaturesSourceI
73 {
74   private static final String TAB_REGEX = "\\t";
75
76   private static final String STARTGROUP = "STARTGROUP";
77
78   private static final String ENDGROUP = "ENDGROUP";
79
80   private static final String STARTFILTERS = "STARTFILTERS";
81
82   private static final String ENDFILTERS = "ENDFILTERS";
83
84   private static final String ID_NOT_SPECIFIED = "ID_NOT_SPECIFIED";
85
86   private static final String NOTE = "Note";
87
88   protected static final String GFF_VERSION = "##gff-version";
89
90   private AlignmentI lastmatchedAl = null;
91
92   private SequenceIdMatcher matcher = null;
93
94   protected AlignmentI dataset;
95
96   protected int gffVersion;
97
98   /**
99    * Creates a new FeaturesFile object.
100    */
101   public FeaturesFile()
102   {
103   }
104
105   /**
106    * Constructor which does not parse the file immediately
107    * 
108    * @param file
109    * @param paste
110    * @throws IOException
111    */
112   public FeaturesFile(String file, DataSourceType paste)
113           throws IOException
114   {
115     super(false, file, paste);
116   }
117
118   /**
119    * @param source
120    * @throws IOException
121    */
122   public FeaturesFile(FileParse source) throws IOException
123   {
124     super(source);
125   }
126
127   /**
128    * Constructor that optionally parses the file immediately
129    * 
130    * @param parseImmediately
131    * @param file
132    * @param type
133    * @throws IOException
134    */
135   public FeaturesFile(boolean parseImmediately, String file,
136           DataSourceType type) throws IOException
137   {
138     super(parseImmediately, file, type);
139   }
140
141   /**
142    * Parse GFF or sequence features file using case-independent matching,
143    * discarding URLs
144    * 
145    * @param align
146    *          - alignment/dataset containing sequences that are to be annotated
147    * @param colours
148    *          - hashtable to store feature colour definitions
149    * @param removeHTML
150    *          - process html strings into plain text
151    * @return true if features were added
152    */
153   public boolean parse(AlignmentI align,
154           Map<String, FeatureColourI> colours, boolean removeHTML)
155   {
156     return parse(align, colours, removeHTML, false);
157   }
158
159   /**
160    * Extends the default addProperties by also adding peptide-to-cDNA mappings
161    * (if any) derived while parsing a GFF file
162    */
163   @Override
164   public void addProperties(AlignmentI al)
165   {
166     super.addProperties(al);
167     if (dataset != null && dataset.getCodonFrames() != null)
168     {
169       AlignmentI ds = (al.getDataset() == null) ? al : al.getDataset();
170       for (AlignedCodonFrame codons : dataset.getCodonFrames())
171       {
172         ds.addCodonFrame(codons);
173       }
174     }
175   }
176
177   /**
178    * Parse GFF or Jalview format sequence features file
179    * 
180    * @param align
181    *          - alignment/dataset containing sequences that are to be annotated
182    * @param colours
183    *          - map to store feature colour definitions
184    * @param removeHTML
185    *          - process html strings into plain text
186    * @param relaxedIdmatching
187    *          - when true, ID matches to compound sequence IDs are allowed
188    * @return true if features were added
189    */
190   public boolean parse(AlignmentI align,
191           Map<String, FeatureColourI> colours, boolean removeHTML,
192           boolean relaxedIdmatching)
193   {
194     return parse(align, colours, null, removeHTML, relaxedIdmatching);
195   }
196
197   /**
198    * Parse GFF or Jalview format sequence features file
199    * 
200    * @param align
201    *          - alignment/dataset containing sequences that are to be annotated
202    * @param colours
203    *          - map to store feature colour definitions
204    * @param filters
205    *          - map to store feature filter definitions
206    * @param removeHTML
207    *          - process html strings into plain text
208    * @param relaxedIdmatching
209    *          - when true, ID matches to compound sequence IDs are allowed
210    * @return true if features were added
211    */
212   public boolean parse(AlignmentI align,
213           Map<String, FeatureColourI> colours,
214           Map<String, FeatureMatcherSetI> filters, boolean removeHTML,
215           boolean relaxedIdmatching)
216   {
217     Map<String, String> gffProps = new HashMap<>();
218     /*
219      * keep track of any sequences we try to create from the data
220      */
221     List<SequenceI> newseqs = new ArrayList<>();
222
223     String line = null;
224     try
225     {
226       String[] gffColumns;
227       String featureGroup = null;
228
229       while ((line = nextLine()) != null)
230       {
231         // skip comments/process pragmas
232         if (line.length() == 0 || line.startsWith("#"))
233         {
234           if (line.toLowerCase().startsWith("##"))
235           {
236             processGffPragma(line, gffProps, align, newseqs);
237           }
238           continue;
239         }
240
241         gffColumns = line.split(TAB_REGEX);
242         if (gffColumns.length == 1)
243         {
244           if (line.trim().equalsIgnoreCase("GFF"))
245           {
246             /*
247              * Jalview features file with appended GFF
248              * assume GFF2 (though it may declare ##gff-version 3)
249              */
250             gffVersion = 2;
251             continue;
252           }
253         }
254
255         if (gffColumns.length > 0 && gffColumns.length < 4)
256         {
257           /*
258            * if 2 or 3 tokens, we anticipate either 'startgroup', 'endgroup' or
259            * a feature type colour specification
260            */
261           String ft = gffColumns[0];
262           if (ft.equalsIgnoreCase(STARTFILTERS))
263           {
264             parseFilters(filters);
265             continue;
266           }
267           if (ft.equalsIgnoreCase(STARTGROUP))
268           {
269             featureGroup = gffColumns[1];
270           }
271           else if (ft.equalsIgnoreCase(ENDGROUP))
272           {
273             // We should check whether this is the current group,
274             // but at present there's no way of showing more than 1 group
275             featureGroup = null;
276           }
277           else
278           {
279             String colscheme = gffColumns[1];
280             FeatureColourI colour = FeatureColour
281                     .parseJalviewFeatureColour(colscheme);
282             if (colour != null)
283             {
284               colours.put(ft, colour);
285             }
286           }
287           continue;
288         }
289
290         /*
291          * if not a comment, GFF pragma, startgroup, endgroup or feature
292          * colour specification, that just leaves a feature details line
293          * in either Jalview or GFF format
294          */
295         if (gffVersion == 0)
296         {
297           parseJalviewFeature(line, gffColumns, align, colours, removeHTML,
298                   relaxedIdmatching, featureGroup);
299         }
300         else
301         {
302           parseGff(gffColumns, align, relaxedIdmatching, newseqs);
303         }
304       }
305       resetMatcher();
306     } catch (Exception ex)
307     {
308       // should report somewhere useful for UI if necessary
309       warningMessage = ((warningMessage == null) ? "" : warningMessage)
310               + "Parsing error at\n" + line;
311       System.out.println("Error parsing feature file: " + ex + "\n" + line);
312       ex.printStackTrace(System.err);
313       resetMatcher();
314       return false;
315     }
316
317     /*
318      * experimental - add any dummy sequences with features to the alignment
319      * - we need them for Ensembl feature extraction - though maybe not otherwise
320      */
321     for (SequenceI newseq : newseqs)
322     {
323       if (newseq.getFeatures().hasFeatures())
324       {
325         align.addSequence(newseq);
326       }
327     }
328     return true;
329   }
330
331   /**
332    * Reads input lines from STARTFILTERS to ENDFILTERS and adds a feature type
333    * filter to the map for each line parsed. After exit from this method,
334    * nextLine() should return the line after ENDFILTERS (or we are already at
335    * end of file if ENDFILTERS was missing).
336    * 
337    * @param filters
338    * @throws IOException
339    */
340   protected void parseFilters(Map<String, FeatureMatcherSetI> filters)
341           throws IOException
342   {
343     String line;
344     while ((line = nextLine()) != null)
345     {
346       if (line.toUpperCase().startsWith(ENDFILTERS))
347       {
348         return;
349       }
350       String[] tokens = line.split(TAB_REGEX);
351       if (tokens.length != 2)
352       {
353         System.err.println(String.format("Invalid token count %d for %d",
354                 tokens.length, line));
355       }
356       else
357       {
358         String featureType = tokens[0];
359         FeatureMatcherSetI fm = FeatureMatcherSet.fromString(tokens[1]);
360         if (fm != null && filters != null)
361         {
362           filters.put(featureType, fm);
363         }
364       }
365     }
366   }
367
368   /**
369    * Try to parse a Jalview format feature specification and add it as a
370    * sequence feature to any matching sequences in the alignment. Returns true
371    * if successful (a feature was added), or false if not.
372    * 
373    * @param line
374    * @param gffColumns
375    * @param alignment
376    * @param featureColours
377    * @param removeHTML
378    * @param relaxedIdmatching
379    * @param featureGroup
380    */
381   protected boolean parseJalviewFeature(String line, String[] gffColumns,
382           AlignmentI alignment, Map<String, FeatureColourI> featureColours,
383           boolean removeHTML, boolean relaxedIdMatching,
384           String featureGroup)
385   {
386     /*
387      * tokens: description seqid seqIndex start end type [score]
388      */
389     if (gffColumns.length < 6)
390     {
391       System.err.println("Ignoring feature line '" + line
392               + "' with too few columns (" + gffColumns.length + ")");
393       return false;
394     }
395     String desc = gffColumns[0];
396     String seqId = gffColumns[1];
397     SequenceI seq = findSequence(seqId, alignment, null, relaxedIdMatching);
398
399     if (!ID_NOT_SPECIFIED.equals(seqId))
400     {
401       seq = findSequence(seqId, alignment, null, relaxedIdMatching);
402     }
403     else
404     {
405       seqId = null;
406       seq = null;
407       String seqIndex = gffColumns[2];
408       try
409       {
410         int idx = Integer.parseInt(seqIndex);
411         seq = alignment.getSequenceAt(idx);
412       } catch (NumberFormatException ex)
413       {
414         System.err.println("Invalid sequence index: " + seqIndex);
415       }
416     }
417
418     if (seq == null)
419     {
420       System.out.println("Sequence not found: " + line);
421       return false;
422     }
423
424     int startPos = Integer.parseInt(gffColumns[3]);
425     int endPos = Integer.parseInt(gffColumns[4]);
426
427     String ft = gffColumns[5];
428
429     if (!featureColours.containsKey(ft))
430     {
431       /* 
432        * Perhaps an old style groups file with no colours -
433        * synthesize a colour from the feature type
434        */
435       Color colour = ColorUtils.createColourFromName(ft);
436       featureColours.put(ft, new FeatureColour(colour));
437     }
438     SequenceFeature sf = null;
439     if (gffColumns.length > 6)
440     {
441       float score = Float.NaN;
442       try
443       {
444         score = new Float(gffColumns[6]).floatValue();
445       } catch (NumberFormatException ex)
446       {
447         sf = new SequenceFeature(ft, desc, startPos, endPos, featureGroup);
448       }
449       sf = new SequenceFeature(ft, desc, startPos, endPos, score,
450               featureGroup);
451     }
452     else
453     {
454       sf = new SequenceFeature(ft, desc, startPos, endPos, featureGroup);
455     }
456
457     parseDescriptionHTML(sf, removeHTML);
458
459     seq.addSequenceFeature(sf);
460
461     while (seqId != null
462             && (seq = alignment.findName(seq, seqId, false)) != null)
463     {
464       seq.addSequenceFeature(new SequenceFeature(sf));
465     }
466     return true;
467   }
468
469   /**
470    * clear any temporary handles used to speed up ID matching
471    */
472   protected void resetMatcher()
473   {
474     lastmatchedAl = null;
475     matcher = null;
476   }
477
478   /**
479    * Returns a sequence matching the given id, as follows
480    * <ul>
481    * <li>strict matching is on exact sequence name</li>
482    * <li>relaxed matching allows matching on a token within the sequence name,
483    * or a dbxref</li>
484    * <li>first tries to find a match in the alignment sequences</li>
485    * <li>else tries to find a match in the new sequences already generated while
486    * parsing the features file</li>
487    * <li>else creates a new placeholder sequence, adds it to the new sequences
488    * list, and returns it</li>
489    * </ul>
490    * 
491    * @param seqId
492    * @param align
493    * @param newseqs
494    * @param relaxedIdMatching
495    * 
496    * @return
497    */
498   protected SequenceI findSequence(String seqId, AlignmentI align,
499           List<SequenceI> newseqs, boolean relaxedIdMatching)
500   {
501     // TODO encapsulate in SequenceIdMatcher, share the matcher
502     // with the GffHelper (removing code duplication)
503     SequenceI match = null;
504     if (relaxedIdMatching)
505     {
506       if (lastmatchedAl != align)
507       {
508         lastmatchedAl = align;
509         matcher = new SequenceIdMatcher(align.getSequencesArray());
510         if (newseqs != null)
511         {
512           matcher.addAll(newseqs);
513         }
514       }
515       match = matcher.findIdMatch(seqId);
516     }
517     else
518     {
519       match = align.findName(seqId, true);
520       if (match == null && newseqs != null)
521       {
522         for (SequenceI m : newseqs)
523         {
524           if (seqId.equals(m.getName()))
525           {
526             return m;
527           }
528         }
529       }
530
531     }
532     if (match == null && newseqs != null)
533     {
534       match = new SequenceDummy(seqId);
535       if (relaxedIdMatching)
536       {
537         matcher.addAll(Arrays.asList(new SequenceI[] { match }));
538       }
539       // add dummy sequence to the newseqs list
540       newseqs.add(match);
541     }
542     return match;
543   }
544
545   public void parseDescriptionHTML(SequenceFeature sf, boolean removeHTML)
546   {
547     if (sf.getDescription() == null)
548     {
549       return;
550     }
551     ParseHtmlBodyAndLinks parsed = new ParseHtmlBodyAndLinks(
552             sf.getDescription(), removeHTML, newline);
553
554     if (removeHTML)
555     {
556       sf.setDescription(parsed.getNonHtmlContent());
557     }
558
559     for (String link : parsed.getLinks())
560     {
561       sf.addLink(link);
562     }
563   }
564
565   /**
566    * Returns contents of a Jalview format features file, for visible features,
567    * as filtered by type and group. Features with a null group are displayed if
568    * their feature type is visible. Non-positional features may optionally be
569    * included (with no check on type or group).
570    * 
571    * @param sequences
572    * @param fr
573    * @param includeNonPositional
574    *          if true, include non-positional features (regardless of group or
575    *          type)
576    * @return
577    */
578   public String printJalviewFormat(SequenceI[] sequences,
579           FeatureRenderer fr, boolean includeNonPositional)
580   {
581     Map<String, FeatureColourI> visibleColours = fr
582             .getDisplayedFeatureCols();
583     Map<String, FeatureMatcherSetI> featureFilters = fr.getFeatureFilters();
584
585     if (!includeNonPositional
586             && (visibleColours == null || visibleColours.isEmpty()))
587     {
588       // no point continuing.
589       return "No Features Visible";
590     }
591
592     /*
593      * write out feature colours (if we know them)
594      */
595     // TODO: decide if feature links should also be written here ?
596     StringBuilder out = new StringBuilder(256);
597     if (visibleColours != null)
598     {
599       for (Entry<String, FeatureColourI> featureColour : visibleColours
600               .entrySet())
601       {
602         FeatureColourI colour = featureColour.getValue();
603         out.append(colour.toJalviewFormat(featureColour.getKey())).append(
604                 newline);
605       }
606     }
607
608     String[] types = visibleColours == null ? new String[0]
609             : visibleColours.keySet()
610                     .toArray(new String[visibleColours.keySet().size()]);
611
612     /*
613      * feature filters if any
614      */
615     outputFeatureFilters(out, visibleColours, featureFilters);
616
617     /*
618      * output features within groups
619      */
620     int count = outputFeaturesByGroup(out, fr, types, sequences,
621             includeNonPositional);
622
623     return count > 0 ? out.toString() : "No Features Visible";
624   }
625
626   /**
627    * Outputs any feature filters defined for visible feature types, sandwiched by
628    * STARTFILTERS and ENDFILTERS lines
629    * 
630    * @param out
631    * @param visible
632    * @param featureFilters
633    */
634   void outputFeatureFilters(StringBuilder out,
635           Map<String, FeatureColourI> visible,
636           Map<String, FeatureMatcherSetI> featureFilters)
637   {
638     if (visible == null || featureFilters == null
639             || featureFilters.isEmpty())
640     {
641       return;
642     }
643
644     boolean first = true;
645     for (String featureType : visible.keySet())
646     {
647       FeatureMatcherSetI filter = featureFilters.get(featureType);
648       if (filter != null)
649       {
650         if (first)
651         {
652           first = false;
653           out.append(newline).append(STARTFILTERS).append(newline);
654         }
655         out.append(featureType).append(TAB).append(filter.toStableString())
656                 .append(newline);
657       }
658     }
659     if (!first)
660     {
661       out.append(ENDFILTERS).append(newline);
662     }
663
664   }
665
666   /**
667    * Appends output of visible sequence features within feature groups to the
668    * output buffer. Groups other than the null or empty group are sandwiched by
669    * STARTGROUP and ENDGROUP lines. Answers the number of features written.
670    * 
671    * @param out
672    * @param fr
673    * @param featureTypes
674    * @param sequences
675    * @param includeNonPositional
676    * @return
677    */
678   private int outputFeaturesByGroup(StringBuilder out,
679           FeatureRenderer fr, String[] featureTypes,
680           SequenceI[] sequences, boolean includeNonPositional)
681   {
682     List<String> featureGroups = fr.getFeatureGroups();
683
684     /*
685      * sort groups alphabetically, and ensure that features with a
686      * null or empty group are output after those in named groups
687      */
688     List<String> sortedGroups = new ArrayList<>(featureGroups);
689     sortedGroups.remove(null);
690     sortedGroups.remove("");
691     Collections.sort(sortedGroups);
692     sortedGroups.add(null);
693     sortedGroups.add("");
694
695     int count = 0;
696     List<String> visibleGroups = fr.getDisplayedFeatureGroups();
697
698     /*
699      * loop over all groups (may be visible or not);
700      * non-positional features are output even if group is not visible
701      */
702     for (String group : sortedGroups)
703     {
704       boolean firstInGroup = true;
705       boolean isNullGroup = group == null || "".equals(group);
706
707       for (int i = 0; i < sequences.length; i++)
708       {
709         String sequenceName = sequences[i].getName();
710         List<SequenceFeature> features = new ArrayList<>();
711
712         /*
713          * get any non-positional features in this group, if wanted
714          * (for any feature type, whether visible or not)
715          */
716         if (includeNonPositional)
717         {
718           features.addAll(sequences[i].getFeatures()
719                   .getFeaturesForGroup(false, group));
720         }
721
722         /*
723          * add positional features for visible feature types, but
724          * (for named groups) only if feature group is visible
725          */
726         if (featureTypes.length > 0
727                 && (isNullGroup || visibleGroups.contains(group)))
728         {
729           features.addAll(sequences[i].getFeatures().getFeaturesForGroup(
730                   true, group, featureTypes));
731         }
732
733         for (SequenceFeature sf : features)
734         {
735           if (sf.isNonPositional() || fr.isVisible(sf))
736           {
737             count++;
738             if (firstInGroup)
739             {
740               out.append(newline);
741               if (!isNullGroup)
742               {
743                 out.append(STARTGROUP).append(TAB).append(group)
744                         .append(newline);
745               }
746             }
747             firstInGroup = false;
748             out.append(formatJalviewFeature(sequenceName, sf));
749           }
750         }
751       }
752
753       if (!isNullGroup && !firstInGroup)
754       {
755         out.append(ENDGROUP).append(TAB).append(group).append(newline);
756       }
757     }
758     return count;
759   }
760
761   /**
762    * @param out
763    * @param sequenceName
764    * @param sequenceFeature
765    */
766   protected String formatJalviewFeature(
767           String sequenceName, SequenceFeature sequenceFeature)
768   {
769     StringBuilder out = new StringBuilder(64);
770     if (sequenceFeature.description == null
771             || sequenceFeature.description.equals(""))
772     {
773       out.append(sequenceFeature.type).append(TAB);
774     }
775     else
776     {
777       if (sequenceFeature.links != null
778               && sequenceFeature.getDescription().indexOf("<html>") == -1)
779       {
780         out.append("<html>");
781       }
782
783       out.append(sequenceFeature.description);
784       if (sequenceFeature.links != null)
785       {
786         for (int l = 0; l < sequenceFeature.links.size(); l++)
787         {
788           String label = sequenceFeature.links.elementAt(l);
789           String href = label.substring(label.indexOf("|") + 1);
790           label = label.substring(0, label.indexOf("|"));
791
792           if (sequenceFeature.description.indexOf(href) == -1)
793           {
794             out.append(" <a href=\"" + href + "\">" + label + "</a>");
795           }
796         }
797
798         if (sequenceFeature.getDescription().indexOf("</html>") == -1)
799         {
800           out.append("</html>");
801         }
802       }
803
804       out.append(TAB);
805     }
806     out.append(sequenceName);
807     out.append("\t-1\t");
808     out.append(sequenceFeature.begin);
809     out.append(TAB);
810     out.append(sequenceFeature.end);
811     out.append(TAB);
812     out.append(sequenceFeature.type);
813     if (!Float.isNaN(sequenceFeature.score))
814     {
815       out.append(TAB);
816       out.append(sequenceFeature.score);
817     }
818     out.append(newline);
819
820     return out.toString();
821   }
822
823   /**
824    * Parse method that is called when a GFF file is dragged to the desktop
825    */
826   @Override
827   public void parse()
828   {
829     AlignViewportI av = getViewport();
830     if (av != null)
831     {
832       if (av.getAlignment() != null)
833       {
834         dataset = av.getAlignment().getDataset();
835       }
836       if (dataset == null)
837       {
838         // working in the applet context ?
839         dataset = av.getAlignment();
840       }
841     }
842     else
843     {
844       dataset = new Alignment(new SequenceI[] {});
845     }
846
847     Map<String, FeatureColourI> featureColours = new HashMap<>();
848     boolean parseResult = parse(dataset, featureColours, false, true);
849     if (!parseResult)
850     {
851       // pass error up somehow
852     }
853     if (av != null)
854     {
855       // update viewport with the dataset data ?
856     }
857     else
858     {
859       setSeqs(dataset.getSequencesArray());
860     }
861   }
862
863   /**
864    * Implementation of unused abstract method
865    * 
866    * @return error message
867    */
868   @Override
869   public String print(SequenceI[] sqs, boolean jvsuffix)
870   {
871     System.out.println("Use printGffFormat() or printJalviewFormat()");
872     return null;
873   }
874
875   /**
876    * Returns features output in GFF2 format
877    * 
878    * @param sequences
879    *          the sequences whose features are to be output
880    * @param visible
881    *          a map whose keys are the type names of visible features
882    * @param visibleFeatureGroups
883    * @param includeNonPositionalFeatures
884    * @return
885    */
886   public String printGffFormat(SequenceI[] sequences,
887           FeatureRenderer fr, boolean includeNonPositionalFeatures)
888   {
889     Map<String, FeatureColourI> visibleColours = fr.getDisplayedFeatureCols();
890
891     StringBuilder out = new StringBuilder(256);
892
893     out.append(String.format("%s %d\n", GFF_VERSION, gffVersion == 0 ? 2 : gffVersion));
894
895     if (!includeNonPositionalFeatures
896             && (visibleColours == null || visibleColours.isEmpty()))
897     {
898       return out.toString();
899     }
900
901     String[] types = visibleColours == null ? new String[0]
902             : visibleColours.keySet()
903                     .toArray(new String[visibleColours.keySet().size()]);
904
905     for (SequenceI seq : sequences)
906     {
907       List<SequenceFeature> features = new ArrayList<>();
908       if (includeNonPositionalFeatures)
909       {
910         features.addAll(seq.getFeatures().getNonPositionalFeatures());
911       }
912       if (visibleColours != null && !visibleColours.isEmpty())
913       {
914         features.addAll(seq.getFeatures().getPositionalFeatures(types));
915       }
916
917       for (SequenceFeature sf : features)
918       {
919         if (!sf.isNonPositional() && !fr.isVisible(sf))
920         {
921           /*
922            * feature hidden by group visibility, colour threshold,
923            * or feature filter condition
924            */
925           continue;
926         }
927
928         String source = sf.featureGroup;
929         if (source == null)
930         {
931           source = sf.getDescription();
932         }
933
934         out.append(seq.getName());
935         out.append(TAB);
936         out.append(source);
937         out.append(TAB);
938         out.append(sf.type);
939         out.append(TAB);
940         out.append(sf.begin);
941         out.append(TAB);
942         out.append(sf.end);
943         out.append(TAB);
944         out.append(sf.score);
945         out.append(TAB);
946
947         int strand = sf.getStrand();
948         out.append(strand == 1 ? "+" : (strand == -1 ? "-" : "."));
949         out.append(TAB);
950
951         String phase = sf.getPhase();
952         out.append(phase == null ? "." : phase);
953
954         // miscellaneous key-values (GFF column 9)
955         String attributes = sf.getAttributes();
956         if (attributes != null)
957         {
958           out.append(TAB).append(attributes);
959         }
960
961         out.append(newline);
962       }
963     }
964
965     return out.toString();
966   }
967
968   /**
969    * Returns a mapping given list of one or more Align descriptors (exonerate
970    * format)
971    * 
972    * @param alignedRegions
973    *          a list of "Align fromStart toStart fromCount"
974    * @param mapIsFromCdna
975    *          if true, 'from' is dna, else 'from' is protein
976    * @param strand
977    *          either 1 (forward) or -1 (reverse)
978    * @return
979    * @throws IOException
980    */
981   protected MapList constructCodonMappingFromAlign(
982           List<String> alignedRegions, boolean mapIsFromCdna, int strand)
983           throws IOException
984   {
985     if (strand == 0)
986     {
987       throw new IOException(
988               "Invalid strand for a codon mapping (cannot be 0)");
989     }
990     int regions = alignedRegions.size();
991     // arrays to hold [start, end] for each aligned region
992     int[] fromRanges = new int[regions * 2]; // from dna
993     int[] toRanges = new int[regions * 2]; // to protein
994     int fromRangesIndex = 0;
995     int toRangesIndex = 0;
996
997     for (String range : alignedRegions)
998     {
999       /* 
1000        * Align mapFromStart mapToStart mapFromCount
1001        * e.g. if mapIsFromCdna
1002        *     Align 11270 143 120
1003        * means:
1004        *     120 bases from pos 11270 align to pos 143 in peptide
1005        * if !mapIsFromCdna this would instead be
1006        *     Align 143 11270 40 
1007        */
1008       String[] tokens = range.split(" ");
1009       if (tokens.length != 3)
1010       {
1011         throw new IOException("Wrong number of fields for Align");
1012       }
1013       int fromStart = 0;
1014       int toStart = 0;
1015       int fromCount = 0;
1016       try
1017       {
1018         fromStart = Integer.parseInt(tokens[0]);
1019         toStart = Integer.parseInt(tokens[1]);
1020         fromCount = Integer.parseInt(tokens[2]);
1021       } catch (NumberFormatException nfe)
1022       {
1023         throw new IOException(
1024                 "Invalid number in Align field: " + nfe.getMessage());
1025       }
1026
1027       /*
1028        * Jalview always models from dna to protein, so adjust values if the
1029        * GFF mapping is from protein to dna
1030        */
1031       if (!mapIsFromCdna)
1032       {
1033         fromCount *= 3;
1034         int temp = fromStart;
1035         fromStart = toStart;
1036         toStart = temp;
1037       }
1038       fromRanges[fromRangesIndex++] = fromStart;
1039       fromRanges[fromRangesIndex++] = fromStart + strand * (fromCount - 1);
1040
1041       /*
1042        * If a codon has an intron gap, there will be contiguous 'toRanges';
1043        * this is handled for us by the MapList constructor. 
1044        * (It is not clear that exonerate ever generates this case)  
1045        */
1046       toRanges[toRangesIndex++] = toStart;
1047       toRanges[toRangesIndex++] = toStart + (fromCount - 1) / 3;
1048     }
1049
1050     return new MapList(fromRanges, toRanges, 3, 1);
1051   }
1052
1053   /**
1054    * Parse a GFF format feature. This may include creating a 'dummy' sequence to
1055    * hold the feature, or for its mapped sequence, or both, to be resolved
1056    * either later in the GFF file (##FASTA section), or when the user loads
1057    * additional sequences.
1058    * 
1059    * @param gffColumns
1060    * @param alignment
1061    * @param relaxedIdMatching
1062    * @param newseqs
1063    * @return
1064    */
1065   protected SequenceI parseGff(String[] gffColumns, AlignmentI alignment,
1066           boolean relaxedIdMatching, List<SequenceI> newseqs)
1067   {
1068     /*
1069      * GFF: seqid source type start end score strand phase [attributes]
1070      */
1071     if (gffColumns.length < 5)
1072     {
1073       System.err.println("Ignoring GFF feature line with too few columns ("
1074               + gffColumns.length + ")");
1075       return null;
1076     }
1077
1078     /*
1079      * locate referenced sequence in alignment _or_ 
1080      * as a forward or external reference (SequenceDummy)
1081      */
1082     String seqId = gffColumns[0];
1083     SequenceI seq = findSequence(seqId, alignment, newseqs,
1084             relaxedIdMatching);
1085
1086     SequenceFeature sf = null;
1087     GffHelperI helper = GffHelperFactory.getHelper(gffColumns);
1088     if (helper != null)
1089     {
1090       try
1091       {
1092         sf = helper.processGff(seq, gffColumns, alignment, newseqs,
1093                 relaxedIdMatching);
1094         if (sf != null)
1095         {
1096           seq.addSequenceFeature(sf);
1097           while ((seq = alignment.findName(seq, seqId, true)) != null)
1098           {
1099             seq.addSequenceFeature(new SequenceFeature(sf));
1100           }
1101         }
1102       } catch (IOException e)
1103       {
1104         System.err.println("GFF parsing failed with: " + e.getMessage());
1105         return null;
1106       }
1107     }
1108
1109     return seq;
1110   }
1111
1112   /**
1113    * Process the 'column 9' data of the GFF file. This is less formally defined,
1114    * and its interpretation will vary depending on the tool that has generated
1115    * it.
1116    * 
1117    * @param attributes
1118    * @param sf
1119    */
1120   protected void processGffColumnNine(String attributes, SequenceFeature sf)
1121   {
1122     sf.setAttributes(attributes);
1123
1124     /*
1125      * Parse attributes in column 9 and add them to the sequence feature's 
1126      * 'otherData' table; use Note as a best proxy for description
1127      */
1128     char nameValueSeparator = gffVersion == 3 ? '=' : ' ';
1129     // TODO check we don't break GFF2 values which include commas here
1130     Map<String, List<String>> nameValues = GffHelperBase
1131             .parseNameValuePairs(attributes, ";", nameValueSeparator, ",");
1132     for (Entry<String, List<String>> attr : nameValues.entrySet())
1133     {
1134       String values = StringUtils.listToDelimitedString(attr.getValue(),
1135               "; ");
1136       sf.setValue(attr.getKey(), values);
1137       if (NOTE.equals(attr.getKey()))
1138       {
1139         sf.setDescription(values);
1140       }
1141     }
1142   }
1143
1144   /**
1145    * After encountering ##fasta in a GFF3 file, process the remainder of the
1146    * file as FAST sequence data. Any placeholder sequences created during
1147    * feature parsing are updated with the actual sequences.
1148    * 
1149    * @param align
1150    * @param newseqs
1151    * @throws IOException
1152    */
1153   protected void processAsFasta(AlignmentI align, List<SequenceI> newseqs)
1154           throws IOException
1155   {
1156     try
1157     {
1158       mark();
1159     } catch (IOException q)
1160     {
1161     }
1162     FastaFile parser = new FastaFile(this);
1163     List<SequenceI> includedseqs = parser.getSeqs();
1164
1165     SequenceIdMatcher smatcher = new SequenceIdMatcher(newseqs);
1166
1167     /*
1168      * iterate over includedseqs, and replacing matching ones with newseqs
1169      * sequences. Generic iterator not used here because we modify
1170      * includedseqs as we go
1171      */
1172     for (int p = 0, pSize = includedseqs.size(); p < pSize; p++)
1173     {
1174       // search for any dummy seqs that this sequence can be used to update
1175       SequenceI includedSeq = includedseqs.get(p);
1176       SequenceI dummyseq = smatcher.findIdMatch(includedSeq);
1177       if (dummyseq != null && dummyseq instanceof SequenceDummy)
1178       {
1179         // probably have the pattern wrong
1180         // idea is that a flyweight proxy for a sequence ID can be created for
1181         // 1. stable reference creation
1182         // 2. addition of annotation
1183         // 3. future replacement by a real sequence
1184         // current pattern is to create SequenceDummy objects - a convenience
1185         // constructor for a Sequence.
1186         // problem is that when promoted to a real sequence, all references
1187         // need to be updated somehow. We avoid that by keeping the same object.
1188         ((SequenceDummy) dummyseq).become(includedSeq);
1189         dummyseq.createDatasetSequence();
1190
1191         /*
1192          * Update mappings so they are now to the dataset sequence
1193          */
1194         for (AlignedCodonFrame mapping : align.getCodonFrames())
1195         {
1196           mapping.updateToDataset(dummyseq);
1197         }
1198
1199         /*
1200          * replace parsed sequence with the realised forward reference
1201          */
1202         includedseqs.set(p, dummyseq);
1203
1204         /*
1205          * and remove from the newseqs list
1206          */
1207         newseqs.remove(dummyseq);
1208       }
1209     }
1210
1211     /*
1212      * finally add sequences to the dataset
1213      */
1214     for (SequenceI seq : includedseqs)
1215     {
1216       // experimental: mapping-based 'alignment' to query sequence
1217       AlignmentUtils.alignSequenceAs(seq, align,
1218               String.valueOf(align.getGapCharacter()), false, true);
1219
1220       // rename sequences if GFF handler requested this
1221       // TODO a more elegant way e.g. gffHelper.postProcess(newseqs) ?
1222       List<SequenceFeature> sfs = seq.getFeatures().getPositionalFeatures();
1223       if (!sfs.isEmpty())
1224       {
1225         String newName = (String) sfs.get(0).getValue(
1226                 GffHelperI.RENAME_TOKEN);
1227         if (newName != null)
1228         {
1229           seq.setName(newName);
1230         }
1231       }
1232       align.addSequence(seq);
1233     }
1234   }
1235
1236   /**
1237    * Process a ## directive
1238    * 
1239    * @param line
1240    * @param gffProps
1241    * @param align
1242    * @param newseqs
1243    * @throws IOException
1244    */
1245   protected void processGffPragma(String line, Map<String, String> gffProps,
1246           AlignmentI align, List<SequenceI> newseqs) throws IOException
1247   {
1248     line = line.trim();
1249     if ("###".equals(line))
1250     {
1251       // close off any open 'forward references'
1252       return;
1253     }
1254
1255     String[] tokens = line.substring(2).split(" ");
1256     String pragma = tokens[0];
1257     String value = tokens.length == 1 ? null : tokens[1];
1258
1259     if ("gff-version".equalsIgnoreCase(pragma))
1260     {
1261       if (value != null)
1262       {
1263         try
1264         {
1265           // value may be e.g. "3.1.2"
1266           gffVersion = Integer.parseInt(value.split("\\.")[0]);
1267         } catch (NumberFormatException e)
1268         {
1269           // ignore
1270         }
1271       }
1272     }
1273     else if ("sequence-region".equalsIgnoreCase(pragma))
1274     {
1275       // could capture <seqid start end> if wanted here
1276     }
1277     else if ("feature-ontology".equalsIgnoreCase(pragma))
1278     {
1279       // should resolve against the specified feature ontology URI
1280     }
1281     else if ("attribute-ontology".equalsIgnoreCase(pragma))
1282     {
1283       // URI of attribute ontology - not currently used in GFF3
1284     }
1285     else if ("source-ontology".equalsIgnoreCase(pragma))
1286     {
1287       // URI of source ontology - not currently used in GFF3
1288     }
1289     else if ("species-build".equalsIgnoreCase(pragma))
1290     {
1291       // save URI of specific NCBI taxon version of annotations
1292       gffProps.put("species-build", value);
1293     }
1294     else if ("fasta".equalsIgnoreCase(pragma))
1295     {
1296       // process the rest of the file as a fasta file and replace any dummy
1297       // sequence IDs
1298       processAsFasta(align, newseqs);
1299     }
1300     else
1301     {
1302       System.err.println("Ignoring unknown pragma: " + line);
1303     }
1304   }
1305 }