relaxed ID matching parameter JAL-753
[jalview.git] / src / jalview / io / FeaturesFile.java
1 /*\r
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.6)\r
3  * Copyright (C) 2010 J Procter, AM Waterhouse, G Barton, M Clamp, S Searle\r
4  * \r
5  * This file is part of Jalview.\r
6  * \r
7  * Jalview is free software: you can redistribute it and/or\r
8  * modify it under the terms of the GNU General Public License \r
9  * as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\r
10  * \r
11  * Jalview is distributed in the hope that it will be useful, but \r
12  * WITHOUT ANY WARRANTY; without even the implied warranty \r
13  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR \r
14  * PURPOSE.  See the GNU General Public License for more details.\r
15  * \r
16  * You should have received a copy of the GNU General Public License along with Jalview.  If not, see <http://www.gnu.org/licenses/>.\r
17  */\r
18 package jalview.io;\r
19 \r
20 import java.io.*;\r
21 import java.util.*;\r
22 \r
23 import jalview.analysis.SequenceIdMatcher;\r
24 import jalview.datamodel.*;\r
25 import jalview.schemes.*;\r
26 import jalview.util.Format;\r
27 \r
28 /**\r
29  * Parse and create Jalview Features files Detects GFF format features files and\r
30  * parses. Does not implement standard print() - call specific printFeatures or\r
31  * printGFF. Uses AlignmentI.findSequence(String id) to find the sequence object\r
32  * for the features annotation - this normally works on an exact match.\r
33  * \r
34  * @author AMW\r
35  * @version $Revision$\r
36  */\r
37 public class FeaturesFile extends AlignFile\r
38 {\r
39   /**\r
40    * work around for GFF interpretation bug where source string becomes\r
41    * description rather than a group\r
42    */\r
43   private boolean doGffSource = true;\r
44 \r
45   /**\r
46    * Creates a new FeaturesFile object.\r
47    */\r
48   public FeaturesFile()\r
49   {\r
50   }\r
51 \r
52   /**\r
53    * Creates a new FeaturesFile object.\r
54    * \r
55    * @param inFile\r
56    *          DOCUMENT ME!\r
57    * @param type\r
58    *          DOCUMENT ME!\r
59    * \r
60    * @throws IOException\r
61    *           DOCUMENT ME!\r
62    */\r
63   public FeaturesFile(String inFile, String type) throws IOException\r
64   {\r
65     super(inFile, type);\r
66   }\r
67 \r
68   public FeaturesFile(FileParse source) throws IOException\r
69   {\r
70     super(source);\r
71   }\r
72 \r
73   /**\r
74    * Parse GFF or sequence features file using case-independent matching, discarding URLs\r
75    * @param align - alignment/dataset containing sequences that are to be annotated\r
76    * @param colours - hashtable to store feature colour definitions\r
77    * @param removeHTML - process html strings into plain text\r
78    * @return true if features were added\r
79    */\r
80   public boolean parse(AlignmentI align, Hashtable colours,\r
81           boolean removeHTML)\r
82   {\r
83     return parse(align, colours, null, removeHTML, false);\r
84   }\r
85 \r
86   /**\r
87    * Parse GFF or sequence features file optionally using case-independent matching, discarding URLs\r
88    * @param align - alignment/dataset containing sequences that are to be annotated\r
89    * @param colours - hashtable to store feature colour definitions\r
90    * @param removeHTML - process html strings into plain text\r
91    * @param relaxedIdmatching - when true, ID matches to compound sequence IDs are allowed\r
92    * @return true if features were added\r
93    */\r
94   public boolean parse(AlignmentI align, \r
95           Hashtable colours, boolean removeHTML, boolean relaxedIdMatching)\r
96   {\r
97     return parse(align, colours, null, removeHTML, relaxedIdMatching);\r
98   }\r
99 \r
100   /**\r
101    * Parse GFF or sequence features file optionally using case-independent matching\r
102    * @param align - alignment/dataset containing sequences that are to be annotated\r
103    * @param colours - hashtable to store feature colour definitions\r
104    * @param featureLink - hashtable to store associated URLs \r
105    * @param removeHTML - process html strings into plain text\r
106    * @return true if features were added\r
107    */\r
108   public boolean parse(AlignmentI align, Hashtable colours,\r
109           Hashtable featureLink, boolean removeHTML)\r
110   {\r
111     return parse(align, colours, featureLink, removeHTML, false);\r
112   }\r
113 \r
114   /**\r
115   /**\r
116    * Parse GFF or sequence features file \r
117    * @param align - alignment/dataset containing sequences that are to be annotated\r
118    * @param colours - hashtable to store feature colour definitions\r
119    * @param featureLink - hashtable to store associated URLs \r
120    * @param removeHTML - process html strings into plain text\r
121    * @param relaxedIdmatching - when true, ID matches to compound sequence IDs are allowed\r
122    * @return true if features were added\r
123    */\r
124   public boolean parse(AlignmentI align,\r
125           Hashtable colours, Hashtable featureLink, boolean removeHTML, boolean relaxedIdmatching)\r
126   {\r
127 \r
128     String line = null;\r
129     try\r
130     {\r
131       SequenceI seq = null;\r
132       String type, desc, token = null;\r
133 \r
134       int index, start, end;\r
135       float score;\r
136       StringTokenizer st;\r
137       SequenceFeature sf;\r
138       String featureGroup = null, groupLink = null;\r
139       Hashtable typeLink = new Hashtable();\r
140       /**\r
141        * when true, assume GFF style features rather than Jalview style.\r
142        */\r
143       boolean GFFFile = true;\r
144       while ((line = nextLine()) != null)\r
145       {\r
146         if (line.startsWith("#"))\r
147         {\r
148           continue;\r
149         }\r
150 \r
151         st = new StringTokenizer(line, "\t");\r
152         if (st.countTokens() == 1)\r
153         {\r
154           if (line.trim().equalsIgnoreCase("GFF"))\r
155           {\r
156             // Start parsing file as if it might be GFF again.\r
157             GFFFile = true;\r
158             continue;\r
159           }\r
160         }\r
161         if (st.countTokens() > 1 && st.countTokens() < 4)\r
162         {\r
163           GFFFile = false;\r
164           type = st.nextToken();\r
165           if (type.equalsIgnoreCase("startgroup"))\r
166           {\r
167             featureGroup = st.nextToken();\r
168             if (st.hasMoreElements())\r
169             {\r
170               groupLink = st.nextToken();\r
171               featureLink.put(featureGroup, groupLink);\r
172             }\r
173           }\r
174           else if (type.equalsIgnoreCase("endgroup"))\r
175           {\r
176             // We should check whether this is the current group,\r
177             // but at present theres no way of showing more than 1 group\r
178             st.nextToken();\r
179             featureGroup = null;\r
180             groupLink = null;\r
181           }\r
182           else\r
183           {\r
184             Object colour = null;\r
185             String colscheme = st.nextToken();\r
186             if (colscheme.indexOf("|") > -1\r
187                     || colscheme.trim().equalsIgnoreCase("label"))\r
188             {\r
189               // Parse '|' separated graduated colourscheme fields:\r
190               // [label|][mincolour|maxcolour|[absolute|]minvalue|maxvalue|thresholdtype|thresholdvalue]\r
191               // can either provide 'label' only, first is optional, next two\r
192               // colors are required (but may be\r
193               // left blank), next is optional, nxt two min/max are required.\r
194               // first is either 'label'\r
195               // first/second and third are both hexadecimal or word equivalent\r
196               // colour.\r
197               // next two are values parsed as floats.\r
198               // fifth is either 'above','below', or 'none'.\r
199               // sixth is a float value and only required when fifth is either\r
200               // 'above' or 'below'.\r
201               StringTokenizer gcol = new StringTokenizer(colscheme, "|",\r
202                       true);\r
203               // set defaults\r
204               int threshtype = AnnotationColourGradient.NO_THRESHOLD;\r
205               float min = Float.MIN_VALUE, max = Float.MAX_VALUE, threshval = Float.NaN;\r
206               boolean labelCol = false;\r
207               // Parse spec line\r
208               String mincol = gcol.nextToken();\r
209               if (mincol == "|")\r
210               {\r
211                 System.err\r
212                         .println("Expected either 'label' or a colour specification in the line: "\r
213                                 + line);\r
214                 continue;\r
215               }\r
216               String maxcol = null;\r
217               if (mincol.toLowerCase().indexOf("label") == 0)\r
218               {\r
219                 labelCol = true;\r
220                 mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null); // skip\r
221                                                                            // '|'\r
222                 mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);\r
223               }\r
224               String abso = null, minval, maxval;\r
225               if (mincol != null)\r
226               {\r
227                 // at least four more tokens\r
228                 if (mincol.equals("|"))\r
229                 {\r
230                   mincol = "";\r
231                 }\r
232                 else\r
233                 {\r
234                   gcol.nextToken(); // skip next '|'\r
235                 }\r
236                 // continue parsing rest of line\r
237                 maxcol = gcol.nextToken();\r
238                 if (maxcol.equals("|"))\r
239                 {\r
240                   maxcol = "";\r
241                 }\r
242                 else\r
243                 {\r
244                   gcol.nextToken(); // skip next '|'\r
245                 }\r
246                 abso = gcol.nextToken();\r
247                 gcol.nextToken(); // skip next '|'\r
248                 if (abso.toLowerCase().indexOf("abso") != 0)\r
249                 {\r
250                   minval = abso;\r
251                   abso = null;\r
252                 }\r
253                 else\r
254                 {\r
255                   minval = gcol.nextToken();\r
256                   gcol.nextToken(); // skip next '|'\r
257                 }\r
258                 maxval = gcol.nextToken();\r
259                 if (gcol.hasMoreTokens())\r
260                 {\r
261                   gcol.nextToken(); // skip next '|'\r
262                 }\r
263                 try\r
264                 {\r
265                   if (minval.length() > 0)\r
266                   {\r
267                     min = new Float(minval).floatValue();\r
268                   }\r
269                 } catch (Exception e)\r
270                 {\r
271                   System.err\r
272                           .println("Couldn't parse the minimum value for graduated colour for type ("\r
273                                   + colscheme\r
274                                   + ") - did you misspell 'auto' for the optional automatic colour switch ?");\r
275                   e.printStackTrace();\r
276                 }\r
277                 try\r
278                 {\r
279                   if (maxval.length() > 0)\r
280                   {\r
281                     max = new Float(maxval).floatValue();\r
282                   }\r
283                 } catch (Exception e)\r
284                 {\r
285                   System.err\r
286                           .println("Couldn't parse the maximum value for graduated colour for type ("\r
287                                   + colscheme + ")");\r
288                   e.printStackTrace();\r
289                 }\r
290               }\r
291               else\r
292               {\r
293                 // add in some dummy min/max colours for the label-only\r
294                 // colourscheme.\r
295                 mincol = "FFFFFF";\r
296                 maxcol = "000000";\r
297               }\r
298               try\r
299               {\r
300                 colour = new jalview.schemes.GraduatedColor(\r
301                         new UserColourScheme(mincol).findColour('A'),\r
302                         new UserColourScheme(maxcol).findColour('A'), min,\r
303                         max);\r
304               } catch (Exception e)\r
305               {\r
306                 System.err\r
307                         .println("Couldn't parse the graduated colour scheme ("\r
308                                 + colscheme + ")");\r
309                 e.printStackTrace();\r
310               }\r
311               if (colour != null)\r
312               {\r
313                 ((jalview.schemes.GraduatedColor) colour)\r
314                         .setColourByLabel(labelCol);\r
315                 ((jalview.schemes.GraduatedColor) colour)\r
316                         .setAutoScaled(abso == null);\r
317                 // add in any additional parameters\r
318                 String ttype = null, tval = null;\r
319                 if (gcol.hasMoreTokens())\r
320                 {\r
321                   // threshold type and possibly a threshold value\r
322                   ttype = gcol.nextToken();\r
323                   if (ttype.toLowerCase().startsWith("below"))\r
324                   {\r
325                     ((jalview.schemes.GraduatedColor) colour)\r
326                             .setThreshType(AnnotationColourGradient.BELOW_THRESHOLD);\r
327                   }\r
328                   else if (ttype.toLowerCase().startsWith("above"))\r
329                   {\r
330                     ((jalview.schemes.GraduatedColor) colour)\r
331                             .setThreshType(AnnotationColourGradient.ABOVE_THRESHOLD);\r
332                   }\r
333                   else\r
334                   {\r
335                     ((jalview.schemes.GraduatedColor) colour)\r
336                             .setThreshType(AnnotationColourGradient.NO_THRESHOLD);\r
337                     if (!ttype.toLowerCase().startsWith("no"))\r
338                     {\r
339                       System.err\r
340                               .println("Ignoring unrecognised threshold type : "\r
341                                       + ttype);\r
342                     }\r
343                   }\r
344                 }\r
345                 if (((GraduatedColor) colour).getThreshType() != AnnotationColourGradient.NO_THRESHOLD)\r
346                 {\r
347                   try\r
348                   {\r
349                     gcol.nextToken();\r
350                     tval = gcol.nextToken();\r
351                     ((jalview.schemes.GraduatedColor) colour)\r
352                             .setThresh(new Float(tval).floatValue());\r
353                   } catch (Exception e)\r
354                   {\r
355                     System.err\r
356                             .println("Couldn't parse threshold value as a float: ("\r
357                                     + tval + ")");\r
358                     e.printStackTrace();\r
359                   }\r
360                 }\r
361                 // parse the thresh-is-min token ?\r
362                 if (gcol.hasMoreTokens())\r
363                 {\r
364                   System.err\r
365                           .println("Ignoring additional tokens in parameters in graduated colour specification\n");\r
366                   while (gcol.hasMoreTokens())\r
367                   {\r
368                     System.err.println("|" + gcol.nextToken());\r
369                   }\r
370                   System.err.println("\n");\r
371                 }\r
372               }\r
373             }\r
374             else\r
375             {\r
376               UserColourScheme ucs = new UserColourScheme(colscheme);\r
377               colour = ucs.findColour('A');\r
378             }\r
379             if (colour != null)\r
380             {\r
381               colours.put(type, colour);\r
382             }\r
383             if (st.hasMoreElements())\r
384             {\r
385               String link = st.nextToken();\r
386               typeLink.put(type, link);\r
387               if (featureLink == null)\r
388               {\r
389                 featureLink = new Hashtable();\r
390               }\r
391               featureLink.put(type, link);\r
392             }\r
393           }\r
394           continue;\r
395         }\r
396         String seqId = "";\r
397         while (st.hasMoreElements())\r
398         {\r
399 \r
400           if (GFFFile)\r
401           {\r
402             // Still possible this is an old Jalview file,\r
403             // which does not have type colours at the beginning\r
404             seqId = token = st.nextToken();\r
405             seq = findName(align, seqId, relaxedIdmatching);\r
406             if (seq != null)\r
407             {\r
408               desc = st.nextToken();\r
409               String group = null;\r
410               if (doGffSource && desc.indexOf(' ') == -1)\r
411               {\r
412                 // could also be a source term rather than description line\r
413                 group = new String(desc);\r
414               }\r
415               type = st.nextToken();\r
416               try\r
417               {\r
418                 String stt = st.nextToken();\r
419                 if (stt.length() == 0 || stt.equals("-"))\r
420                 {\r
421                   start = 0;\r
422                 }\r
423                 else\r
424                 {\r
425                   start = Integer.parseInt(stt);\r
426                 }\r
427               } catch (NumberFormatException ex)\r
428               {\r
429                 start = 0;\r
430               }\r
431               try\r
432               {\r
433                 String stt = st.nextToken();\r
434                 if (stt.length() == 0 || stt.equals("-"))\r
435                 {\r
436                   end = 0;\r
437                 }\r
438                 else\r
439                 {\r
440                   end = Integer.parseInt(stt);\r
441                 }\r
442               } catch (NumberFormatException ex)\r
443               {\r
444                 end = 0;\r
445               }\r
446               // TODO: decide if non positional feature assertion for input data\r
447               // where end==0 is generally valid\r
448               if (end == 0)\r
449               {\r
450                 // treat as non-positional feature, regardless.\r
451                 start = 0;\r
452               }\r
453               try\r
454               {\r
455                 score = new Float(st.nextToken()).floatValue();\r
456               } catch (NumberFormatException ex)\r
457               {\r
458                 score = 0;\r
459               }\r
460 \r
461               sf = new SequenceFeature(type, desc, start, end, score, group);\r
462 \r
463               try\r
464               {\r
465                 sf.setValue("STRAND", st.nextToken());\r
466                 sf.setValue("FRAME", st.nextToken());\r
467               } catch (Exception ex)\r
468               {\r
469               }\r
470 \r
471               if (st.hasMoreTokens())\r
472               {\r
473                 StringBuffer attributes = new StringBuffer();\r
474                 while (st.hasMoreTokens())\r
475                 {\r
476                   attributes.append("\t" + st.nextElement());\r
477                 }\r
478                 // TODO validate and split GFF2 attributes field ? parse out\r
479                 // ([A-Za-z][A-Za-z0-9_]*) <value> ; and add as\r
480                 // sf.setValue(attrib, val);\r
481                 sf.setValue("ATTRIBUTES", attributes.toString());\r
482               }\r
483 \r
484               seq.addSequenceFeature(sf);\r
485               while ((seq = align.findName(seq, seqId, true)) != null)\r
486               {\r
487                 seq.addSequenceFeature(new SequenceFeature(sf));\r
488               }\r
489               break;\r
490             }\r
491           }\r
492 \r
493           if (GFFFile && seq == null)\r
494           {\r
495             desc = token;\r
496           }\r
497           else\r
498           {\r
499             desc = st.nextToken();\r
500           }\r
501           if (!st.hasMoreTokens())\r
502           {\r
503             System.err\r
504                     .println("DEBUG: Run out of tokens when trying to identify the destination for the feature.. giving up.");\r
505             // in all probability, this isn't a file we understand, so bail\r
506             // quietly.\r
507             return false;\r
508           }\r
509 \r
510           token = st.nextToken();\r
511 \r
512           if (!token.equals("ID_NOT_SPECIFIED"))\r
513           {\r
514             seq = findName(align, seqId = token, relaxedIdmatching);\r
515             st.nextToken();\r
516           }\r
517           else\r
518           {\r
519             seqId = null;\r
520             try\r
521             {\r
522               index = Integer.parseInt(st.nextToken());\r
523               seq = align.getSequenceAt(index);\r
524             } catch (NumberFormatException ex)\r
525             {\r
526               seq = null;\r
527             }\r
528           }\r
529 \r
530           if (seq == null)\r
531           {\r
532             System.out.println("Sequence not found: " + line);\r
533             break;\r
534           }\r
535 \r
536           start = Integer.parseInt(st.nextToken());\r
537           end = Integer.parseInt(st.nextToken());\r
538 \r
539           type = st.nextToken();\r
540 \r
541           if (!colours.containsKey(type))\r
542           {\r
543             // Probably the old style groups file\r
544             UserColourScheme ucs = new UserColourScheme(type);\r
545             colours.put(type, ucs.findColour('A'));\r
546           }\r
547           sf = new SequenceFeature(type, desc, "", start, end, featureGroup);\r
548           if (st.hasMoreTokens())\r
549           {\r
550             try\r
551             {\r
552               score = new Float(st.nextToken()).floatValue();\r
553               // update colourgradient bounds if allowed to\r
554             } catch (NumberFormatException ex)\r
555             {\r
556               score = 0;\r
557             }\r
558             sf.setScore(score);\r
559           }\r
560           if (groupLink != null && removeHTML)\r
561           {\r
562             sf.addLink(groupLink);\r
563             sf.description += "%LINK%";\r
564           }\r
565           if (typeLink.containsKey(type) && removeHTML)\r
566           {\r
567             sf.addLink(typeLink.get(type).toString());\r
568             sf.description += "%LINK%";\r
569           }\r
570 \r
571           parseDescriptionHTML(sf, removeHTML);\r
572 \r
573           seq.addSequenceFeature(sf);\r
574 \r
575           while (seqId != null\r
576                   && (seq = align.findName(seq, seqId, false)) != null)\r
577           {\r
578             seq.addSequenceFeature(new SequenceFeature(sf));\r
579           }\r
580           // If we got here, its not a GFFFile\r
581           GFFFile = false;\r
582         }\r
583       }\r
584       resetMatcher();\r
585     } catch (Exception ex)\r
586     {\r
587       System.out.println(line);\r
588       System.out.println("Error parsing feature file: " + ex + "\n" + line);\r
589       ex.printStackTrace(System.err);\r
590       resetMatcher();\r
591       return false;\r
592     }\r
593 \r
594     return true;\r
595   }\r
596 \r
597   private AlignmentI lastmatchedAl = null;\r
598 \r
599   private SequenceIdMatcher matcher = null;\r
600 \r
601   /**\r
602    * clear any temporary handles used to speed up ID matching\r
603    */\r
604   private void resetMatcher()\r
605   {\r
606     lastmatchedAl = null;\r
607     matcher = null;\r
608   }\r
609 \r
610   private SequenceI findName(AlignmentI align, String seqId,\r
611           boolean relaxedIdMatching)\r
612   {\r
613     SequenceI match = null;\r
614     if (relaxedIdMatching)\r
615     {\r
616       if (lastmatchedAl != align)\r
617       {\r
618         matcher = new SequenceIdMatcher(\r
619                 (lastmatchedAl = align).getSequencesArray());\r
620       }\r
621       match = matcher.findIdMatch(seqId);\r
622     }\r
623     else\r
624     {\r
625       match = align.findName(seqId, true);\r
626     }\r
627     return match;\r
628   }\r
629 \r
630   public void parseDescriptionHTML(SequenceFeature sf, boolean removeHTML)\r
631   {\r
632     if (sf.getDescription() == null)\r
633     {\r
634       return;\r
635     }\r
636 \r
637     if (removeHTML\r
638             && sf.getDescription().toUpperCase().indexOf("<HTML>") == -1)\r
639     {\r
640       removeHTML = false;\r
641     }\r
642 \r
643     StringBuffer sb = new StringBuffer();\r
644     StringTokenizer st = new StringTokenizer(sf.getDescription(), "<");\r
645     String token, link;\r
646     int startTag;\r
647     String tag = null;\r
648     while (st.hasMoreElements())\r
649     {\r
650       token = st.nextToken("&>");\r
651       if (token.equalsIgnoreCase("html") || token.startsWith("/"))\r
652       {\r
653         continue;\r
654       }\r
655 \r
656       tag = null;\r
657       startTag = token.indexOf("<");\r
658 \r
659       if (startTag > -1)\r
660       {\r
661         tag = token.substring(startTag + 1);\r
662         token = token.substring(0, startTag);\r
663       }\r
664 \r
665       if (tag != null && tag.toUpperCase().startsWith("A HREF="))\r
666       {\r
667         if (token.length() > 0)\r
668         {\r
669           sb.append(token);\r
670         }\r
671         link = tag.substring(tag.indexOf("\"") + 1, tag.length() - 1);\r
672         String label = st.nextToken("<>");\r
673         sf.addLink(label + "|" + link);\r
674         sb.append(label + "%LINK%");\r
675       }\r
676       else if (tag != null && tag.equalsIgnoreCase("br"))\r
677       {\r
678         sb.append(newline);\r
679       }\r
680       else if (token.startsWith("lt;"))\r
681       {\r
682         sb.append("<" + token.substring(3));\r
683       }\r
684       else if (token.startsWith("gt;"))\r
685       {\r
686         sb.append(">" + token.substring(3));\r
687       }\r
688       else if (token.startsWith("amp;"))\r
689       {\r
690         sb.append("&" + token.substring(4));\r
691       }\r
692       else\r
693       {\r
694         sb.append(token);\r
695       }\r
696     }\r
697 \r
698     if (removeHTML)\r
699     {\r
700       sf.description = sb.toString();\r
701     }\r
702 \r
703   }\r
704 \r
705   /**\r
706    * generate a features file for seqs includes non-pos features by default.\r
707    * \r
708    * @param seqs\r
709    *          source of sequence features\r
710    * @param visible\r
711    *          hash of feature types and colours\r
712    * @return features file contents\r
713    */\r
714   public String printJalviewFormat(SequenceI[] seqs, Hashtable visible)\r
715   {\r
716     return printJalviewFormat(seqs, visible, true, true);\r
717   }\r
718 \r
719   /**\r
720    * generate a features file for seqs with colours from visible (if any)\r
721    * \r
722    * @param seqs\r
723    *          source of features\r
724    * @param visible\r
725    *          hash of Colours for each feature type\r
726    * @param visOnly\r
727    *          when true only feature types in 'visible' will be output\r
728    * @param nonpos\r
729    *          indicates if non-positional features should be output (regardless\r
730    *          of group or type)\r
731    * @return features file contents\r
732    */\r
733   public String printJalviewFormat(SequenceI[] seqs, Hashtable visible,\r
734           boolean visOnly, boolean nonpos)\r
735   {\r
736     StringBuffer out = new StringBuffer();\r
737     SequenceFeature[] next;\r
738     boolean featuresGen = false;\r
739     if (visOnly && !nonpos && (visible == null || visible.size() < 1))\r
740     {\r
741       // no point continuing.\r
742       return "No Features Visible";\r
743     }\r
744 \r
745     if (visible != null && visOnly)\r
746     {\r
747       // write feature colours only if we're given them and we are generating\r
748       // viewed features\r
749       // TODO: decide if feature links should also be written here ?\r
750       Enumeration en = visible.keys();\r
751       String type, color;\r
752       while (en.hasMoreElements())\r
753       {\r
754         type = en.nextElement().toString();\r
755 \r
756         if (visible.get(type) instanceof GraduatedColor)\r
757         {\r
758           GraduatedColor gc = (GraduatedColor) visible.get(type);\r
759           color = (gc.isColourByLabel() ? "label|" : "")\r
760                   + Format.getHexString(gc.getMinColor()) + "|"\r
761                   + Format.getHexString(gc.getMaxColor())\r
762                   + (gc.isAutoScale() ? "|" : "|abso|") + gc.getMin() + "|"\r
763                   + gc.getMax() + "|";\r
764           if (gc.getThreshType() != AnnotationColourGradient.NO_THRESHOLD)\r
765           {\r
766             if (gc.getThreshType() == AnnotationColourGradient.BELOW_THRESHOLD)\r
767             {\r
768               color += "below";\r
769             }\r
770             else\r
771             {\r
772               if (gc.getThreshType() != AnnotationColourGradient.ABOVE_THRESHOLD)\r
773               {\r
774                 System.err.println("WARNING: Unsupported threshold type ("\r
775                         + gc.getThreshType() + ") : Assuming 'above'");\r
776               }\r
777               color += "above";\r
778             }\r
779             // add the value\r
780             color += "|" + gc.getThresh();\r
781           }\r
782           else\r
783           {\r
784             color += "none";\r
785           }\r
786         }\r
787         else if (visible.get(type) instanceof java.awt.Color)\r
788         {\r
789           color = Format.getHexString((java.awt.Color) visible.get(type));\r
790         }\r
791         else\r
792         {\r
793           // legacy support for integer objects containing colour triplet values\r
794           color = Format.getHexString(new java.awt.Color(Integer\r
795                   .parseInt(visible.get(type).toString())));\r
796         }\r
797         out.append(type);\r
798         out.append("\t");\r
799         out.append(color);\r
800         out.append(newline);\r
801       }\r
802     }\r
803     // Work out which groups are both present and visible\r
804     Vector groups = new Vector();\r
805     int groupIndex = 0;\r
806     boolean isnonpos = false;\r
807 \r
808     for (int i = 0; i < seqs.length; i++)\r
809     {\r
810       next = seqs[i].getSequenceFeatures();\r
811       if (next != null)\r
812       {\r
813         for (int j = 0; j < next.length; j++)\r
814         {\r
815           isnonpos = next[j].begin == 0 && next[j].end == 0;\r
816           if ((!nonpos && isnonpos)\r
817                   || (!isnonpos && visOnly && !visible\r
818                           .containsKey(next[j].type)))\r
819           {\r
820             continue;\r
821           }\r
822 \r
823           if (next[j].featureGroup != null\r
824                   && !groups.contains(next[j].featureGroup))\r
825           {\r
826             groups.addElement(next[j].featureGroup);\r
827           }\r
828         }\r
829       }\r
830     }\r
831 \r
832     String group = null;\r
833     do\r
834     {\r
835 \r
836       if (groups.size() > 0 && groupIndex < groups.size())\r
837       {\r
838         group = groups.elementAt(groupIndex).toString();\r
839         out.append(newline);\r
840         out.append("STARTGROUP\t");\r
841         out.append(group);\r
842         out.append(newline);\r
843       }\r
844       else\r
845       {\r
846         group = null;\r
847       }\r
848 \r
849       for (int i = 0; i < seqs.length; i++)\r
850       {\r
851         next = seqs[i].getSequenceFeatures();\r
852         if (next != null)\r
853         {\r
854           for (int j = 0; j < next.length; j++)\r
855           {\r
856             isnonpos = next[j].begin == 0 && next[j].end == 0;\r
857             if ((!nonpos && isnonpos)\r
858                     || (!isnonpos && visOnly && !visible\r
859                             .containsKey(next[j].type)))\r
860             {\r
861               // skip if feature is nonpos and we ignore them or if we only\r
862               // output visible and it isn't non-pos and it's not visible\r
863               continue;\r
864             }\r
865 \r
866             if (group != null\r
867                     && (next[j].featureGroup == null || !next[j].featureGroup\r
868                             .equals(group)))\r
869             {\r
870               continue;\r
871             }\r
872 \r
873             if (group == null && next[j].featureGroup != null)\r
874             {\r
875               continue;\r
876             }\r
877             // we have features to output\r
878             featuresGen = true;\r
879             if (next[j].description == null\r
880                     || next[j].description.equals(""))\r
881             {\r
882               out.append(next[j].type + "\t");\r
883             }\r
884             else\r
885             {\r
886               if (next[j].links != null\r
887                       && next[j].getDescription().indexOf("<html>") == -1)\r
888               {\r
889                 out.append("<html>");\r
890               }\r
891 \r
892               out.append(next[j].description + " ");\r
893               if (next[j].links != null)\r
894               {\r
895                 for (int l = 0; l < next[j].links.size(); l++)\r
896                 {\r
897                   String label = next[j].links.elementAt(l).toString();\r
898                   String href = label.substring(label.indexOf("|") + 1);\r
899                   label = label.substring(0, label.indexOf("|"));\r
900 \r
901                   if (next[j].description.indexOf(href) == -1)\r
902                   {\r
903                     out.append("<a href=\"" + href + "\">" + label + "</a>");\r
904                   }\r
905                 }\r
906 \r
907                 if (next[j].getDescription().indexOf("</html>") == -1)\r
908                 {\r
909                   out.append("</html>");\r
910                 }\r
911               }\r
912 \r
913               out.append("\t");\r
914             }\r
915             out.append(seqs[i].getName());\r
916             out.append("\t-1\t");\r
917             out.append(next[j].begin);\r
918             out.append("\t");\r
919             out.append(next[j].end);\r
920             out.append("\t");\r
921             out.append(next[j].type);\r
922             if (next[j].score != Float.NaN)\r
923             {\r
924               out.append("\t");\r
925               out.append(next[j].score);\r
926             }\r
927             out.append(newline);\r
928           }\r
929         }\r
930       }\r
931 \r
932       if (group != null)\r
933       {\r
934         out.append("ENDGROUP\t");\r
935         out.append(group);\r
936         out.append(newline);\r
937         groupIndex++;\r
938       }\r
939       else\r
940       {\r
941         break;\r
942       }\r
943 \r
944     } while (groupIndex < groups.size() + 1);\r
945 \r
946     if (!featuresGen)\r
947     {\r
948       return "No Features Visible";\r
949     }\r
950 \r
951     return out.toString();\r
952   }\r
953 \r
954   /**\r
955    * generate a gff file for sequence features includes non-pos features by\r
956    * default.\r
957    * \r
958    * @param seqs\r
959    * @param visible\r
960    * @return\r
961    */\r
962   public String printGFFFormat(SequenceI[] seqs, Hashtable visible)\r
963   {\r
964     return printGFFFormat(seqs, visible, true, true);\r
965   }\r
966 \r
967   public String printGFFFormat(SequenceI[] seqs, Hashtable visible,\r
968           boolean visOnly, boolean nonpos)\r
969   {\r
970     StringBuffer out = new StringBuffer();\r
971     SequenceFeature[] next;\r
972     String source;\r
973     boolean isnonpos;\r
974     for (int i = 0; i < seqs.length; i++)\r
975     {\r
976       if (seqs[i].getSequenceFeatures() != null)\r
977       {\r
978         next = seqs[i].getSequenceFeatures();\r
979         for (int j = 0; j < next.length; j++)\r
980         {\r
981           isnonpos = next[j].begin == 0 && next[j].end == 0;\r
982           if ((!nonpos && isnonpos)\r
983                   || (!isnonpos && visOnly && !visible\r
984                           .containsKey(next[j].type)))\r
985           {\r
986             continue;\r
987           }\r
988 \r
989           source = next[j].featureGroup;\r
990           if (source == null)\r
991           {\r
992             source = next[j].getDescription();\r
993           }\r
994 \r
995           out.append(seqs[i].getName());\r
996           out.append("\t");\r
997           out.append(source);\r
998           out.append("\t");\r
999           out.append(next[j].type);\r
1000           out.append("\t");\r
1001           out.append(next[j].begin);\r
1002           out.append("\t");\r
1003           out.append(next[j].end);\r
1004           out.append("\t");\r
1005           out.append(next[j].score);\r
1006           out.append("\t");\r
1007 \r
1008           if (next[j].getValue("STRAND") != null)\r
1009           {\r
1010             out.append(next[j].getValue("STRAND"));\r
1011             out.append("\t");\r
1012           }\r
1013           else\r
1014           {\r
1015             out.append(".\t");\r
1016           }\r
1017 \r
1018           if (next[j].getValue("FRAME") != null)\r
1019           {\r
1020             out.append(next[j].getValue("FRAME"));\r
1021           }\r
1022           else\r
1023           {\r
1024             out.append(".");\r
1025           }\r
1026           // TODO: verify/check GFF - should there be a /t here before attribute\r
1027           // output ?\r
1028 \r
1029           if (next[j].getValue("ATTRIBUTES") != null)\r
1030           {\r
1031             out.append(next[j].getValue("ATTRIBUTES"));\r
1032           }\r
1033 \r
1034           out.append(newline);\r
1035 \r
1036         }\r
1037       }\r
1038     }\r
1039 \r
1040     return out.toString();\r
1041   }\r
1042 \r
1043   /**\r
1044    * this is only for the benefit of object polymorphism - method does nothing.\r
1045    */\r
1046   public void parse()\r
1047   {\r
1048     // IGNORED\r
1049   }\r
1050 \r
1051   /**\r
1052    * this is only for the benefit of object polymorphism - method does nothing.\r
1053    * \r
1054    * @return error message\r
1055    */\r
1056   public String print()\r
1057   {\r
1058     return "USE printGFFFormat() or printJalviewFormat()";\r
1059   }\r
1060 \r
1061 }\r