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