JAL-2476 ability to drag and drop score matrix file to desktop, simplify
[jalview.git] / src / jalview / io / ScoreMatrixFile.java
1 package jalview.io;
2
3 import jalview.analysis.scoremodels.ScoreMatrix;
4 import jalview.analysis.scoremodels.ScoreModels;
5 import jalview.datamodel.SequenceI;
6
7 import java.io.IOException;
8 import java.util.StringTokenizer;
9
10 /**
11  * A class that can parse a file containing a substitution matrix and register
12  * it for use in Jalview
13  * <p>
14  * Accepts 'NCBI' format (e.g.
15  * https://www.ncbi.nlm.nih.gov/Class/FieldGuide/BLOSUM62.txt), with the
16  * addition of a header line to provide a matrix name, e.g.
17  * 
18  * <pre>
19  * ScoreMatrix BLOSUM62
20  * </pre>
21  * 
22  * Also accepts 'AAindex' format (as described at
23  * http://www.genome.jp/aaindex/aaindex_help.html) with the minimum data
24  * required being
25  * 
26  * <pre>
27  * H accession number (used as score matrix identifier in Jalview)
28  * D description (used for tooltip in Jalview)
29  * M rows = symbolList
30  * and the substitution scores
31  * </pre>
32  */
33 public class ScoreMatrixFile extends AlignFile implements
34         AlignmentFileReaderI
35 {
36   // first non-comment line identifier - also checked in IdentifyFile
37   public static final String SCOREMATRIX = "SCOREMATRIX";
38
39   private static final String DELIMITERS = " ,\t";
40
41   private static final String COMMENT_CHAR = "#";
42
43   private String matrixName;
44
45   /*
46    * aaindex format has scores for diagonal and below only
47    */
48   boolean isLowerDiagonalOnly;
49
50   /*
51    * ncbi format has symbols as first column on score rows
52    */
53   boolean hasGuideColumn;
54
55   /**
56    * Constructor given a file reader. The file is parsed immediately and if
57    * successful, the score model registered with the ScoreModels singleton.
58    * 
59    * @param source
60    * @throws IOException
61    */
62   public ScoreMatrixFile(FileParse source) throws IOException
63   {
64     super(true, source);
65   }
66
67   /**
68    * Constructor given a file reader. The data is optionally parsed immediately.
69    * 
70    * @param parseImmediately
71    * @param source
72    * @throws IOException
73    */
74   public ScoreMatrixFile(boolean parseImmediately, FileParse source)
75           throws IOException
76   {
77     super(parseImmediately, source);
78   }
79
80   @Override
81   public String print(SequenceI[] sqs, boolean jvsuffix)
82   {
83     return null;
84   }
85
86   /**
87    * Parses the score matrix file, and if successful registers the matrix so it
88    * will be shown in Jalview menus. This method is not thread-safe (a separate
89    * instance of this class should be used by each thread).
90    */
91   @Override
92   public void parse() throws IOException
93   {
94     ScoreMatrix sm = parseMatrix();
95
96     ScoreModels.getInstance().registerScoreModel(sm);
97   }
98
99   /**
100    * Parses the score matrix file and constructs a ScoreMatrix object. If an
101    * error is found in parsing, it is thrown as FileFormatException. Any
102    * warnings are written to syserr.
103    * 
104    * @return
105    * @throws IOException
106    */
107   public ScoreMatrix parseMatrix() throws IOException
108   {
109     ScoreMatrix sm = null;
110     int lineNo = 0;
111     String name = null;
112     char[] alphabet = null;
113     float[][] scores = null;
114     int size = 0;
115     int row = 0;
116     String err = null;
117     String data;
118     isLowerDiagonalOnly = false;
119
120     while ((data = nextLine()) != null)
121     {
122       lineNo++;
123       data = data.trim();
124       if (data.startsWith(COMMENT_CHAR) || data.length() == 0)
125       {
126         continue;
127       }
128       if (data.toUpperCase().startsWith(SCOREMATRIX))
129       {
130         /*
131          * Parse name from ScoreMatrix <name>
132          * we allow any delimiter after ScoreMatrix then take the rest of the line
133          */
134         if (name != null)
135         {
136           throw new FileFormatException(
137                   "Error: 'ScoreMatrix' repeated in file at line "
138                           + lineNo);
139         }
140         StringTokenizer nameLine = new StringTokenizer(data, DELIMITERS);
141         if (nameLine.countTokens() < 2)
142         {
143           err = "Format error: expected 'ScoreMatrix <name>', found '"
144                   + data + "' at line " + lineNo;
145           throw new FileFormatException(err);
146         }
147         nameLine.nextToken(); // 'ScoreMatrix'
148         name = nameLine.nextToken(); // next field
149         name = data.substring(1).substring(data.substring(1).indexOf(name));
150         continue;
151       }
152       else if (data.startsWith("H ") && name == null)
153       {
154         /*
155          * AAindex identifier 
156          */
157         return parseAAIndexFormat(lineNo, data);
158       }
159       else if (name == null)
160       {
161         err = "Format error: 'ScoreMatrix <name>' should be the first non-comment line";
162         throw new FileFormatException(err);
163       }
164
165       /*
166        * next non-comment line after ScoreMatrix should be the 
167        * column header line with the alphabet of scored symbols
168        */
169       if (alphabet == null)
170       {
171         StringTokenizer columnHeadings = new StringTokenizer(data,
172                 DELIMITERS);
173         size = columnHeadings.countTokens();
174         alphabet = new char[size];
175         int col = 0;
176         while (columnHeadings.hasMoreTokens())
177         {
178           alphabet[col++] = columnHeadings.nextToken().charAt(0);
179         }
180         scores = new float[size][];
181         continue;
182       }
183
184       /*
185        * too much information
186        */
187       if (row >= size)
188       {
189         err = "Unexpected extra input line in score model file: '" + data
190                 + "'";
191         throw new FileFormatException(err);
192       }
193
194       parseValues(data, lineNo, scores, row, alphabet);
195       row++;
196     }
197
198     /*
199      * out of data - check we found enough
200      */
201     if (row < size)
202     {
203       err = String
204               .format("Expected %d rows of score data in score matrix but only found %d",
205                       size, row);
206       throw new FileFormatException(err);
207     }
208
209     /*
210      * If we get here, then name, alphabet and scores have been parsed successfully
211      */
212     sm = new ScoreMatrix(name, alphabet, scores);
213     matrixName = name;
214
215     return sm;
216   }
217
218   /**
219    * Parse input as AAIndex format, starting from the header line with the
220    * accession id
221    * 
222    * @param lineNo
223    * @param data
224    * @return
225    * @throws IOException
226    */
227   protected ScoreMatrix parseAAIndexFormat(int lineNo, String data)
228           throws IOException
229   {
230     String name = data.substring(2).trim();
231     String description = null;
232
233     float[][] scores = null;
234     char[] alphabet = null;
235     int row = 0;
236     int size = 0;
237
238     while ((data = nextLine()) != null)
239     {
240       lineNo++;
241       data = data.trim();
242       if (skipAAindexLine(data))
243       {
244         continue;
245       }
246       if (data.startsWith("D "))
247       {
248         description = data.substring(2).trim();
249       }
250       else if (data.startsWith("M "))
251       {
252         alphabet = parseAAindexRowsColumns(lineNo, data);
253         size = alphabet.length;
254         scores = new float[size][size];
255       }
256       else if (scores == null)
257       {
258         throw new FileFormatException(
259                 "No alphabet specified in matrix file");
260       }
261       else if (row >= size)
262       {
263         throw new FileFormatException("Too many data rows in matrix file");
264       }
265       else
266       {
267         parseValues(data, lineNo, scores, row, alphabet);
268         row++;
269       }
270     }
271
272     ScoreMatrix sm = new ScoreMatrix(name, alphabet, scores);
273     sm.setDescription(description);
274     matrixName = name;
275
276     return sm;
277   }
278
279   /**
280    * Parse one row of score values, delimited by whitespace or commas. The line
281    * may optionally include the symbol from which the scores are defined. Values
282    * may be present for all columns, or only up to the diagonal (in which case
283    * upper diagonal values are set symmetrically).
284    * 
285    * @param data
286    *          the line to be parsed
287    * @param lineNo
288    * @param scores
289    *          the score matrix to add data to
290    * @param row
291    *          the row number / alphabet index position
292    * @param alphabet
293    * @return
294    * @throws exception
295    *           if invalid, or too few, or too many values
296    */
297   protected void parseValues(String data, int lineNo, float[][] scores,
298           int row, char[] alphabet) throws FileFormatException
299   {
300     String err;
301     int size = alphabet.length;
302     StringTokenizer scoreLine = new StringTokenizer(data, DELIMITERS);
303
304     int tokenCount = scoreLine.countTokens();
305
306     /*
307      * inspect first row to see if it includes the symbol in the first column,
308      * and to see if it is lower diagonal values only (i.e. just one score)
309      */
310     if (row == 0)
311     {
312       if (data.startsWith(String.valueOf(alphabet[0])))
313       {
314         hasGuideColumn = true;
315       }
316       if (tokenCount == (hasGuideColumn ? 2 : 1))
317       {
318         isLowerDiagonalOnly = true;
319       }
320     }
321
322     if (hasGuideColumn)
323     {
324       /*
325        * check 'guide' symbol is the row'th letter of the alphabet
326        */
327       String symbol = scoreLine.nextToken();
328       if (symbol.length() > 1 || symbol.charAt(0) != alphabet[row])
329       {
330         err = String
331                 .format("Error parsing score matrix at line %d, expected '%s' but found '%s'",
332                         lineNo, alphabet[row], symbol);
333         throw new FileFormatException(err);
334       }
335       tokenCount = scoreLine.countTokens(); // excluding guide symbol
336     }
337
338     /*
339      * check the right number of values (lower diagonal or full format)
340      */
341     if (isLowerDiagonalOnly && tokenCount != row + 1)
342     {
343       err = String.format(
344               "Expected %d scores at line %d: '%s' but found %d", row + 1,
345               lineNo, data, tokenCount);
346         throw new FileFormatException(err);
347     }
348
349     if (!isLowerDiagonalOnly && tokenCount != size)
350     {
351       err = String.format(
352               "Expected %d scores at line %d: '%s' but found %d", size,
353               lineNo, data, scoreLine.countTokens());
354       throw new FileFormatException(err);
355     }
356
357     /*
358      * parse and set the values, setting the symmetrical value
359      * as well if lower diagonal format data
360      */
361     scores[row] = new float[size];
362     int col = 0;
363     String value = null;
364     while (scoreLine.hasMoreTokens())
365     {
366       try
367       {
368         value = scoreLine.nextToken();
369         scores[row][col] = Float.valueOf(value);
370         if (isLowerDiagonalOnly)
371         {
372           scores[col][row] = scores[row][col];
373         }
374         col++;
375       } catch (NumberFormatException e)
376       {
377         err = String.format(
378                 "Invalid score value '%s' at line %d column %d", value,
379                 lineNo, col);
380         throw new FileFormatException(err);
381       }
382     }
383   }
384
385   /**
386    * Parse the line in an aaindex file that looks like
387    * 
388    * <pre>
389    * M rows = ARNDCQEGHILKMFPSTWYV, cols = ARNDCQEGHILKMFPSTWYV
390    * </pre>
391    * 
392    * rejecting it if rows and cols do not match. Returns the string of
393    * characters in the row/cols alphabet.
394    * 
395    * @param lineNo
396    * @param data
397    * @return
398    * @throws FileFormatException
399    */
400   protected char[] parseAAindexRowsColumns(int lineNo, String data)
401           throws FileFormatException
402   {
403     String err = "Unexpected aaIndex score matrix data at line " + lineNo
404             + ": " + data;
405     
406     try
407     {
408       String[] toks = data.split(",");
409       String rowsAlphabet = toks[0].split("=")[1].trim();
410       String colsAlphabet = toks[1].split("=")[1].trim();
411       if (!rowsAlphabet.equals(colsAlphabet))
412       {
413         throw new FileFormatException("rows != cols");
414       }
415       return rowsAlphabet.toCharArray();
416     } catch (Throwable t)
417     {
418       throw new FileFormatException(err + " " + t.getMessage());
419     }
420   }
421
422   /**
423    * Answers true if line is one we are not interested in from AAindex format
424    * file
425    * 
426    * @param data
427    * @return
428    */
429   protected boolean skipAAindexLine(String data)
430   {
431     if (data.startsWith(COMMENT_CHAR) || data.length() == 0)
432     {
433       return true;
434     }
435     if (data.startsWith("*") || data.startsWith("R ")
436             || data.startsWith("A ") || data.startsWith("T ")
437             || data.startsWith("J ") || data.startsWith("//"))
438     {
439       return true;
440     }
441     return false;
442   }
443
444   public String getMatrixName()
445   {
446     return matrixName;
447   }
448 }