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