JAL-2755 put chromosomal dbref first in report
[jalview.git] / src / jalview / io / SequenceAnnotationReport.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.io;
22
23 import jalview.datamodel.DBRefEntry;
24 import jalview.datamodel.DBRefSource;
25 import jalview.datamodel.SequenceFeature;
26 import jalview.datamodel.SequenceI;
27 import jalview.io.gff.GffConstants;
28 import jalview.util.MessageManager;
29 import jalview.util.UrlLink;
30
31 import java.util.Arrays;
32 import java.util.Collection;
33 import java.util.Comparator;
34 import java.util.LinkedHashMap;
35 import java.util.List;
36 import java.util.Map;
37
38 /**
39  * generate HTML reports for a sequence
40  * 
41  * @author jimp
42  */
43 public class SequenceAnnotationReport
44 {
45   private static final String COMMA = ",";
46
47   private static final String ELLIPSIS = "...";
48
49   private static final int MAX_REFS_PER_SOURCE = 4;
50
51   private static final int MAX_SOURCES = 40;
52
53   private static final String[][] PRIMARY_SOURCES = new String[][] {
54       DBRefSource.CODINGDBS, DBRefSource.DNACODINGDBS,
55       DBRefSource.PROTEINDBS };
56
57   final String linkImageURL;
58
59   /*
60    * Comparator to order DBRefEntry by Source + accession id (case-insensitive),
61    * with 'Primary' sources placed before others, and 'chromosome' first of all
62    */
63   private static Comparator<DBRefEntry> comparator = new Comparator<DBRefEntry>()
64   {
65
66     @Override
67     public int compare(DBRefEntry ref1, DBRefEntry ref2)
68     {
69       if (ref1.isChromosome())
70       {
71         return -1;
72       }
73       if (ref2.isChromosome())
74       {
75         return 1;
76       }
77       String s1 = ref1.getSource();
78       String s2 = ref2.getSource();
79       boolean s1Primary = isPrimarySource(s1);
80       boolean s2Primary = isPrimarySource(s2);
81       if (s1Primary && !s2Primary)
82       {
83         return -1;
84       }
85       if (!s1Primary && s2Primary)
86       {
87         return 1;
88       }
89       int comp = s1 == null ? -1
90               : (s2 == null ? 1 : s1.compareToIgnoreCase(s2));
91       if (comp == 0)
92       {
93         String a1 = ref1.getAccessionId();
94         String a2 = ref2.getAccessionId();
95         comp = a1 == null ? -1
96                 : (a2 == null ? 1 : a1.compareToIgnoreCase(a2));
97       }
98       return comp;
99     }
100
101     private boolean isPrimarySource(String source)
102     {
103       for (String[] primary : PRIMARY_SOURCES)
104       {
105         for (String s : primary)
106         {
107           if (source.equals(s))
108           {
109             return true;
110           }
111         }
112       }
113       return false;
114     }
115   };
116
117   public SequenceAnnotationReport(String linkImageURL)
118   {
119     this.linkImageURL = linkImageURL;
120   }
121
122   /**
123    * Append text for the list of features to the tooltip
124    * 
125    * @param sb
126    * @param rpos
127    * @param features
128    * @param minmax
129    */
130   public void appendFeatures(final StringBuilder sb, int rpos,
131           List<SequenceFeature> features, Map<String, float[][]> minmax)
132   {
133     if (features != null)
134     {
135       for (SequenceFeature feature : features)
136       {
137         appendFeature(sb, rpos, minmax, feature);
138       }
139     }
140   }
141
142   /**
143    * Appends the feature at rpos to the given buffer
144    * 
145    * @param sb
146    * @param rpos
147    * @param minmax
148    * @param feature
149    */
150   void appendFeature(final StringBuilder sb, int rpos,
151           Map<String, float[][]> minmax, SequenceFeature feature)
152   {
153     if (feature.isContactFeature())
154     {
155       if (feature.getBegin() == rpos || feature.getEnd() == rpos)
156       {
157         if (sb.length() > 6)
158         {
159           sb.append("<br>");
160         }
161         sb.append(feature.getType()).append(" ").append(feature.getBegin())
162                 .append(":").append(feature.getEnd());
163       }
164     }
165     else
166     {
167       if (sb.length() > 6)
168       {
169         sb.append("<br>");
170       }
171       // TODO: remove this hack to display link only features
172       boolean linkOnly = feature.getValue("linkonly") != null;
173       if (!linkOnly)
174       {
175         sb.append(feature.getType()).append(" ");
176         if (rpos != 0)
177         {
178           // we are marking a positional feature
179           sb.append(feature.begin);
180         }
181         if (feature.begin != feature.end)
182         {
183           sb.append(" ").append(feature.end);
184         }
185
186         if (feature.getDescription() != null
187                 && !feature.description.equals(feature.getType()))
188         {
189           String tmpString = feature.getDescription();
190           String tmp2up = tmpString.toUpperCase();
191           int startTag = tmp2up.indexOf("<HTML>");
192           if (startTag > -1)
193           {
194             tmpString = tmpString.substring(startTag + 6);
195             tmp2up = tmp2up.substring(startTag + 6);
196           }
197           int endTag = tmp2up.indexOf("</BODY>");
198           if (endTag > -1)
199           {
200             tmpString = tmpString.substring(0, endTag);
201             tmp2up = tmp2up.substring(0, endTag);
202           }
203           endTag = tmp2up.indexOf("</HTML>");
204           if (endTag > -1)
205           {
206             tmpString = tmpString.substring(0, endTag);
207           }
208
209           if (startTag > -1)
210           {
211             sb.append("; ").append(tmpString);
212           }
213           else
214           {
215             if (tmpString.indexOf("<") > -1 || tmpString.indexOf(">") > -1)
216             {
217               // The description does not specify html is to
218               // be used, so we must remove < > symbols
219               tmpString = tmpString.replaceAll("<", "&lt;");
220               tmpString = tmpString.replaceAll(">", "&gt;");
221
222               sb.append("; ");
223               sb.append(tmpString);
224             }
225             else
226             {
227               sb.append("; ").append(tmpString);
228             }
229           }
230         }
231         // check score should be shown
232         if (!Float.isNaN(feature.getScore()))
233         {
234           float[][] rng = (minmax == null) ? null
235                   : minmax.get(feature.getType());
236           if (rng != null && rng[0] != null && rng[0][0] != rng[0][1])
237           {
238             sb.append(" Score=").append(String.valueOf(feature.getScore()));
239           }
240         }
241         String status = (String) feature.getValue("status");
242         if (status != null && status.length() > 0)
243         {
244           sb.append("; (").append(status).append(")");
245         }
246         String clinSig = (String) feature
247                 .getValue(GffConstants.CLINICAL_SIGNIFICANCE);
248         if (clinSig != null)
249         {
250           sb.append("; ").append(clinSig);
251         }
252       }
253     }
254   }
255
256   /**
257    * Format and appends any hyperlinks for the sequence feature to the string
258    * buffer
259    * 
260    * @param sb
261    * @param feature
262    */
263   void appendLinks(final StringBuffer sb, SequenceFeature feature)
264   {
265     if (feature.links != null)
266     {
267       if (linkImageURL != null)
268       {
269         sb.append(" <img src=\"" + linkImageURL + "\">");
270       }
271       else
272       {
273         for (String urlstring : feature.links)
274         {
275           try
276           {
277             for (List<String> urllink : createLinksFrom(null, urlstring))
278             {
279               sb.append("<br/> <a href=\"" + urllink.get(3) + "\" target=\""
280                       + urllink.get(0) + "\">"
281                       + (urllink.get(0).toLowerCase()
282                               .equals(urllink.get(1).toLowerCase())
283                                       ? urllink.get(0)
284                                       : (urllink.get(0) + ":"
285                                               + urllink.get(1)))
286                       + "</a></br>");
287             }
288           } catch (Exception x)
289           {
290             System.err.println(
291                     "problem when creating links from " + urlstring);
292             x.printStackTrace();
293           }
294         }
295       }
296
297     }
298   }
299
300   /**
301    * 
302    * @param seq
303    * @param link
304    * @return Collection< List<String> > { List<String> { link target, link
305    *         label, dynamic component inserted (if any), url }}
306    */
307   Collection<List<String>> createLinksFrom(SequenceI seq, String link)
308   {
309     Map<String, List<String>> urlSets = new LinkedHashMap<String, List<String>>();
310     UrlLink urlLink = new UrlLink(link);
311     if (!urlLink.isValid())
312     {
313       System.err.println(urlLink.getInvalidMessage());
314       return null;
315     }
316
317     urlLink.createLinksFromSeq(seq, urlSets);
318
319     return urlSets.values();
320   }
321
322   public void createSequenceAnnotationReport(final StringBuilder tip,
323           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
324           Map<String, float[][]> minmax)
325   {
326     createSequenceAnnotationReport(tip, sequence, showDbRefs, showNpFeats,
327             minmax, false);
328   }
329
330   /**
331    * Builds an html formatted report of sequence details and appends it to the
332    * provided buffer.
333    * 
334    * @param sb
335    *          buffer to append report to
336    * @param sequence
337    *          the sequence the report is for
338    * @param showDbRefs
339    *          whether to include database references for the sequence
340    * @param showNpFeats
341    *          whether to include non-positional sequence features
342    * @param minmax
343    * @param summary
344    * @return
345    */
346   int createSequenceAnnotationReport(final StringBuilder sb,
347           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
348           Map<String, float[][]> minmax, boolean summary)
349   {
350     String tmp;
351     sb.append("<i>");
352
353     int maxWidth = 0;
354     if (sequence.getDescription() != null)
355     {
356       tmp = sequence.getDescription();
357       sb.append("<br>").append(tmp);
358       maxWidth = Math.max(maxWidth, tmp.length());
359     }
360     SequenceI ds = sequence;
361     while (ds.getDatasetSequence() != null)
362     {
363       ds = ds.getDatasetSequence();
364     }
365     
366     if (showDbRefs)
367     {
368       maxWidth = Math.max(maxWidth, appendDbRefs(sb, ds, summary));
369     }
370
371     /*
372      * add non-positional features if wanted
373      */
374     if (showNpFeats)
375     {
376       for (SequenceFeature sf : sequence.getFeatures()
377               .getNonPositionalFeatures())
378       {
379         int sz = -sb.length();
380         appendFeature(sb, 0, minmax, sf);
381         sz += sb.length();
382         maxWidth = Math.max(maxWidth, sz);
383       }
384     }
385     sb.append("</i>");
386     return maxWidth;
387   }
388
389   /**
390    * A helper method that appends any DBRefs, returning the maximum line length
391    * added
392    * 
393    * @param sb
394    * @param ds
395    * @param summary
396    * @return
397    */
398   protected int appendDbRefs(final StringBuilder sb, SequenceI ds,
399           boolean summary)
400   {
401     DBRefEntry[] dbrefs = ds.getDBRefs();
402     if (dbrefs == null)
403     {
404       return 0;
405     }
406
407     // note this sorts the refs held on the sequence!
408     Arrays.sort(dbrefs, comparator);
409     boolean ellipsis = false;
410     String source = null;
411     String lastSource = null;
412     int countForSource = 0;
413     int sourceCount = 0;
414     boolean moreSources = false;
415     int maxLineLength = 0;
416     int lineLength = 0;
417
418     for (DBRefEntry ref : dbrefs)
419     {
420       source = ref.getSource();
421       if (source == null)
422       {
423         // shouldn't happen
424         continue;
425       }
426       boolean sourceChanged = !source.equals(lastSource);
427       if (sourceChanged)
428       {
429         lineLength = 0;
430         countForSource = 0;
431         sourceCount++;
432       }
433       if (sourceCount > MAX_SOURCES && summary)
434       {
435         ellipsis = true;
436         moreSources = true;
437         break;
438       }
439       lastSource = source;
440       countForSource++;
441       if (countForSource == 1 || !summary)
442       {
443         sb.append("<br>");
444       }
445       if (countForSource <= MAX_REFS_PER_SOURCE || !summary)
446       {
447         String accessionId = ref.getAccessionId();
448         lineLength += accessionId.length() + 1;
449         if (countForSource > 1 && summary)
450         {
451           sb.append(", ").append(accessionId);
452           lineLength++;
453         }
454         else
455         {
456           sb.append(source).append(" ").append(accessionId);
457           lineLength += source.length();
458         }
459         maxLineLength = Math.max(maxLineLength, lineLength);
460       }
461       if (countForSource == MAX_REFS_PER_SOURCE && summary)
462       {
463         sb.append(COMMA).append(ELLIPSIS);
464         ellipsis = true;
465       }
466     }
467     if (moreSources)
468     {
469       sb.append("<br>").append(source)
470               .append(COMMA).append(ELLIPSIS);
471     }
472     if (ellipsis)
473     {
474       sb.append("<br>(");
475       sb.append(MessageManager.getString("label.output_seq_details"));
476       sb.append(")");
477     }
478
479     return maxLineLength;
480   }
481
482   public void createTooltipAnnotationReport(final StringBuilder tip,
483           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
484           Map<String, float[][]> minmax)
485   {
486     int maxWidth = createSequenceAnnotationReport(tip, sequence, showDbRefs,
487             showNpFeats, minmax, true);
488
489     if (maxWidth > 60)
490     {
491       // ? not sure this serves any useful purpose
492       // tip.insert(0, "<table width=350 border=0><tr><td>");
493       // tip.append("</td></tr></table>");
494     }
495   }
496 }