JAL-2422 pull up refactoring in preparation for ChimeraX subclasses
[jalview.git] / src / jalview / structures / models / AAStructureBindingModel.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.structures.models;
22
23 import jalview.api.AlignmentViewPanel;
24 import jalview.api.FeatureRenderer;
25 import jalview.api.SequenceRenderer;
26 import jalview.api.StructureSelectionManagerProvider;
27 import jalview.api.structures.JalviewStructureDisplayI;
28 import jalview.datamodel.AlignmentI;
29 import jalview.datamodel.HiddenColumns;
30 import jalview.datamodel.PDBEntry;
31 import jalview.datamodel.SequenceI;
32 import jalview.io.DataSourceType;
33 import jalview.schemes.ColourSchemeI;
34 import jalview.structure.AtomSpec;
35 import jalview.structure.StructureListener;
36 import jalview.structure.StructureMapping;
37 import jalview.structure.StructureMappingcommandSet;
38 import jalview.structure.StructureSelectionManager;
39 import jalview.util.Comparison;
40 import jalview.util.MessageManager;
41
42 import java.awt.Color;
43 import java.util.ArrayList;
44 import java.util.Arrays;
45 import java.util.BitSet;
46 import java.util.List;
47
48 /**
49  * 
50  * A base class to hold common function for protein structure model binding.
51  * Initial version created by refactoring JMol and Chimera binding models, but
52  * other structure viewers could in principle be accommodated in future.
53  * 
54  * @author gmcarstairs
55  *
56  */
57 public abstract class AAStructureBindingModel
58         extends SequenceStructureBindingModel
59         implements StructureListener, StructureSelectionManagerProvider
60 {
61   /*
62    * the Jalview panel through which the user interacts
63    * with the structure viewer
64    */
65   private JalviewStructureDisplayI viewer;
66
67   private StructureSelectionManager ssm;
68
69   /*
70    * distinct PDB entries (pdb files) associated
71    * with sequences
72    */
73   private PDBEntry[] pdbEntry;
74
75   /*
76    * sequences mapped to each pdbentry
77    */
78   private SequenceI[][] sequence;
79
80   /*
81    * array of target chains for sequences - tied to pdbentry and sequence[]
82    */
83   private String[][] chains;
84
85   /*
86    * datasource protocol for access to PDBEntrylatest
87    */
88   DataSourceType protocol = null;
89
90   protected boolean colourBySequence = true;
91
92   private boolean nucleotide;
93
94   private boolean finishedInit = false;
95
96   /**
97    * current set of model filenames loaded in the Jmol instance
98    */
99   protected String[] modelFileNames = null;
100
101   public String fileLoadingError;
102
103   /**
104    * Data bean class to simplify parameterisation in superposeStructures
105    */
106   protected class SuperposeData
107   {
108     /**
109      * Constructor with alignment width argument
110      * 
111      * @param width
112      */
113     public SuperposeData(int width)
114     {
115       pdbResNo = new int[width];
116     }
117
118     public String filename;
119
120     public String pdbId;
121
122     public String chain = "";
123
124     public boolean isRna;
125
126     /*
127      * The pdb residue number (if any) mapped to each column of the alignment
128      */
129     public int[] pdbResNo;
130   }
131
132   /**
133    * Constructor
134    * 
135    * @param ssm
136    * @param seqs
137    */
138   public AAStructureBindingModel(StructureSelectionManager ssm,
139           SequenceI[][] seqs)
140   {
141     this.ssm = ssm;
142     this.sequence = seqs;
143   }
144
145   /**
146    * Constructor
147    * 
148    * @param ssm
149    * @param pdbentry
150    * @param sequenceIs
151    * @param protocol
152    */
153   public AAStructureBindingModel(StructureSelectionManager ssm,
154           PDBEntry[] pdbentry, SequenceI[][] sequenceIs,
155           DataSourceType protocol)
156   {
157     this.ssm = ssm;
158     this.sequence = sequenceIs;
159     this.nucleotide = Comparison.isNucleotide(sequenceIs);
160     this.pdbEntry = pdbentry;
161     this.protocol = protocol;
162     resolveChains();
163   }
164
165   private boolean resolveChains()
166   {
167     /**
168      * final count of chain mappings discovered
169      */
170     int chainmaps = 0;
171     // JBPNote: JAL-2693 - this should be a list of chain mappings per
172     // [pdbentry][sequence]
173     String[][] newchains = new String[pdbEntry.length][];
174     int pe = 0;
175     for (PDBEntry pdb : pdbEntry)
176     {
177       SequenceI[] seqsForPdb = sequence[pe];
178       if (seqsForPdb != null)
179       {
180         newchains[pe] = new String[seqsForPdb.length];
181         int se = 0;
182         for (SequenceI asq : seqsForPdb)
183         {
184           String chain = (chains != null && chains[pe] != null)
185                   ? chains[pe][se]
186                   : null;
187           SequenceI sq = (asq.getDatasetSequence() == null) ? asq
188                   : asq.getDatasetSequence();
189           if (sq.getAllPDBEntries() != null)
190           {
191             for (PDBEntry pdbentry : sq.getAllPDBEntries())
192             {
193               if (pdb.getFile() != null && pdbentry.getFile() != null
194                       && pdb.getFile().equals(pdbentry.getFile()))
195               {
196                 String chaincode = pdbentry.getChainCode();
197                 if (chaincode != null && chaincode.length() > 0)
198                 {
199                   chain = chaincode;
200                   chainmaps++;
201                   break;
202                 }
203               }
204             }
205           }
206           newchains[pe][se] = chain;
207           se++;
208         }
209         pe++;
210       }
211     }
212
213     chains = newchains;
214     return chainmaps > 0;
215   }
216   public StructureSelectionManager getSsm()
217   {
218     return ssm;
219   }
220
221   /**
222    * Returns the i'th PDBEntry (or null)
223    * 
224    * @param i
225    * @return
226    */
227   public PDBEntry getPdbEntry(int i)
228   {
229     return (pdbEntry != null && pdbEntry.length > i) ? pdbEntry[i] : null;
230   }
231
232   /**
233    * Answers true if this binding includes the given PDB id, else false
234    * 
235    * @param pdbId
236    * @return
237    */
238   public boolean hasPdbId(String pdbId)
239   {
240     if (pdbEntry != null)
241     {
242       for (PDBEntry pdb : pdbEntry)
243       {
244         if (pdb.getId().equals(pdbId))
245         {
246           return true;
247         }
248       }
249     }
250     return false;
251   }
252
253   /**
254    * Returns the number of modelled PDB file entries.
255    * 
256    * @return
257    */
258   public int getPdbCount()
259   {
260     return pdbEntry == null ? 0 : pdbEntry.length;
261   }
262
263   public SequenceI[][] getSequence()
264   {
265     return sequence;
266   }
267
268   public String[][] getChains()
269   {
270     return chains;
271   }
272
273   public DataSourceType getProtocol()
274   {
275     return protocol;
276   }
277
278   // TODO may remove this if calling methods can be pulled up here
279   protected void setPdbentry(PDBEntry[] pdbentry)
280   {
281     this.pdbEntry = pdbentry;
282   }
283
284   protected void setSequence(SequenceI[][] sequence)
285   {
286     this.sequence = sequence;
287   }
288
289   protected void setChains(String[][] chains)
290   {
291     this.chains = chains;
292   }
293
294   /**
295    * Construct a title string for the viewer window based on the data Jalview
296    * knows about
297    * 
298    * @param viewerName
299    *          TODO
300    * @param verbose
301    * 
302    * @return
303    */
304   public String getViewerTitle(String viewerName, boolean verbose)
305   {
306     if (getSequence() == null || getSequence().length < 1
307             || getPdbCount() < 1 || getSequence()[0].length < 1)
308     {
309       return ("Jalview " + viewerName + " Window");
310     }
311     // TODO: give a more informative title when multiple structures are
312     // displayed.
313     StringBuilder title = new StringBuilder(64);
314     final PDBEntry pdbe = getPdbEntry(0);
315     title.append(viewerName + " view for " + getSequence()[0][0].getName()
316             + ":" + pdbe.getId());
317
318     if (verbose)
319     {
320       String method = (String) pdbe.getProperty("method");
321       if (method != null)
322       {
323         title.append(" Method: ").append(method);
324       }
325       String chain = (String) pdbe.getProperty("chains");
326       if (chain != null)
327       {
328         title.append(" Chain:").append(chain);
329       }
330     }
331     return title.toString();
332   }
333
334   /**
335    * Called by after closeViewer is called, to release any resources and
336    * references so they can be garbage collected. Override if needed.
337    */
338   protected void releaseUIResources()
339   {
340
341   }
342
343   public boolean isColourBySequence()
344   {
345     return colourBySequence;
346   }
347
348   public void setColourBySequence(boolean colourBySequence)
349   {
350     this.colourBySequence = colourBySequence;
351   }
352
353   protected void addSequenceAndChain(int pe, SequenceI[] seq,
354           String[] tchain)
355   {
356     if (pe < 0 || pe >= getPdbCount())
357     {
358       throw new Error(MessageManager.formatMessage(
359               "error.implementation_error_no_pdbentry_from_index",
360               new Object[]
361               { Integer.valueOf(pe).toString() }));
362     }
363     final String nullChain = "TheNullChain";
364     List<SequenceI> s = new ArrayList<>();
365     List<String> c = new ArrayList<>();
366     if (getChains() == null)
367     {
368       setChains(new String[getPdbCount()][]);
369     }
370     if (getSequence()[pe] != null)
371     {
372       for (int i = 0; i < getSequence()[pe].length; i++)
373       {
374         s.add(getSequence()[pe][i]);
375         if (getChains()[pe] != null)
376         {
377           if (i < getChains()[pe].length)
378           {
379             c.add(getChains()[pe][i]);
380           }
381           else
382           {
383             c.add(nullChain);
384           }
385         }
386         else
387         {
388           if (tchain != null && tchain.length > 0)
389           {
390             c.add(nullChain);
391           }
392         }
393       }
394     }
395     for (int i = 0; i < seq.length; i++)
396     {
397       if (!s.contains(seq[i]))
398       {
399         s.add(seq[i]);
400         if (tchain != null && i < tchain.length)
401         {
402           c.add(tchain[i] == null ? nullChain : tchain[i]);
403         }
404       }
405     }
406     SequenceI[] tmp = s.toArray(new SequenceI[s.size()]);
407     getSequence()[pe] = tmp;
408     if (c.size() > 0)
409     {
410       String[] tch = c.toArray(new String[c.size()]);
411       for (int i = 0; i < tch.length; i++)
412       {
413         if (tch[i] == nullChain)
414         {
415           tch[i] = null;
416         }
417       }
418       getChains()[pe] = tch;
419     }
420     else
421     {
422       getChains()[pe] = null;
423     }
424   }
425
426   /**
427    * add structures and any known sequence associations
428    * 
429    * @returns the pdb entries added to the current set.
430    */
431   public synchronized PDBEntry[] addSequenceAndChain(PDBEntry[] pdbe,
432           SequenceI[][] seq, String[][] chns)
433   {
434     List<PDBEntry> v = new ArrayList<>();
435     List<int[]> rtn = new ArrayList<>();
436     for (int i = 0; i < getPdbCount(); i++)
437     {
438       v.add(getPdbEntry(i));
439     }
440     for (int i = 0; i < pdbe.length; i++)
441     {
442       int r = v.indexOf(pdbe[i]);
443       if (r == -1 || r >= getPdbCount())
444       {
445         rtn.add(new int[] { v.size(), i });
446         v.add(pdbe[i]);
447       }
448       else
449       {
450         // just make sure the sequence/chain entries are all up to date
451         addSequenceAndChain(r, seq[i], chns[i]);
452       }
453     }
454     pdbe = v.toArray(new PDBEntry[v.size()]);
455     setPdbentry(pdbe);
456     if (rtn.size() > 0)
457     {
458       // expand the tied sequence[] and string[] arrays
459       SequenceI[][] sqs = new SequenceI[getPdbCount()][];
460       String[][] sch = new String[getPdbCount()][];
461       System.arraycopy(getSequence(), 0, sqs, 0, getSequence().length);
462       System.arraycopy(getChains(), 0, sch, 0, this.getChains().length);
463       setSequence(sqs);
464       setChains(sch);
465       pdbe = new PDBEntry[rtn.size()];
466       for (int r = 0; r < pdbe.length; r++)
467       {
468         int[] stri = (rtn.get(r));
469         // record the pdb file as a new addition
470         pdbe[r] = getPdbEntry(stri[0]);
471         // and add the new sequence/chain entries
472         addSequenceAndChain(stri[0], seq[stri[1]], chns[stri[1]]);
473       }
474     }
475     else
476     {
477       pdbe = null;
478     }
479     return pdbe;
480   }
481
482   /**
483    * Add sequences to the pe'th pdbentry's sequence set.
484    * 
485    * @param pe
486    * @param seq
487    */
488   public void addSequence(int pe, SequenceI[] seq)
489   {
490     addSequenceAndChain(pe, seq, null);
491   }
492
493   /**
494    * add the given sequences to the mapping scope for the given pdb file handle
495    * 
496    * @param pdbFile
497    *          - pdbFile identifier
498    * @param seq
499    *          - set of sequences it can be mapped to
500    */
501   public void addSequenceForStructFile(String pdbFile, SequenceI[] seq)
502   {
503     for (int pe = 0; pe < getPdbCount(); pe++)
504     {
505       if (getPdbEntry(pe).getFile().equals(pdbFile))
506       {
507         addSequence(pe, seq);
508       }
509     }
510   }
511
512   @Override
513   public abstract void highlightAtoms(List<AtomSpec> atoms);
514
515   protected boolean isNucleotide()
516   {
517     return this.nucleotide;
518   }
519
520   /**
521    * Returns a readable description of all mappings for the wrapped pdbfile to
522    * any mapped sequences
523    * 
524    * @param pdbfile
525    * @param seqs
526    * @return
527    */
528   public String printMappings()
529   {
530     if (pdbEntry == null)
531     {
532       return "";
533     }
534     StringBuilder sb = new StringBuilder(128);
535     for (int pdbe = 0; pdbe < getPdbCount(); pdbe++)
536     {
537       String pdbfile = getPdbEntry(pdbe).getFile();
538       List<SequenceI> seqs = Arrays.asList(getSequence()[pdbe]);
539       sb.append(getSsm().printMappings(pdbfile, seqs));
540     }
541     return sb.toString();
542   }
543
544   /**
545    * Returns the mapped structure position for a given aligned column of a given
546    * sequence, or -1 if the column is gapped, beyond the end of the sequence, or
547    * not mapped to structure.
548    * 
549    * @param seq
550    * @param alignedPos
551    * @param mapping
552    * @return
553    */
554   protected int getMappedPosition(SequenceI seq, int alignedPos,
555           StructureMapping mapping)
556   {
557     if (alignedPos >= seq.getLength())
558     {
559       return -1;
560     }
561
562     if (Comparison.isGap(seq.getCharAt(alignedPos)))
563     {
564       return -1;
565     }
566     int seqPos = seq.findPosition(alignedPos);
567     int pos = mapping.getPDBResNum(seqPos);
568     return pos;
569   }
570
571   /**
572    * Helper method to identify residues that can participate in a structure
573    * superposition command. For each structure, identify a sequence in the
574    * alignment which is mapped to the structure. Identify non-gapped columns in
575    * the sequence which have a mapping to a residue in the structure. Returns
576    * the index of the first structure that has a mapping to the alignment.
577    * 
578    * @param alignment
579    *          the sequence alignment which is the basis of structure
580    *          superposition
581    * @param matched
582    *          a BitSet, where bit j is set to indicate that every structure has
583    *          a mapped residue present in column j (so the column can
584    *          participate in structure alignment)
585    * @param structures
586    *          an array of data beans corresponding to pdb file index
587    * @return
588    */
589   protected int findSuperposableResidues(AlignmentI alignment,
590           BitSet matched, SuperposeData[] structures)
591   {
592     int refStructure = -1;
593     String[] files = getStructureFiles();
594     if (files == null)
595     {
596       return -1;
597     }
598     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
599     {
600       StructureMapping[] mappings = getSsm().getMapping(files[pdbfnum]);
601       int lastPos = -1;
602
603       /*
604        * Find the first mapped sequence (if any) for this PDB entry which is in
605        * the alignment
606        */
607       final int seqCountForPdbFile = getSequence()[pdbfnum].length;
608       for (int s = 0; s < seqCountForPdbFile; s++)
609       {
610         for (StructureMapping mapping : mappings)
611         {
612           final SequenceI theSequence = getSequence()[pdbfnum][s];
613           if (mapping.getSequence() == theSequence
614                   && alignment.findIndex(theSequence) > -1)
615           {
616             if (refStructure < 0)
617             {
618               refStructure = pdbfnum;
619             }
620             for (int r = 0; r < alignment.getWidth(); r++)
621             {
622               if (!matched.get(r))
623               {
624                 continue;
625               }
626               int pos = getMappedPosition(theSequence, r, mapping);
627               if (pos < 1 || pos == lastPos)
628               {
629                 matched.clear(r);
630                 continue;
631               }
632               lastPos = pos;
633               structures[pdbfnum].pdbResNo[r] = pos;
634             }
635             String chain = mapping.getChain();
636             if (chain != null && chain.trim().length() > 0)
637             {
638               structures[pdbfnum].chain = chain;
639             }
640             structures[pdbfnum].pdbId = mapping.getPdbId();
641             structures[pdbfnum].isRna = theSequence.getRNA() != null;
642
643             /*
644              * move on to next pdb file (ignore sequences for other chains
645              * for the same structure)
646              */
647             s = seqCountForPdbFile;
648             break;
649           }
650         }
651       }
652     }
653     return refStructure;
654   }
655
656   /**
657    * Returns true if the structure viewer has loaded all of the files of
658    * interest (identified by the file mapping having been set up), or false if
659    * any are still not loaded after a timeout interval.
660    * 
661    * @param files
662    */
663   protected boolean waitForFileLoad(String[] files)
664   {
665     /*
666      * give up after 10 secs plus 1 sec per file
667      */
668     long starttime = System.currentTimeMillis();
669     long endTime = 10000 + 1000 * files.length + starttime;
670     String notLoaded = null;
671
672     boolean waiting = true;
673     while (waiting && System.currentTimeMillis() < endTime)
674     {
675       waiting = false;
676       for (String file : files)
677       {
678         notLoaded = file;
679         if (file == null)
680         {
681           continue;
682         }
683         try
684         {
685           StructureMapping[] sm = getSsm().getMapping(file);
686           if (sm == null || sm.length == 0)
687           {
688             waiting = true;
689           }
690         } catch (Throwable x)
691         {
692           waiting = true;
693         }
694       }
695     }
696
697     if (waiting)
698     {
699       System.err.println(
700               "Timed out waiting for structure viewer to load file "
701                       + notLoaded);
702       return false;
703     }
704     return true;
705   }
706
707   @Override
708   public boolean isListeningFor(SequenceI seq)
709   {
710     if (sequence != null)
711     {
712       for (SequenceI[] seqs : sequence)
713       {
714         if (seqs != null)
715         {
716           for (SequenceI s : seqs)
717           {
718             if (s == seq || (s.getDatasetSequence() != null
719                     && s.getDatasetSequence() == seq.getDatasetSequence()))
720             {
721               return true;
722             }
723           }
724         }
725       }
726     }
727     return false;
728   }
729
730   public boolean isFinishedInit()
731   {
732     return finishedInit;
733   }
734
735   public void setFinishedInit(boolean fi)
736   {
737     this.finishedInit = fi;
738   }
739
740   /**
741    * Returns a list of chains mapped in this viewer.
742    * 
743    * @return
744    */
745   public abstract List<String> getChainNames();
746
747   /**
748    * Returns the Jalview panel hosting the structure viewer (if any)
749    * 
750    * @return
751    */
752   public JalviewStructureDisplayI getViewer()
753   {
754     return viewer;
755   }
756
757   public void setViewer(JalviewStructureDisplayI v)
758   {
759     viewer = v;
760   }
761
762   public abstract void setJalviewColourScheme(ColourSchemeI cs);
763
764   /**
765    * Constructs and sends a command to align structures against a reference
766    * structure, based on one or more sequence alignments. May optionally return
767    * an error or warning message for the alignment command.
768    * 
769    * @param alignments
770    *          an array of alignments to process
771    * @param structureIndices
772    *          an array of corresponding reference structures (index into pdb
773    *          file array); if a negative value is passed, the first PDB file
774    *          mapped to an alignment sequence is used as the reference for
775    *          superposition
776    * @param hiddenCols
777    *          an array of corresponding hidden columns for each alignment
778    * @return
779    */
780   public abstract String superposeStructures(AlignmentI[] alignments,
781           int[] structureIndices, HiddenColumns[] hiddenCols);
782
783   public abstract void setBackgroundColour(Color col);
784
785   protected abstract StructureMappingcommandSet[] getColourBySequenceCommands(
786           String[] files, SequenceRenderer sr, AlignmentViewPanel avp);
787
788   /**
789    * returns the current sequenceRenderer that should be used to colour the
790    * structures
791    * 
792    * @param alignment
793    * 
794    * @return
795    */
796   public abstract SequenceRenderer getSequenceRenderer(
797           AlignmentViewPanel alignment);
798
799   protected abstract void colourBySequence(
800           StructureMappingcommandSet[] colourBySequenceCommands);
801
802   public abstract void colourByChain();
803
804   public abstract void colourByCharge();
805
806   /**
807    * colour any structures associated with sequences in the given alignment
808    * using the getFeatureRenderer() and getSequenceRenderer() renderers but only
809    * if colourBySequence is enabled.
810    */
811   public void colourBySequence(AlignmentViewPanel alignmentv)
812   {
813     if (!colourBySequence || !isLoadingFinished())
814     {
815       return;
816     }
817     if (getSsm() == null)
818     {
819       return;
820     }
821     String[] files = getStructureFiles();
822
823     SequenceRenderer sr = getSequenceRenderer(alignmentv);
824
825     StructureMappingcommandSet[] colourBySequenceCommands = getColourBySequenceCommands(
826             files, sr, alignmentv);
827     colourBySequence(colourBySequenceCommands);
828   }
829
830   public boolean hasFileLoadingError()
831   {
832     return fileLoadingError != null && fileLoadingError.length() > 0;
833   }
834
835   /**
836    * Returns the FeatureRenderer for the given alignment view, or null if
837    * feature display is turned off in the view.
838    * 
839    * @param avp
840    * @return
841    */
842   public FeatureRenderer getFeatureRenderer(AlignmentViewPanel avp)
843   {
844     AlignmentViewPanel ap = (avp == null) ? getViewer().getAlignmentPanel()
845             : avp;
846     return ap.getAlignViewport().isShowSequenceFeatures()
847             ? ap.getFeatureRenderer()
848             : null;
849   }
850 }