file format enum wip changes
[jalview.git] / src / jalview / io / PhylipFile.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.datamodel.Sequence;
24 import jalview.datamodel.SequenceI;
25
26 import java.io.IOException;
27
28 /**
29  * <p>
30  * Parser and exporter for PHYLIP file format, as defined <a
31  * href="http://evolution.genetics.washington.edu/phylip/doc/main.html">in the
32  * documentation</a>. The parser imports PHYLIP files in both sequential and
33  * interleaved format, and (currently) exports in interleaved format (using 60
34  * characters per matrix for the sequence).
35  * <p>
36  *
37  * <p>
38  * The following assumptions have been made for input
39  * <ul>
40  * <li>Sequences are expressed as letters, not real numbers with decimal points
41  * separated by blanks (which is a valid option according to the specification)</li>
42  * </ul>
43  *
44  * The following assumptions have been made for output
45  * <ul>
46  * <li>Interleaved format is used, with each matrix consisting of 60 characters;
47  * </li>
48  * <li>a blank line is added between each matrix;</li>
49  * <li>no spacing is added between the sequence characters.</li>
50  * </ul>
51  *
52  *
53  * </p>
54  *
55  * @author David Corsar
56  *
57  *
58  */
59 public class PhylipFile extends AlignFile
60 {
61
62   public static final String FILE_DESC = "PHYLIP";
63
64   /**
65    * 
66    * @see {@link AlignFile#AlignFile()}
67    */
68   public PhylipFile()
69   {
70     super();
71   }
72
73   /**
74    * 
75    * @param source
76    * @throws IOException
77    */
78   public PhylipFile(FileParse source) throws IOException
79   {
80     super(source);
81   }
82
83   /**
84    * @param inFile
85    * @param sourceType
86    * @throws IOException
87    * @see {@link AlignFile#AlignFile(FileParse)}
88    */
89   public PhylipFile(String inFile, DataSourceType sourceType)
90           throws IOException
91   {
92     super(inFile, sourceType);
93   }
94
95   /**
96    * Parses the input source
97    * 
98    * @see {@link AlignFile#parse()}
99    */
100   @Override
101   public void parse() throws IOException
102   {
103     try
104     {
105       // First line should contain number of species and number of
106       // characters, separated by blanks
107       String line = nextLine();
108       String[] lineElements = line.trim().split("\\s+");
109       if (lineElements.length < 2)
110       {
111         throw new IOException(
112                 "First line must contain the number of specifies and number of characters");
113       }
114
115       int numberSpecies = Integer.parseInt(lineElements[0]), numberCharacters = Integer
116               .parseInt(lineElements[1]);
117
118       if (numberSpecies <= 0)
119       {
120         // there are no sequences in this file so exit a nothing to
121         // parse
122         return;
123       }
124
125       SequenceI[] sequenceElements = new Sequence[numberSpecies];
126       StringBuffer[] sequences = new StringBuffer[numberSpecies];
127
128       // if file is in sequential format there is only one data matrix,
129       // else there are multiple
130
131       // read the first data matrix
132       for (int i = 0; i < numberSpecies; i++)
133       {
134         line = nextLine();
135         // lines start with the name - a maximum of 10 characters
136         // if less, then padded out or terminated with a tab
137         String potentialName = line.substring(0, 10);
138         int tabIndex = potentialName.indexOf('\t');
139         if (tabIndex == -1)
140         {
141           sequenceElements[i] = parseId(validateName(potentialName));
142           sequences[i] = new StringBuffer(
143                   removeWhitespace(line.substring(10)));
144         }
145         else
146         {
147           sequenceElements[i] = parseId(validateName(potentialName
148                   .substring(0, tabIndex)));
149           sequences[i] = new StringBuffer(
150                   removeWhitespace(line.substring(tabIndex)));
151         }
152       }
153
154       // determine if interleaved
155       if ((sequences[0]).length() != numberCharacters)
156       {
157         // interleaved file, so have to read the remainder
158         int i = 0;
159         for (line = nextLine(); line != null; line = nextLine())
160         {
161           // ignore blank lines, as defined by the specification
162           if (line.length() > 0)
163           {
164             sequences[i++].append(removeWhitespace(line));
165           }
166           // reached end of matrix, so get ready for the next one
167           if (i == sequences.length)
168           {
169             i = 0;
170           }
171         }
172       }
173
174       // file parsed completely, now store sequences
175       for (int i = 0; i < numberSpecies; i++)
176       {
177         // first check sequence is the expected length
178         if (sequences[i].length() != numberCharacters)
179         {
180           throw new IOException(sequenceElements[i].getName()
181                   + " sequence is incorrect length - should be "
182                   + numberCharacters + " but is " + sequences[i].length());
183         }
184         sequenceElements[i].setSequence(sequences[i].toString());
185         seqs.add(sequenceElements[i]);
186       }
187
188     } catch (IOException e)
189     {
190       System.err.println("Exception parsing PHYLIP file " + e);
191       e.printStackTrace(System.err);
192       throw e;
193     }
194
195   }
196
197   /**
198    * Removes any whitespace from txt, used to strip and spaces added to
199    * sequences to improve human readability
200    * 
201    * @param txt
202    * @return
203    */
204   private String removeWhitespace(String txt)
205   {
206     return txt.replaceAll("\\s*", "");
207   }
208
209   /**
210    * According to the specification, the name cannot have parentheses, square
211    * brackets, colon, semicolon, comma
212    * 
213    * @param name
214    * @return
215    * @throws IOException
216    */
217   private String validateName(String name) throws IOException
218   {
219     char[] invalidCharacters = new char[] { '(', ')', '[', ']', ':', ';',
220         ',' };
221     for (char c : invalidCharacters)
222     {
223       if (name.indexOf(c) > -1)
224       {
225         throw new IOException("Species name contains illegal character "
226                 + c);
227       }
228     }
229     return name;
230   }
231
232   /**
233    * <p>
234    * Prints the seqs in interleaved format, with each matrix consisting of 60
235    * characters; a blank line is added between each matrix; no spacing is added
236    * between the sequence characters.
237    * </p>
238    * 
239    * 
240    * @see {@link AlignFile#print()}
241    */
242   @Override
243   public String print()
244   {
245
246     StringBuffer sb = new StringBuffer(Integer.toString(seqs.size()));
247     sb.append(" ");
248     // if there are no sequences, then define the number of characters as 0
249     sb.append(
250             (seqs.size() > 0) ? Integer
251                     .toString(seqs.get(0).getSequence().length) : "0")
252             .append(newline);
253
254     // Due to how IO is handled, there doesn't appear to be a way to store
255     // if the original file was sequential or interleaved; if there is, then
256     // use that to set the value of the following variable
257     boolean sequential = false;
258
259     // maximum number of columns for each row of interleaved format
260     int numInterleavedColumns = 60;
261
262     int sequenceLength = 0;
263     for (SequenceI s : seqs)
264     {
265
266       // ensure name is only 10 characters
267       String name = s.getName();
268       if (name.length() > 10)
269       {
270         name = name.substring(0, 10);
271       }
272       else
273       {
274         // add padding 10 characters
275         name = String.format("%1$-" + 10 + "s", s.getName());
276       }
277       sb.append(name);
278
279       // sequential has the entire sequence following the name
280       if (sequential)
281       {
282         sb.append(s.getSequence());
283       }
284       else
285       {
286         // Jalview ensures all sequences are of same length so no need
287         // to keep track of min/max length
288         sequenceLength = s.getSequence().length;
289         // interleaved breaks the sequence into chunks for
290         // interleavedColumns characters
291         sb.append(s.getSequence(0,
292                 Math.min(numInterleavedColumns, sequenceLength)));
293       }
294       sb.append(newline);
295     }
296
297     // add the remaining matrixes if interleaved and there is something to
298     // add
299     if (!sequential && sequenceLength > numInterleavedColumns)
300     {
301       // determine number of remaining matrixes
302       int numMatrics = sequenceLength / numInterleavedColumns;
303       if ((sequenceLength % numInterleavedColumns) > 0)
304       {
305         numMatrics++;
306       }
307
308       // start i = 1 as first matrix has already been printed
309       for (int i = 1; i < numMatrics; i++)
310       {
311         // add blank line to separate this matrix from previous
312         sb.append(newline);
313         int start = i * numInterleavedColumns;
314         for (SequenceI s : seqs)
315         {
316           sb.append(
317                   s.getSequence(start, Math.min(start
318                           + numInterleavedColumns, sequenceLength)))
319                   .append(newline);
320         }
321       }
322
323     }
324
325     return sb.toString();
326   }
327 }