JAL-2136 Introduced DynamicData model, modified AnnotionFile to store Phyre meta...
[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.DynamicData;
26 import jalview.datamodel.PDBEntry;
27 import jalview.datamodel.SequenceFeature;
28 import jalview.datamodel.SequenceI;
29 import jalview.io.gff.GffConstants;
30 import jalview.util.MessageManager;
31 import jalview.util.UrlLink;
32
33 import java.util.Arrays;
34 import java.util.Collection;
35 import java.util.Comparator;
36 import java.util.LinkedHashMap;
37 import java.util.List;
38 import java.util.Map;
39
40 /**
41  * generate HTML reports for a sequence
42  * 
43  * @author jimp
44  */
45 public class SequenceAnnotationReport
46 {
47   private static final String COMMA = ",";
48
49   private static final String ELLIPSIS = "...";
50
51   private static final int MAX_REFS_PER_SOURCE = 4;
52
53   private static final int MAX_SOURCES = 40;
54
55   private static final String[][] PRIMARY_SOURCES = new String[][] {
56       DBRefSource.CODINGDBS, DBRefSource.DNACODINGDBS,
57       DBRefSource.PROTEINDBS };
58
59   final String linkImageURL;
60
61   /*
62    * Comparator to order DBRefEntry by Source + accession id (case-insensitive)
63    */
64   private static Comparator<DBRefEntry> comparator = new Comparator<DBRefEntry>()
65   {
66
67     @Override
68     public int compare(DBRefEntry ref1, DBRefEntry ref2)
69     {
70       String s1 = ref1.getSource();
71       String s2 = ref2.getSource();
72       boolean s1Primary = isPrimarySource(s1);
73       boolean s2Primary = isPrimarySource(s2);
74       if (s1Primary && !s2Primary)
75       {
76         return -1;
77       }
78       if (!s1Primary && s2Primary)
79       {
80         return 1;
81       }
82       int comp = s1 == null ? -1 : (s2 == null ? 1 : s1
83               .compareToIgnoreCase(s2));
84       if (comp == 0)
85       {
86         String a1 = ref1.getAccessionId();
87         String a2 = ref2.getAccessionId();
88         comp = a1 == null ? -1 : (a2 == null ? 1 : a1
89                 .compareToIgnoreCase(a2));
90       }
91       return comp;
92     }
93
94     private boolean isPrimarySource(String source)
95     {
96       for (String[] primary : PRIMARY_SOURCES)
97       {
98         for (String s : primary)
99         {
100           if (source.equals(s))
101           {
102             return true;
103           }
104         }
105       }
106       return false;
107     }
108   };
109
110   public SequenceAnnotationReport(String linkImageURL)
111   {
112     this.linkImageURL = linkImageURL;
113   }
114
115   /**
116    * Append text for the list of features to the tooltip
117    * 
118    * @param sb
119    * @param rpos
120    * @param features
121    * @param minmax
122    */
123   public void appendFeatures(final StringBuilder sb, int rpos,
124           List<SequenceFeature> features, Map<String, float[][]> minmax)
125   {
126     if (features != null)
127     {
128       for (SequenceFeature feature : features)
129       {
130         appendFeature(sb, rpos, minmax, feature);
131       }
132     }
133   }
134
135   /**
136    * Appends the feature at rpos to the given buffer
137    * 
138    * @param sb
139    * @param rpos
140    * @param minmax
141    * @param feature
142    */
143   void appendFeature(final StringBuilder sb, int rpos,
144           Map<String, float[][]> minmax, SequenceFeature feature)
145   {
146     if (feature.isContactFeature())
147     {
148       if (feature.getBegin() == rpos || feature.getEnd() == rpos)
149       {
150         if (sb.length() > 6)
151         {
152           sb.append("<br>");
153         }
154         sb.append(feature.getType()).append(" ").append(feature.getBegin())
155                 .append(":")
156                 .append(feature.getEnd());
157       }
158     }
159     else
160     {
161       if (sb.length() > 6)
162       {
163         sb.append("<br>");
164       }
165       // TODO: remove this hack to display link only features
166       boolean linkOnly = feature.getValue("linkonly") != null;
167       if (!linkOnly)
168       {
169         sb.append(feature.getType()).append(" ");
170         if (rpos != 0)
171         {
172           // we are marking a positional feature
173           sb.append(feature.begin);
174         }
175         if (feature.begin != feature.end)
176         {
177           sb.append(" ").append(feature.end);
178         }
179
180         if (feature.getDescription() != null
181                 && !feature.description.equals(feature.getType()))
182         {
183           String tmpString = feature.getDescription();
184           String tmp2up = tmpString.toUpperCase();
185           int startTag = tmp2up.indexOf("<HTML>");
186           if (startTag > -1)
187           {
188             tmpString = tmpString.substring(startTag + 6);
189             tmp2up = tmp2up.substring(startTag + 6);
190           }
191           int endTag = tmp2up.indexOf("</BODY>");
192           if (endTag > -1)
193           {
194             tmpString = tmpString.substring(0, endTag);
195             tmp2up = tmp2up.substring(0, endTag);
196           }
197           endTag = tmp2up.indexOf("</HTML>");
198           if (endTag > -1)
199           {
200             tmpString = tmpString.substring(0, endTag);
201           }
202
203           if (startTag > -1)
204           {
205             sb.append("; ").append(tmpString);
206           }
207           else
208           {
209             if (tmpString.indexOf("<") > -1 || tmpString.indexOf(">") > -1)
210             {
211               // The description does not specify html is to
212               // be used, so we must remove < > symbols
213               tmpString = tmpString.replaceAll("<", "&lt;");
214               tmpString = tmpString.replaceAll(">", "&gt;");
215
216               sb.append("; ");
217               sb.append(tmpString);
218             }
219             else
220             {
221               sb.append("; ").append(tmpString);
222             }
223           }
224         }
225         // check score should be shown
226         if (!Float.isNaN(feature.getScore()))
227         {
228           float[][] rng = (minmax == null) ? null : minmax.get(feature
229                   .getType());
230           if (rng != null && rng[0] != null && rng[0][0] != rng[0][1])
231           {
232             sb.append(" Score=").append(String.valueOf(feature.getScore()));
233           }
234         }
235         String status = (String) feature.getValue("status");
236         if (status != null && status.length() > 0)
237         {
238           sb.append("; (").append(status).append(")");
239         }
240         String clinSig = (String) feature
241                 .getValue(GffConstants.CLINICAL_SIGNIFICANCE);
242         if (clinSig != null)
243         {
244           sb.append("; ").append(clinSig);
245         }
246       }
247     }
248   }
249
250   /**
251    * Format and appends any hyperlinks for the sequence feature to the string
252    * buffer
253    * 
254    * @param sb
255    * @param feature
256    */
257   void appendLinks(final StringBuffer sb, SequenceFeature feature)
258   {
259     if (feature.links != null)
260     {
261       if (linkImageURL != null)
262       {
263         sb.append(" <img src=\"" + linkImageURL + "\">");
264       }
265       else
266       {
267         for (String urlstring : feature.links)
268         {
269           try
270           {
271             for (List<String> urllink : createLinksFrom(null, urlstring))
272             {
273               sb.append("<br/> <a href=\""
274                       + urllink.get(3)
275                       + "\" target=\""
276                       + urllink.get(0)
277                       + "\">"
278                       + (urllink.get(0).toLowerCase()
279                               .equals(urllink.get(1).toLowerCase()) ? urllink
280                               .get(0) : (urllink.get(0) + ":" + urllink
281                               .get(1)))
282                       + "</a></br>");
283             }
284           } catch (Exception x)
285           {
286             System.err.println("problem when creating links from "
287                     + urlstring);
288             x.printStackTrace();
289           }
290         }
291       }
292
293     }
294   }
295
296   /**
297    * 
298    * @param seq
299    * @param link
300    * @return Collection< List<String> > { List<String> { link target, link
301    *         label, dynamic component inserted (if any), url }}
302    */
303   Collection<List<String>> createLinksFrom(SequenceI seq, String link)
304   {
305     Map<String, List<String>> urlSets = new LinkedHashMap<String, List<String>>();
306     UrlLink urlLink = new UrlLink(link);
307     if (!urlLink.isValid())
308     {
309       System.err.println(urlLink.getInvalidMessage());
310       return null;
311     }
312
313     urlLink.createLinksFromSeq(seq, urlSets);
314
315     return urlSets.values();
316   }
317
318   public void createSequenceAnnotationReport(final StringBuilder tip,
319           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
320           Map<String, float[][]> minmax)
321   {
322     createSequenceAnnotationReport(tip, sequence, showDbRefs, showNpFeats,
323             minmax, false);
324   }
325
326   /**
327    * Builds an html formatted report of sequence details and appends it to the
328    * provided buffer.
329    * 
330    * @param sb
331    *          buffer to append report to
332    * @param sequence
333    *          the sequence the report is for
334    * @param showDbRefs
335    *          whether to include database references for the sequence
336    * @param showNpFeats
337    *          whether to include non-positional sequence features
338    * @param minmax
339    * @param summary
340    * @return
341    */
342   int createSequenceAnnotationReport(final StringBuilder sb,
343           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
344           Map<String, float[][]> minmax, boolean summary)
345   {
346     String tmp;
347     sb.append("<i>");
348
349     int maxWidth = 0;
350     if (sequence.getDescription() != null)
351     {
352       tmp = sequence.getDescription();
353       sb.append("<br>").append(tmp);
354       maxWidth = Math.max(maxWidth, tmp.length());
355     }
356     SequenceI ds = sequence;
357     while (ds.getDatasetSequence() != null)
358     {
359       ds = ds.getDatasetSequence();
360     }
361     DBRefEntry[] dbrefs = ds.getDBRefs();
362     if (showDbRefs && dbrefs != null)
363     {
364       // note this sorts the refs held on the sequence!
365       Arrays.sort(dbrefs, comparator);
366       boolean ellipsis = false;
367       String source = null;
368       String lastSource = null;
369       int countForSource = 0;
370       int sourceCount = 0;
371       boolean moreSources = false;
372       int lineLength = 0;
373
374       for (DBRefEntry ref : dbrefs)
375       {
376         source = ref.getSource();
377         if (source == null)
378         {
379           // shouldn't happen
380           continue;
381         }
382         boolean sourceChanged = !source.equals(lastSource);
383         if (sourceChanged)
384         {
385           lineLength = 0;
386           countForSource = 0;
387           sourceCount++;
388         }
389         if (sourceCount > MAX_SOURCES && summary)
390         {
391           ellipsis = true;
392           moreSources = true;
393           break;
394         }
395         lastSource = source;
396         countForSource++;
397         if (countForSource == 1 || !summary)
398         {
399           sb.append("<br>");
400         }
401         if (countForSource <= MAX_REFS_PER_SOURCE || !summary)
402         {
403           String accessionId = ref.getAccessionId();
404           lineLength += accessionId.length() + 1;
405           if (countForSource > 1 && summary)
406           {
407             sb.append(", ").append(accessionId);
408             lineLength++;
409           }
410           else
411           {
412             sb.append(source).append(" ").append(accessionId);
413             lineLength += source.length();
414           }
415           maxWidth = Math.max(maxWidth, lineLength);
416         }
417         if (countForSource == MAX_REFS_PER_SOURCE && summary)
418         {
419           sb.append(COMMA).append(ELLIPSIS);
420           ellipsis = true;
421         }
422       }
423       if (moreSources)
424       {
425         sb.append("<br>").append(ELLIPSIS).append(COMMA).append(source)
426                 .append(COMMA).append(ELLIPSIS);
427       }
428       if (ellipsis)
429       {
430         sb.append("<br>(");
431         sb.append(MessageManager.getString("label.output_seq_details"));
432         sb.append(")");
433       }
434     }
435
436     /*
437      * add non-positional features if wanted
438      */
439     SequenceFeature[] features = sequence.getSequenceFeatures();
440     if (showNpFeats && features != null)
441     {
442       for (int i = 0; i < features.length; i++)
443       {
444         if (features[i].begin == 0 && features[i].end == 0)
445         {
446           int sz = -sb.length();
447           appendFeature(sb, 0, minmax, features[i]);
448           sz += sb.length();
449           maxWidth = Math.max(maxWidth, sz);
450         }
451       }
452     }
453     sb.append("</i>");
454     List<PDBEntry> pdbEntries = ds.getAllPDBEntries();
455     sb.append(getToolTipTextFromPDBEntries(pdbEntries));
456     return maxWidth;
457   }
458
459   private String getToolTipTextFromPDBEntries(List<PDBEntry> pdbEntries)
460   {
461     String tooltip = "";
462     if (pdbEntries.isEmpty())
463     {
464       return tooltip;
465     }
466     if (pdbEntries.size() > 1)
467     {
468       int x = 0;
469       PDBEntry bestRanked = null;
470       for (PDBEntry pdb : pdbEntries)
471       {
472         if (pdb.getProperty("DYNAMIC_DATA_PHYRE2") != null)
473         {
474           x++;
475         }
476         // best ranked entry must be from a Phyre
477         if (x > 0 && bestRanked == null)
478         {
479           bestRanked = pdb;
480         }
481       }
482       tooltip = (x > 0) ? "<table border=\"1\" width=100%><tr><td>Contains <b>"
483               + x
484               + "</b> Phyre2 model structure(s)</td></tr>"
485               + "<tr><td>Best ranked Phyre2 model is <b>"
486               + bestRanked.getId() + "</b></td></tr></table>"
487               : "";
488     }
489     else
490     {
491       PDBEntry pdb = pdbEntries.iterator().next();
492       if (pdb.getProperty("DYNAMIC_DATA_PHYRE2") != null)
493       {
494         tooltip = getPhyreToolTipFromDynamicData((List<DynamicData>) pdb
495                 .getProperty("DYNAMIC_DATA_PHYRE2"));
496       }
497     }
498     return tooltip;
499   }
500
501   private String getPhyreToolTipFromDynamicData(
502           List<DynamicData> dynamicDataList)
503   {
504     StringBuilder phyre2InfoBuilder = new StringBuilder();
505     phyre2InfoBuilder
506             .append("<html><table border=\"1\" width=100%>")
507             .append("<tr><td colspan=\"2\"><strong>Phyre2 Template Info</strong></td></tr>");
508     for (DynamicData data : dynamicDataList)
509     {
510       if (data.isDisplay())
511       {
512         phyre2InfoBuilder.append("<tr><td>").append(data.getFieldTitle())
513                 .append("</td><td>").append(data.getFieldValue())
514                 .append("</td></tr>");
515       }
516     }
517     phyre2InfoBuilder.append("</table></html>");
518     return phyre2InfoBuilder.toString();
519   }
520
521   public void createTooltipAnnotationReport(final StringBuilder tip,
522           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
523           Map<String, float[][]> minmax)
524   {
525     int maxWidth = createSequenceAnnotationReport(tip, sequence,
526             showDbRefs, showNpFeats, minmax, true);
527
528     if (maxWidth > 60)
529     {
530       // ? not sure this serves any useful purpose
531       // tip.insert(0, "<table width=350 border=0><tr><td>");
532       // tip.append("</td></tr></table>");
533     }
534   }
535 }