Clover icon

Coverage Report

  1. Project Clover database Thu Aug 13 2020 12:04:21 BST
  2. Package jalview.io

File FeaturesFile.java

 

Coverage histogram

../../img/srcFileCovDistChart8.png
20% of files have more coverage

Code metrics

214
431
29
1
1,537
945
164
0.38
14.86
29
5.66

Classes

Class Line # Actions
FeaturesFile 75 431 164
0.719584672%
 

Contributing tests

This file is covered by 16 tests. .

Source view

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