d92c78a841182343b77f7d201ef6833f520a2adc
[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 java.awt.Color;
24 import java.io.File;
25 import java.io.IOException;
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.BitSet;
29 import java.util.HashMap;
30 import java.util.LinkedHashMap;
31 import java.util.List;
32 import java.util.Map;
33
34 import javax.swing.SwingUtilities;
35
36 import jalview.api.AlignViewportI;
37 import jalview.api.AlignmentViewPanel;
38 import jalview.api.FeatureRenderer;
39 import jalview.api.SequenceRenderer;
40 import jalview.api.StructureSelectionManagerProvider;
41 import jalview.api.structures.JalviewStructureDisplayI;
42 import jalview.bin.Cache;
43 import jalview.datamodel.AlignmentI;
44 import jalview.datamodel.HiddenColumns;
45 import jalview.datamodel.MappedFeatures;
46 import jalview.datamodel.PDBEntry;
47 import jalview.datamodel.SequenceFeature;
48 import jalview.datamodel.SequenceI;
49 import jalview.ext.rbvi.chimera.JalviewChimeraBinding;
50 import jalview.gui.AlignmentPanel;
51 import jalview.gui.Desktop;
52 import jalview.gui.StructureViewer.ViewerType;
53 import jalview.io.DataSourceType;
54 import jalview.io.StructureFile;
55 import jalview.renderer.seqfeatures.FeatureColourFinder;
56 import jalview.schemes.ColourSchemeI;
57 import jalview.schemes.ResidueProperties;
58 import jalview.structure.AtomSpec;
59 import jalview.structure.AtomSpecModel;
60 import jalview.structure.StructureCommand;
61 import jalview.structure.StructureCommandI;
62 import jalview.structure.StructureCommandsI;
63 import jalview.structure.StructureListener;
64 import jalview.structure.StructureMapping;
65 import jalview.structure.StructureSelectionManager;
66 import jalview.util.Comparison;
67 import jalview.util.MessageManager;
68
69 /**
70  * 
71  * A base class to hold common function for protein structure model binding.
72  * Initial version created by refactoring JMol and Chimera binding models, but
73  * other structure viewers could in principle be accommodated in future.
74  * 
75  * @author gmcarstairs
76  *
77  */
78 public abstract class AAStructureBindingModel
79         extends SequenceStructureBindingModel
80         implements StructureListener, StructureSelectionManagerProvider
81 {
82   /**
83    * Data bean class to simplify parameterisation in superposeStructures
84    */
85   public static class SuperposeData
86   {
87     public String filename;
88
89     public String pdbId;
90
91     public String chain = "";
92
93     public boolean isRna;
94
95     /*
96      * The pdb residue number (if any) mapped to columns of the alignment
97      */
98     public int[] pdbResNo; // or use SparseIntArray?
99
100     public String modelId;
101
102     /**
103      * Constructor
104      * 
105      * @param width
106      *          width of alignment (number of columns that may potentially
107      *          participate in superposition)
108      * @param model
109      *          structure viewer model number
110      */
111     public SuperposeData(int width, String model)
112     {
113       pdbResNo = new int[width];
114       modelId = model;
115     }
116   }
117
118   private static final int MIN_POS_TO_SUPERPOSE = 4;
119
120   private static final String COLOURING_STRUCTURES = MessageManager
121           .getString("status.colouring_structures");
122
123   /*
124    * the Jalview panel through which the user interacts
125    * with the structure viewer
126    */
127   private JalviewStructureDisplayI viewer;
128
129   /*
130    * helper that generates command syntax
131    */
132   private StructureCommandsI commandGenerator;
133
134   private StructureSelectionManager ssm;
135
136   /*
137    * modelled chains, formatted as "pdbid:chainCode"
138    */
139   private List<String> chainNames;
140
141   /*
142    * lookup of pdb file name by key "pdbid:chainCode"
143    */
144   private Map<String, String> chainFile;
145
146   /*
147    * distinct PDB entries (pdb files) associated
148    * with sequences
149    */
150   private PDBEntry[] pdbEntry;
151
152   /*
153    * sequences mapped to each pdbentry
154    */
155   private SequenceI[][] sequence;
156
157   /*
158    * array of target chains for sequences - tied to pdbentry and sequence[]
159    */
160   private String[][] chains;
161
162   /*
163    * datasource protocol for access to PDBEntrylatest
164    */
165   DataSourceType protocol = null;
166
167   protected boolean colourBySequence = true;
168
169   private boolean nucleotide;
170
171   private boolean finishedInit = false;
172
173   /**
174    * current set of model filenames loaded in the viewer
175    */
176   protected String[] modelFileNames = null;
177
178   public String fileLoadingError;
179
180   protected Thread externalViewerMonitor;
181
182   /**
183    * Constructor
184    * 
185    * @param ssm
186    * @param seqs
187    */
188   public AAStructureBindingModel(StructureSelectionManager ssm,
189           SequenceI[][] seqs)
190   {
191     this.ssm = ssm;
192     this.sequence = seqs;
193     chainNames = new ArrayList<>();
194     chainFile = new HashMap<>();
195   }
196
197   /**
198    * Constructor
199    * 
200    * @param ssm
201    * @param pdbentry
202    * @param sequenceIs
203    * @param protocol
204    */
205   public AAStructureBindingModel(StructureSelectionManager ssm,
206           PDBEntry[] pdbentry, SequenceI[][] sequenceIs,
207           DataSourceType protocol)
208   {
209     this(ssm, sequenceIs);
210     this.nucleotide = Comparison.isNucleotide(sequenceIs);
211     this.pdbEntry = pdbentry;
212     this.protocol = protocol;
213     resolveChains();
214   }
215
216   private boolean resolveChains()
217   {
218     /**
219      * final count of chain mappings discovered
220      */
221     int chainmaps = 0;
222     // JBPNote: JAL-2693 - this should be a list of chain mappings per
223     // [pdbentry][sequence]
224     String[][] newchains = new String[pdbEntry.length][];
225     int pe = 0;
226     for (PDBEntry pdb : pdbEntry)
227     {
228       SequenceI[] seqsForPdb = sequence[pe];
229       if (seqsForPdb != null)
230       {
231         newchains[pe] = new String[seqsForPdb.length];
232         int se = 0;
233         for (SequenceI asq : seqsForPdb)
234         {
235           String chain = (chains != null && chains[pe] != null)
236                   ? chains[pe][se]
237                   : null;
238           SequenceI sq = (asq.getDatasetSequence() == null) ? asq
239                   : asq.getDatasetSequence();
240           if (sq.getAllPDBEntries() != null)
241           {
242             for (PDBEntry pdbentry : sq.getAllPDBEntries())
243             {
244               if (pdb.getFile() != null && pdbentry.getFile() != null
245                       && pdb.getFile().equals(pdbentry.getFile()))
246               {
247                 String chaincode = pdbentry.getChainCode();
248                 if (chaincode != null && chaincode.length() > 0)
249                 {
250                   chain = chaincode;
251                   chainmaps++;
252                   break;
253                 }
254               }
255             }
256           }
257           newchains[pe][se] = chain;
258           se++;
259         }
260         pe++;
261       }
262     }
263
264     chains = newchains;
265     return chainmaps > 0;
266   }
267
268   public StructureSelectionManager getSsm()
269   {
270     return ssm;
271   }
272
273   /**
274    * Returns the i'th PDBEntry (or null)
275    * 
276    * @param i
277    * @return
278    */
279   public PDBEntry getPdbEntry(int i)
280   {
281     return (pdbEntry != null && pdbEntry.length > i) ? pdbEntry[i] : null;
282   }
283
284   /**
285    * Answers true if this binding includes the given PDB id, else false
286    * 
287    * @param pdbId
288    * @return
289    */
290   public boolean hasPdbId(String pdbId)
291   {
292     if (pdbEntry != null)
293     {
294       for (PDBEntry pdb : pdbEntry)
295       {
296         if (pdb.getId().equals(pdbId))
297         {
298           return true;
299         }
300       }
301     }
302     return false;
303   }
304
305   /**
306    * Returns the number of modelled PDB file entries.
307    * 
308    * @return
309    */
310   public int getPdbCount()
311   {
312     return pdbEntry == null ? 0 : pdbEntry.length;
313   }
314
315   public SequenceI[][] getSequence()
316   {
317     return sequence;
318   }
319
320   public String[][] getChains()
321   {
322     return chains;
323   }
324
325   public DataSourceType getProtocol()
326   {
327     return protocol;
328   }
329
330   // TODO may remove this if calling methods can be pulled up here
331   protected void setPdbentry(PDBEntry[] pdbentry)
332   {
333     this.pdbEntry = pdbentry;
334   }
335
336   protected void setSequence(SequenceI[][] sequence)
337   {
338     this.sequence = sequence;
339   }
340
341   protected void setChains(String[][] chains)
342   {
343     this.chains = chains;
344   }
345
346   /**
347    * Construct a title string for the viewer window based on the data Jalview
348    * knows about
349    * 
350    * @param viewerName
351    *          TODO
352    * @param verbose
353    * 
354    * @return
355    */
356   public String getViewerTitle(String viewerName, boolean verbose)
357   {
358     if (getSequence() == null || getSequence().length < 1
359             || getPdbCount() < 1 || getSequence()[0].length < 1)
360     {
361       return ("Jalview " + viewerName + " Window");
362     }
363     // TODO: give a more informative title when multiple structures are
364     // displayed.
365     StringBuilder title = new StringBuilder(64);
366     final PDBEntry pdbe = getPdbEntry(0);
367     title.append(viewerName + " view for " + getSequence()[0][0].getName()
368             + ":" + pdbe.getId());
369
370     if (verbose)
371     {
372       String method = (String) pdbe.getProperty("method");
373       if (method != null)
374       {
375         title.append(" Method: ").append(method);
376       }
377       String chain = (String) pdbe.getProperty("chains");
378       if (chain != null)
379       {
380         title.append(" Chain:").append(chain);
381       }
382     }
383     return title.toString();
384   }
385
386   /**
387    * Called by after closeViewer is called, to release any resources and
388    * references so they can be garbage collected. Override if needed.
389    */
390   protected void releaseUIResources()
391   {
392   }
393
394   @Override
395   public void releaseReferences(Object svl)
396   {
397   }
398
399   public boolean isColourBySequence()
400   {
401     return colourBySequence;
402   }
403
404   /**
405    * Called when the binding thinks the UI needs to be refreshed after a
406    * structure viewer state change. This could be because structures were
407    * loaded, or because an error has occurred. Default does nothing, override as
408    * required.
409    */
410   public void refreshGUI()
411   {
412   }
413
414   /**
415    * Instruct the Jalview binding to update the pdbentries vector if necessary
416    * prior to matching the jmol view's contents to the list of structure files
417    * Jalview knows about. By default does nothing, override as required.
418    */
419   public void refreshPdbEntries()
420   {
421   }
422
423   public void setColourBySequence(boolean colourBySequence)
424   {
425     this.colourBySequence = colourBySequence;
426   }
427
428   protected void addSequenceAndChain(int pe, SequenceI[] seq,
429           String[] tchain)
430   {
431     if (pe < 0 || pe >= getPdbCount())
432     {
433       throw new Error(MessageManager.formatMessage(
434               "error.implementation_error_no_pdbentry_from_index",
435               new Object[]
436               { Integer.valueOf(pe).toString() }));
437     }
438     final String nullChain = "TheNullChain";
439     List<SequenceI> s = new ArrayList<>();
440     List<String> c = new ArrayList<>();
441     if (getChains() == null)
442     {
443       setChains(new String[getPdbCount()][]);
444     }
445     if (getSequence()[pe] != null)
446     {
447       for (int i = 0; i < getSequence()[pe].length; i++)
448       {
449         s.add(getSequence()[pe][i]);
450         if (getChains()[pe] != null)
451         {
452           if (i < getChains()[pe].length)
453           {
454             c.add(getChains()[pe][i]);
455           }
456           else
457           {
458             c.add(nullChain);
459           }
460         }
461         else
462         {
463           if (tchain != null && tchain.length > 0)
464           {
465             c.add(nullChain);
466           }
467         }
468       }
469     }
470     for (int i = 0; i < seq.length; i++)
471     {
472       if (!s.contains(seq[i]))
473       {
474         s.add(seq[i]);
475         if (tchain != null && i < tchain.length)
476         {
477           c.add(tchain[i] == null ? nullChain : tchain[i]);
478         }
479       }
480     }
481     SequenceI[] tmp = s.toArray(new SequenceI[s.size()]);
482     getSequence()[pe] = tmp;
483     if (c.size() > 0)
484     {
485       String[] tch = c.toArray(new String[c.size()]);
486       for (int i = 0; i < tch.length; i++)
487       {
488         if (tch[i] == nullChain)
489         {
490           tch[i] = null;
491         }
492       }
493       getChains()[pe] = tch;
494     }
495     else
496     {
497       getChains()[pe] = null;
498     }
499   }
500
501   /**
502    * add structures and any known sequence associations
503    * 
504    * @returns the pdb entries added to the current set.
505    */
506   public synchronized PDBEntry[] addSequenceAndChain(PDBEntry[] pdbe,
507           SequenceI[][] seq, String[][] chns)
508   {
509     List<PDBEntry> v = new ArrayList<>();
510     List<int[]> rtn = new ArrayList<>();
511     for (int i = 0; i < getPdbCount(); i++)
512     {
513       v.add(getPdbEntry(i));
514     }
515     for (int i = 0; i < pdbe.length; i++)
516     {
517       int r = v.indexOf(pdbe[i]);
518       if (r == -1 || r >= getPdbCount())
519       {
520         rtn.add(new int[] { v.size(), i });
521         v.add(pdbe[i]);
522       }
523       else
524       {
525         // just make sure the sequence/chain entries are all up to date
526         addSequenceAndChain(r, seq[i], chns[i]);
527       }
528     }
529     pdbe = v.toArray(new PDBEntry[v.size()]);
530     setPdbentry(pdbe);
531     if (rtn.size() > 0)
532     {
533       // expand the tied sequence[] and string[] arrays
534       SequenceI[][] sqs = new SequenceI[getPdbCount()][];
535       String[][] sch = new String[getPdbCount()][];
536       System.arraycopy(getSequence(), 0, sqs, 0, getSequence().length);
537       System.arraycopy(getChains(), 0, sch, 0, this.getChains().length);
538       setSequence(sqs);
539       setChains(sch);
540       pdbe = new PDBEntry[rtn.size()];
541       for (int r = 0; r < pdbe.length; r++)
542       {
543         int[] stri = (rtn.get(r));
544         // record the pdb file as a new addition
545         pdbe[r] = getPdbEntry(stri[0]);
546         // and add the new sequence/chain entries
547         addSequenceAndChain(stri[0], seq[stri[1]], chns[stri[1]]);
548       }
549     }
550     else
551     {
552       pdbe = null;
553     }
554     return pdbe;
555   }
556
557   /**
558    * Add sequences to the pe'th pdbentry's sequence set.
559    * 
560    * @param pe
561    * @param seq
562    */
563   public void addSequence(int pe, SequenceI[] seq)
564   {
565     addSequenceAndChain(pe, seq, null);
566   }
567
568   /**
569    * add the given sequences to the mapping scope for the given pdb file handle
570    * 
571    * @param pdbFile
572    *          - pdbFile identifier
573    * @param seq
574    *          - set of sequences it can be mapped to
575    */
576   public void addSequenceForStructFile(String pdbFile, SequenceI[] seq)
577   {
578     for (int pe = 0; pe < getPdbCount(); pe++)
579     {
580       if (getPdbEntry(pe).getFile().equals(pdbFile))
581       {
582         addSequence(pe, seq);
583       }
584     }
585   }
586
587   @Override
588   public abstract void highlightAtoms(List<AtomSpec> atoms);
589
590   protected boolean isNucleotide()
591   {
592     return this.nucleotide;
593   }
594
595   /**
596    * Returns a readable description of all mappings for the wrapped pdbfile to
597    * any mapped sequences
598    * 
599    * @param pdbfile
600    * @param seqs
601    * @return
602    */
603   public String printMappings()
604   {
605     if (pdbEntry == null)
606     {
607       return "";
608     }
609     StringBuilder sb = new StringBuilder(128);
610     for (int pdbe = 0; pdbe < getPdbCount(); pdbe++)
611     {
612       String pdbfile = getPdbEntry(pdbe).getFile();
613       List<SequenceI> seqs = Arrays.asList(getSequence()[pdbe]);
614       sb.append(getSsm().printMappings(pdbfile, seqs));
615     }
616     return sb.toString();
617   }
618
619   /**
620    * Returns the mapped structure position for a given aligned column of a given
621    * sequence, or -1 if the column is gapped, beyond the end of the sequence, or
622    * not mapped to structure.
623    * 
624    * @param seq
625    * @param alignedPos
626    * @param mapping
627    * @return
628    */
629   protected int getMappedPosition(SequenceI seq, int alignedPos,
630           StructureMapping mapping)
631   {
632     if (alignedPos >= seq.getLength())
633     {
634       return -1;
635     }
636
637     if (Comparison.isGap(seq.getCharAt(alignedPos)))
638     {
639       return -1;
640     }
641     int seqPos = seq.findPosition(alignedPos);
642     int pos = mapping.getPDBResNum(seqPos);
643     return pos;
644   }
645
646   /**
647    * Helper method to identify residues that can participate in a structure
648    * superposition command. For each structure, identify a sequence in the
649    * alignment which is mapped to the structure. Identify non-gapped columns in
650    * the sequence which have a mapping to a residue in the structure. Returns
651    * the index of the first structure that has a mapping to the alignment.
652    * 
653    * @param alignment
654    *          the sequence alignment which is the basis of structure
655    *          superposition
656    * @param matched
657    *          a BitSet, where bit j is set to indicate that every structure has
658    *          a mapped residue present in column j (so the column can
659    *          participate in structure alignment)
660    * @param structures
661    *          an array of data beans corresponding to pdb file index
662    * @return
663    */
664   protected int findSuperposableResidues(AlignmentI alignment,
665           BitSet matched,
666           AAStructureBindingModel.SuperposeData[] structures)
667   {
668     int refStructure = -1;
669     String[] files = getStructureFiles();
670     if (files == null)
671     {
672       return -1;
673     }
674     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
675     {
676       StructureMapping[] mappings = getSsm().getMapping(files[pdbfnum]);
677       int lastPos = -1;
678
679       /*
680        * Find the first mapped sequence (if any) for this PDB entry which is in
681        * the alignment
682        */
683       final int seqCountForPdbFile = getSequence()[pdbfnum].length;
684       for (int s = 0; s < seqCountForPdbFile; s++)
685       {
686         for (StructureMapping mapping : mappings)
687         {
688           final SequenceI theSequence = getSequence()[pdbfnum][s];
689           if (mapping.getSequence() == theSequence
690                   && alignment.findIndex(theSequence) > -1)
691           {
692             if (refStructure < 0)
693             {
694               refStructure = pdbfnum;
695             }
696             for (int r = 0; r < alignment.getWidth(); r++)
697             {
698               if (!matched.get(r))
699               {
700                 continue;
701               }
702               int pos = getMappedPosition(theSequence, r, mapping);
703               if (pos < 1 || pos == lastPos)
704               {
705                 matched.clear(r);
706                 continue;
707               }
708               lastPos = pos;
709               structures[pdbfnum].pdbResNo[r] = pos;
710             }
711             String chain = mapping.getChain();
712             if (chain != null && chain.trim().length() > 0)
713             {
714               structures[pdbfnum].chain = chain;
715             }
716             structures[pdbfnum].pdbId = mapping.getPdbId();
717             structures[pdbfnum].isRna = theSequence.getRNA() != null;
718
719             /*
720              * move on to next pdb file (ignore sequences for other chains
721              * for the same structure)
722              */
723             s = seqCountForPdbFile;
724             break; // fixme break out of two loops here!
725           }
726         }
727       }
728     }
729     return refStructure;
730   }
731
732   /**
733    * Returns true if the structure viewer has loaded all of the files of
734    * interest (identified by the file mapping having been set up), or false if
735    * any are still not loaded after a timeout interval.
736    * 
737    * @param files
738    */
739   protected boolean waitForFileLoad(String[] files)
740   {
741     /*
742      * give up after 10 secs plus 1 sec per file
743      */
744     long starttime = System.currentTimeMillis();
745     long endTime = 10000 + 1000 * files.length + starttime;
746     String notLoaded = null;
747
748     boolean waiting = true;
749     while (waiting && System.currentTimeMillis() < endTime)
750     {
751       waiting = false;
752       for (String file : files)
753       {
754         notLoaded = file;
755         if (file == null)
756         {
757           continue;
758         }
759         try
760         {
761           StructureMapping[] sm = getSsm().getMapping(file);
762           if (sm == null || sm.length == 0)
763           {
764             waiting = true;
765           }
766         } catch (Throwable x)
767         {
768           waiting = true;
769         }
770       }
771     }
772
773     if (waiting)
774     {
775       System.err.println(
776               "Timed out waiting for structure viewer to load file "
777                       + notLoaded);
778       return false;
779     }
780     return true;
781   }
782
783   @Override
784   public boolean isListeningFor(SequenceI seq)
785   {
786     if (sequence != null)
787     {
788       for (SequenceI[] seqs : sequence)
789       {
790         if (seqs != null)
791         {
792           for (SequenceI s : seqs)
793           {
794             if (s == seq || (s.getDatasetSequence() != null
795                     && s.getDatasetSequence() == seq.getDatasetSequence()))
796             {
797               return true;
798             }
799           }
800         }
801       }
802     }
803     return false;
804   }
805
806   public boolean isFinishedInit()
807   {
808     return finishedInit;
809   }
810
811   public void setFinishedInit(boolean fi)
812   {
813     this.finishedInit = fi;
814   }
815
816   /**
817    * Returns a list of chains mapped in this viewer, formatted as
818    * "pdbid:chainCode"
819    * 
820    * @return
821    */
822   public List<String> getChainNames()
823   {
824     return chainNames;
825   }
826
827   /**
828    * Returns the Jalview panel hosting the structure viewer (if any)
829    * 
830    * @return
831    */
832   public JalviewStructureDisplayI getViewer()
833   {
834     return viewer;
835   }
836
837   public void setViewer(JalviewStructureDisplayI v)
838   {
839     viewer = v;
840   }
841
842   /**
843    * Constructs and sends a command to align structures against a reference
844    * structure, based on one or more sequence alignments. May optionally return
845    * an error or warning message for the alignment command(s).
846    * 
847    * @param alignWith
848    *          an array of one or more alignment views to process
849    * @return
850    */
851   public String superposeStructures(List<AlignmentViewPanel> alignWith)
852   {
853     String error = "";
854     String[] files = getStructureFiles();
855
856     if (!waitForFileLoad(files))
857     {
858       return null;
859     }
860     refreshPdbEntries();
861
862     for (AlignmentViewPanel view : alignWith)
863     {
864       AlignmentI alignment = view.getAlignment();
865       HiddenColumns hiddenCols = alignment.getHiddenColumns();
866
867       /*
868        * 'matched' bit i will be set for visible alignment columns i where
869        * all sequences have a residue with a mapping to their PDB structure
870        */
871       BitSet matched = new BitSet();
872       final int width = alignment.getWidth();
873       for (int m = 0; m < width; m++)
874       {
875         if (hiddenCols == null || hiddenCols.isVisible(m))
876         {
877           matched.set(m);
878         }
879       }
880
881       AAStructureBindingModel.SuperposeData[] structures = new AAStructureBindingModel.SuperposeData[files.length];
882       for (int f = 0; f < files.length; f++)
883       {
884         structures[f] = new AAStructureBindingModel.SuperposeData(width,
885                 getModelIdForFile(files[f]));
886       }
887
888       /*
889        * Calculate the superposable alignment columns ('matched'), and the
890        * corresponding structure residue positions (structures.pdbResNo)
891        */
892       int refStructure = findSuperposableResidues(alignment, matched,
893               structures);
894
895       /*
896        * require at least 4 positions to be able to execute superposition
897        */
898       int nmatched = matched.cardinality();
899       if (nmatched < MIN_POS_TO_SUPERPOSE)
900       {
901         String msg = MessageManager
902                 .formatMessage("label.insufficient_residues", nmatched);
903         error += view.getViewName() + ": " + msg + "; ";
904         continue;
905       }
906
907       /*
908        * get a model of the superposable residues in the reference structure 
909        */
910       AtomSpecModel refAtoms = getAtomSpec(structures[refStructure],
911               matched);
912
913       /*
914        * Show all as backbone before doing superposition(s)
915        * (residues used for matching will be shown as ribbon)
916        */
917       // todo better way to ensure synchronous than setting getReply true!!
918       executeCommands(commandGenerator.showBackbone(), true, null);
919
920       /*
921        * superpose each (other) structure to the reference in turn
922        */
923       for (int i = 0; i < structures.length; i++)
924       {
925         if (i != refStructure)
926         {
927           AtomSpecModel atomSpec = getAtomSpec(structures[i], matched);
928           List<StructureCommandI> commands = commandGenerator
929                   .superposeStructures(refAtoms, atomSpec);
930           List<String> replies = executeCommands(commands, true, null);
931           for (String reply : replies)
932           {
933             // return this error (Chimera only) to the user
934             if (reply.toLowerCase().contains("unequal numbers of atoms"))
935             {
936               error += "; " + reply;
937             }
938           }
939         }
940       }
941     }
942
943     return error;
944   }
945
946   private AtomSpecModel getAtomSpec(
947           AAStructureBindingModel.SuperposeData superposeData,
948           BitSet matched)
949   {
950     AtomSpecModel model = new AtomSpecModel();
951     int nextColumnMatch = matched.nextSetBit(0);
952     while (nextColumnMatch != -1)
953     {
954       int pdbResNum = superposeData.pdbResNo[nextColumnMatch];
955       model.addRange(superposeData.modelId, pdbResNum, pdbResNum,
956               superposeData.chain);
957       nextColumnMatch = matched.nextSetBit(nextColumnMatch + 1);
958     }
959
960     return model;
961   }
962
963   /**
964    * returns the current sequenceRenderer that should be used to colour the
965    * structures
966    * 
967    * @param alignment
968    * 
969    * @return
970    */
971   public abstract SequenceRenderer getSequenceRenderer(
972           AlignmentViewPanel alignment);
973
974   /**
975    * Sends a command to the structure viewer to colour each chain with a
976    * distinct colour (to the extent supported by the viewer)
977    */
978   public void colourByChain()
979   {
980     colourBySequence = false;
981
982     // TODO: JAL-628 colour chains distinctly across all visible models
983
984     executeCommand(commandGenerator.colourByChain(), false,
985             COLOURING_STRUCTURES);
986   }
987
988   /**
989    * Sends a command to the structure viewer to colour each chain with a
990    * distinct colour (to the extent supported by the viewer)
991    */
992   public void colourByCharge()
993   {
994     colourBySequence = false;
995
996     executeCommands(commandGenerator.colourByCharge(), false,
997             COLOURING_STRUCTURES);
998   }
999
1000   /**
1001    * Sends a command to the structure to apply a colour scheme (defined in
1002    * Jalview but not necessarily applied to the alignment), which defines a
1003    * colour per residue letter. More complex schemes (e.g. that depend on
1004    * consensus) cannot be used here and are ignored.
1005    * 
1006    * @param cs
1007    */
1008   public void colourByJalviewColourScheme(ColourSchemeI cs)
1009   {
1010     colourBySequence = false;
1011
1012     if (cs == null || !cs.isSimple())
1013     {
1014       return;
1015     }
1016
1017     /*
1018      * build a map of {Residue3LetterCode, Color}
1019      */
1020     Map<String, Color> colours = new HashMap<>();
1021     List<String> residues = ResidueProperties.getResidues(isNucleotide(),
1022             false);
1023     for (String resName : residues)
1024     {
1025       char res = resName.length() == 3
1026               ? ResidueProperties.getSingleCharacterCode(resName)
1027               : resName.charAt(0);
1028       Color colour = cs.findColour(res, 0, null, null, 0f);
1029       colours.put(resName, colour);
1030     }
1031
1032     /*
1033      * pass to the command constructor, and send the command
1034      */
1035     List<StructureCommandI> cmd = commandGenerator
1036             .colourByResidues(colours);
1037     executeCommands(cmd, false, COLOURING_STRUCTURES);
1038   }
1039
1040   public void setBackgroundColour(Color col)
1041   {
1042     StructureCommandI cmd = commandGenerator.setBackgroundColour(col);
1043     executeCommand(cmd, false, null);
1044   }
1045
1046   /**
1047    * Sends one command to the structure viewer. If {@code getReply} is true, the
1048    * command is sent synchronously, otherwise in a deferred thread.
1049    * <p>
1050    * If a progress message is supplied, this is displayed before command
1051    * execution, and removed afterwards.
1052    * 
1053    * @param cmd
1054    * @param getReply
1055    * @param msg
1056    * @return
1057    */
1058   private List<String> executeCommand(StructureCommandI cmd,
1059           boolean getReply, String msg)
1060   {
1061     if (getReply)
1062     {
1063       /*
1064        * synchronous (same thread) execution so reply can be returned
1065        */
1066       final JalviewStructureDisplayI theViewer = getViewer();
1067       final long handle = msg == null ? 0 : theViewer.startProgressBar(msg);
1068       try
1069       {
1070         return executeCommand(cmd, getReply);
1071       } finally
1072       {
1073         if (msg != null)
1074         {
1075           theViewer.stopProgressBar(null, handle);
1076         }
1077       }
1078     }
1079     else
1080     {
1081       /*
1082        * asynchronous (new thread) execution if no reply needed
1083        */
1084       final JalviewStructureDisplayI theViewer = getViewer();
1085       final long handle = msg == null ? 0 : theViewer.startProgressBar(msg);
1086
1087       SwingUtilities.invokeLater(new Runnable()
1088       {
1089         @Override
1090         public void run()
1091         {
1092           try
1093           {
1094             executeCommand(cmd, false);
1095           } finally
1096           {
1097             if (msg != null)
1098             {
1099               theViewer.stopProgressBar(null, handle);
1100             }
1101           }
1102         }
1103       });
1104       return null;
1105     }
1106   }
1107
1108   /**
1109    * Execute one structure viewer command. If {@code getReply} is true, may
1110    * optionally return one or more reply messages, else returns null.
1111    * 
1112    * @param cmd
1113    * @param getReply
1114    */
1115   protected abstract List<String> executeCommand(StructureCommandI cmd,
1116           boolean getReply);
1117
1118   /**
1119    * Executes one or more structure viewer commands. If a progress message is
1120    * provided, it is shown first, and removed after all commands have been run.
1121    * 
1122    * @param commands
1123    * @param getReply
1124    * @param msg
1125    */
1126   protected List<String> executeCommands(List<StructureCommandI> commands,
1127           boolean getReply, String msg)
1128   {
1129     /*
1130      * show progress message if specified
1131      */
1132     final JalviewStructureDisplayI theViewer = getViewer();
1133     final long handle = msg == null ? 0 : theViewer.startProgressBar(msg);
1134
1135     List<String> response = getReply ? new ArrayList<>() : null;
1136     try
1137     {
1138       for (StructureCommandI cmd : commands)
1139       {
1140         List<String> replies = executeCommand(cmd, getReply, null);
1141         if (getReply && replies != null)
1142         {
1143           response.addAll(replies);
1144         }
1145       }
1146       return response;
1147     } finally
1148     {
1149       if (msg != null)
1150       {
1151         theViewer.stopProgressBar(null, handle);
1152       }
1153     }
1154   }
1155
1156   /**
1157    * Colours any structures associated with sequences in the given alignment as
1158    * coloured in the alignment view, provided colourBySequence is enabled
1159    */
1160   public void colourBySequence(AlignmentViewPanel alignmentv)
1161   {
1162     if (!colourBySequence || !isLoadingFinished() || getSsm() == null)
1163     {
1164       return;
1165     }
1166     Map<Object, AtomSpecModel> colourMap = buildColoursMap(ssm, sequence,
1167             alignmentv);
1168
1169     List<StructureCommandI> colourBySequenceCommands = commandGenerator
1170             .colourBySequence(colourMap);
1171     executeCommands(colourBySequenceCommands, false, null);
1172   }
1173
1174   /**
1175    * Centre the display in the structure viewer
1176    */
1177   public void focusView()
1178   {
1179     executeCommand(commandGenerator.focusView(), false, null);
1180   }
1181
1182   /**
1183    * Generates and executes a command to show only specified chains in the
1184    * structure viewer. The list of chains to show should contain entries
1185    * formatted as "pdbid:chaincode".
1186    * 
1187    * @param toShow
1188    */
1189   public void showChains(List<String> toShow)
1190   {
1191     // todo or reformat toShow list entries as modelNo:pdbId:chainCode ?
1192
1193     /*
1194      * Reformat the pdbid:chainCode values as modelNo:chainCode
1195      * since this is what is needed to construct the viewer command
1196      * todo: find a less messy way to do this
1197      */
1198     List<String> showThese = new ArrayList<>();
1199     for (String chainId : toShow)
1200     {
1201       String[] tokens = chainId.split("\\:");
1202       if (tokens.length == 2)
1203       {
1204         String pdbFile = getFileForChain(chainId);
1205         String model = getModelIdForFile(pdbFile);
1206         showThese.add(model + ":" + tokens[1]);
1207       }
1208     }
1209     executeCommands(commandGenerator.showChains(showThese), false, null);
1210   }
1211
1212   /**
1213    * Answers the structure viewer's model id given a PDB file name. Returns an
1214    * empty string if model id is not found.
1215    * 
1216    * @param chainId
1217    * @return
1218    */
1219   protected abstract String getModelIdForFile(String chainId);
1220
1221   public boolean hasFileLoadingError()
1222   {
1223     return fileLoadingError != null && fileLoadingError.length() > 0;
1224   }
1225
1226   /**
1227    * Returns the FeatureRenderer for the given alignment view, or null if
1228    * feature display is turned off in the view.
1229    * 
1230    * @param avp
1231    * @return
1232    */
1233   public FeatureRenderer getFeatureRenderer(AlignmentViewPanel avp)
1234   {
1235     AlignmentViewPanel ap = (avp == null) ? getViewer().getAlignmentPanel()
1236             : avp;
1237     if (ap == null)
1238     {
1239       return null;
1240     }
1241     return ap.getAlignViewport().isShowSequenceFeatures()
1242             ? ap.getFeatureRenderer()
1243             : null;
1244   }
1245
1246   protected void setStructureCommands(StructureCommandsI cmd)
1247   {
1248     commandGenerator = cmd;
1249   }
1250
1251   /**
1252    * Records association of one chain id (formatted as "pdbid:chainCode") with
1253    * the corresponding PDB file name
1254    * 
1255    * @param chainId
1256    * @param fileName
1257    */
1258   public void addChainFile(String chainId, String fileName)
1259   {
1260     chainFile.put(chainId, fileName);
1261   }
1262
1263   /**
1264    * Returns the PDB filename for the given chain id (formatted as
1265    * "pdbid:chainCode"), or null if not found
1266    * 
1267    * @param chainId
1268    * @return
1269    */
1270   protected String getFileForChain(String chainId)
1271   {
1272     return chainFile.get(chainId);
1273   }
1274
1275   @Override
1276   public void updateColours(Object source)
1277   {
1278     AlignmentViewPanel ap = (AlignmentViewPanel) source;
1279     // ignore events from panels not used to colour this view
1280     if (!getViewer().isUsedForColourBy(ap))
1281     {
1282       return;
1283     }
1284     if (!isLoadingFromArchive())
1285     {
1286       colourBySequence(ap);
1287     }
1288   }
1289
1290   public StructureCommandsI getCommandGenerator()
1291   {
1292     return commandGenerator;
1293   }
1294
1295   protected abstract ViewerType getViewerType();
1296
1297   /**
1298    * Send a structure viewer command asynchronously in a new thread. If the
1299    * progress message is not null, display this message while the command is
1300    * executing.
1301    * 
1302    * @param command
1303    * @param progressMsg
1304    */
1305   protected void sendAsynchronousCommand(StructureCommandI command,
1306           String progressMsg)
1307   {
1308     final JalviewStructureDisplayI theViewer = getViewer();
1309     final long handle = progressMsg == null ? 0
1310             : theViewer.startProgressBar(progressMsg);
1311     SwingUtilities.invokeLater(new Runnable()
1312     {
1313       @Override
1314       public void run()
1315       {
1316         try
1317         {
1318           executeCommand(command, false, null);
1319         } finally
1320         {
1321           if (progressMsg != null)
1322           {
1323             theViewer.stopProgressBar(null, handle);
1324           }
1325         }
1326       }
1327     });
1328
1329   }
1330
1331   /**
1332    * Builds a data structure which records mapped structure residues for each
1333    * colour. From this we can easily generate the viewer commands for colour by
1334    * sequence. Constructs and returns a map of {@code Color} to
1335    * {@code AtomSpecModel}, where the atomspec model holds
1336    * 
1337    * <pre>
1338    *   Model ids
1339    *     Chains
1340    *       Residue positions
1341    * </pre>
1342    * 
1343    * Ordering is by order of addition (for colours), natural ordering (for
1344    * models and chains)
1345    * 
1346    * @param ssm
1347    * @param sequence
1348    * @param viewPanel
1349    * @return
1350    */
1351   protected Map<Object, AtomSpecModel> buildColoursMap(
1352           StructureSelectionManager ssm, SequenceI[][] sequence,
1353           AlignmentViewPanel viewPanel)
1354   {
1355     String[] files = getStructureFiles();
1356     SequenceRenderer sr = getSequenceRenderer(viewPanel);
1357     FeatureRenderer fr = viewPanel.getFeatureRenderer();
1358     FeatureColourFinder finder = new FeatureColourFinder(fr);
1359     AlignViewportI viewport = viewPanel.getAlignViewport();
1360     HiddenColumns cs = viewport.getAlignment().getHiddenColumns();
1361     AlignmentI al = viewport.getAlignment();
1362     Map<Object, AtomSpecModel> colourMap = new LinkedHashMap<>();
1363     Color lastColour = null;
1364
1365     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
1366     {
1367       final String modelId = getModelIdForFile(files[pdbfnum]);
1368       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
1369
1370       if (mapping == null || mapping.length < 1)
1371       {
1372         continue;
1373       }
1374
1375       int startPos = -1, lastPos = -1;
1376       String lastChain = "";
1377       for (int s = 0; s < sequence[pdbfnum].length; s++)
1378       {
1379         for (int sp, m = 0; m < mapping.length; m++)
1380         {
1381           final SequenceI seq = sequence[pdbfnum][s];
1382           if (mapping[m].getSequence() == seq
1383                   && (sp = al.findIndex(seq)) > -1)
1384           {
1385             SequenceI asp = al.getSequenceAt(sp);
1386             for (int r = 0; r < asp.getLength(); r++)
1387             {
1388               // no mapping to gaps in sequence
1389               if (Comparison.isGap(asp.getCharAt(r)))
1390               {
1391                 continue;
1392               }
1393               int pos = mapping[m].getPDBResNum(asp.findPosition(r));
1394
1395               if (pos < 1 || pos == lastPos)
1396               {
1397                 continue;
1398               }
1399
1400               Color colour = sr.getResidueColour(seq, r, finder);
1401
1402               /*
1403                * darker colour for hidden regions
1404                */
1405               if (!cs.isVisible(r))
1406               {
1407                 colour = Color.GRAY;
1408               }
1409
1410               final String chain = mapping[m].getChain();
1411
1412               /*
1413                * Just keep incrementing the end position for this colour range
1414                * _unless_ colour, PDB model or chain has changed, or there is a
1415                * gap in the mapped residue sequence
1416                */
1417               final boolean newColour = !colour.equals(lastColour);
1418               final boolean nonContig = lastPos + 1 != pos;
1419               final boolean newChain = !chain.equals(lastChain);
1420               if (newColour || nonContig || newChain)
1421               {
1422                 if (startPos != -1)
1423                 {
1424                   addAtomSpecRange(colourMap, lastColour, modelId, startPos,
1425                           lastPos, lastChain);
1426                 }
1427                 startPos = pos;
1428               }
1429               lastColour = colour;
1430               lastPos = pos;
1431               lastChain = chain;
1432             }
1433             // final colour range
1434             if (lastColour != null)
1435             {
1436               addAtomSpecRange(colourMap, lastColour, modelId, startPos,
1437                       lastPos, lastChain);
1438             }
1439             // break;
1440           }
1441         }
1442       }
1443     }
1444     return colourMap;
1445   }
1446
1447   /**
1448    * todo better refactoring (map lookup or similar to get viewer structure id)
1449    * 
1450    * @param pdbfnum
1451    * @param file
1452    * @return
1453    */
1454   protected String getModelId(int pdbfnum, String file)
1455   {
1456     return String.valueOf(pdbfnum);
1457   }
1458
1459   /**
1460    * Saves chains, formatted as "pdbId:chainCode", and lookups from this to the
1461    * full PDB file path
1462    * 
1463    * @param pdb
1464    * @param file
1465    */
1466   public void stashFoundChains(StructureFile pdb, String file)
1467   {
1468     for (int i = 0; i < pdb.getChains().size(); i++)
1469     {
1470       String chid = pdb.getId() + ":" + pdb.getChains().elementAt(i).id;
1471       addChainFile(chid, file);
1472       getChainNames().add(chid);
1473     }
1474   }
1475
1476   /**
1477    * Helper method to add one contiguous range to the AtomSpec model for the
1478    * given value (creating the model if necessary). As used by Jalview,
1479    * {@code value} is
1480    * <ul>
1481    * <li>a colour, when building a 'colour structure by sequence' command</li>
1482    * <li>a feature value, when building a 'set Chimera attributes from features'
1483    * command</li>
1484    * </ul>
1485    * 
1486    * @param map
1487    * @param value
1488    * @param model
1489    * @param startPos
1490    * @param endPos
1491    * @param chain
1492    */
1493   public static final void addAtomSpecRange(Map<Object, AtomSpecModel> map,
1494           Object value, String model, int startPos, int endPos,
1495           String chain)
1496   {
1497     /*
1498      * Get/initialize map of data for the colour
1499      */
1500     AtomSpecModel atomSpec = map.get(value);
1501     if (atomSpec == null)
1502     {
1503       atomSpec = new AtomSpecModel();
1504       map.put(value, atomSpec);
1505     }
1506
1507     atomSpec.addRange(model, startPos, endPos, chain);
1508   }
1509
1510   /**
1511    * Returns the file extension (including '.' separator) to use for a saved
1512    * viewer session file. Default is to return null (not supported), override as
1513    * required.
1514    * 
1515    * @return
1516    */
1517   public String getSessionFileExtension()
1518   {
1519     return null;
1520   }
1521
1522   /**
1523    * If supported, saves the state of the structure viewer to a temporary file
1524    * and returns the file. Returns null and logs an error on any failure.
1525    * 
1526    * @return
1527    */
1528   public File saveSession()
1529   {
1530     String prefix = getViewerType().toString();
1531     String suffix = getSessionFileExtension();
1532     File f = null;
1533     try
1534     {
1535       f = File.createTempFile(prefix, suffix);
1536       saveSession(f);
1537     } catch (IOException e)
1538     {
1539       Cache.log.error(String.format("Error saving %s session: %s", prefix,
1540               e.toString()));
1541     }
1542
1543     return f;
1544   }
1545
1546   /**
1547    * Saves the structure viewer session to the given file
1548    * 
1549    * @param f
1550    */
1551   protected void saveSession(File f)
1552   {
1553     StructureCommandI cmd = commandGenerator.saveSession(f.getPath());
1554     if (cmd != null)
1555     {
1556       executeCommand(cmd, false);
1557     }
1558   }
1559
1560   /**
1561    * Returns true if the viewer is an external structure viewer for which the
1562    * process is still alive, else false (for Jmol, or an external viewer which
1563    * the user has independently closed)
1564    * 
1565    * @return
1566    */
1567   public boolean isViewerRunning()
1568   {
1569     return false;
1570   }
1571
1572   /**
1573    * Closes Jalview's structure viewer panel and releases associated resources.
1574    * If it is managing an external viewer program, and {@code forceClose} is
1575    * true, also asks that program to close.
1576    * 
1577    * @param forceClose
1578    */
1579   public void closeViewer(boolean forceClose)
1580   {
1581     getSsm().removeStructureViewerListener(this, this.getStructureFiles());
1582     releaseUIResources();
1583
1584     /*
1585      * end the thread that closes this panel if the external viewer closes
1586      */
1587     if (externalViewerMonitor != null)
1588     {
1589       externalViewerMonitor.interrupt();
1590       externalViewerMonitor = null;
1591     }
1592
1593     stopListening();
1594
1595     if (forceClose)
1596     {
1597       StructureCommandI cmd = getCommandGenerator().closeViewer();
1598       if (cmd != null)
1599       {
1600         executeCommand(cmd, false);
1601       }
1602     }
1603   }
1604
1605   /**
1606    * Returns the URL of a help page for the structure viewer, or null if none is
1607    * known
1608    * 
1609    * @return
1610    */
1611   public String getHelpURL()
1612   {
1613     return null;
1614   }
1615
1616   /**
1617    * <pre>
1618    * Helper method to build a map of 
1619    *   { featureType, { feature value, AtomSpecModel } }
1620    * </pre>
1621    * 
1622    * @param viewPanel
1623    * @return
1624    */
1625   protected Map<String, Map<Object, AtomSpecModel>> buildFeaturesMap(
1626           AlignmentViewPanel viewPanel)
1627   {
1628     Map<String, Map<Object, AtomSpecModel>> theMap = new LinkedHashMap<>();
1629     String[] files = getStructureFiles();
1630     if (files == null)
1631     {
1632       return theMap;
1633     }
1634
1635     FeatureRenderer fr = viewPanel.getFeatureRenderer();
1636     if (fr == null)
1637     {
1638       return theMap;
1639     }
1640
1641     AlignViewportI viewport = viewPanel.getAlignViewport();
1642     List<String> visibleFeatures = fr.getDisplayedFeatureTypes();
1643
1644     /*
1645      * if alignment is showing features from complement, we also transfer
1646      * these features to the corresponding mapped structure residues
1647      */
1648     boolean showLinkedFeatures = viewport.isShowComplementFeatures();
1649     List<String> complementFeatures = new ArrayList<>();
1650     FeatureRenderer complementRenderer = null;
1651     if (showLinkedFeatures)
1652     {
1653       AlignViewportI comp = fr.getViewport().getCodingComplement();
1654       if (comp != null)
1655       {
1656         complementRenderer = Desktop.getAlignFrameFor(comp)
1657                 .getFeatureRenderer();
1658         complementFeatures = complementRenderer.getDisplayedFeatureTypes();
1659       }
1660     }
1661     if (visibleFeatures.isEmpty() && complementFeatures.isEmpty())
1662     {
1663       return theMap;
1664     }
1665
1666     AlignmentI alignment = viewPanel.getAlignment();
1667     SequenceI[][] seqs = getSequence();
1668
1669     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
1670     {
1671       String modelId = getModelIdForFile(files[pdbfnum]);
1672       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
1673
1674       if (mapping == null || mapping.length < 1)
1675       {
1676         continue;
1677       }
1678
1679       for (int seqNo = 0; seqNo < seqs[pdbfnum].length; seqNo++)
1680       {
1681         for (int m = 0; m < mapping.length; m++)
1682         {
1683           final SequenceI seq = seqs[pdbfnum][seqNo];
1684           int sp = alignment.findIndex(seq);
1685           StructureMapping structureMapping = mapping[m];
1686           if (structureMapping.getSequence() == seq && sp > -1)
1687           {
1688             /*
1689              * found a sequence with a mapping to a structure;
1690              * now scan its features
1691              */
1692             if (!visibleFeatures.isEmpty())
1693             {
1694               scanSequenceFeatures(visibleFeatures, structureMapping, seq,
1695                       theMap, modelId);
1696             }
1697             if (showLinkedFeatures)
1698             {
1699               scanComplementFeatures(complementRenderer, structureMapping,
1700                       seq, theMap, modelId);
1701             }
1702           }
1703         }
1704       }
1705     }
1706     return theMap;
1707   }
1708
1709   /**
1710    * Ask the structure viewer to open a session file. Returns true if
1711    * successful, else false (or not supported).
1712    * 
1713    * @param filepath
1714    * @return
1715    */
1716   public boolean openSession(String filepath)
1717   {
1718     StructureCommandI cmd = getCommandGenerator().openSession(filepath);
1719     if (cmd == null)
1720     {
1721       return false;
1722     }
1723     executeCommand(cmd, true);
1724     // todo: test for failure - how?
1725     return true;
1726   }
1727
1728   /**
1729    * Scans visible features in mapped positions of the CDS/peptide complement,
1730    * and adds any found to the map of attribute values/structure positions
1731    * 
1732    * @param complementRenderer
1733    * @param structureMapping
1734    * @param seq
1735    * @param theMap
1736    * @param modelNumber
1737    */
1738   protected static void scanComplementFeatures(
1739           FeatureRenderer complementRenderer,
1740           StructureMapping structureMapping, SequenceI seq,
1741           Map<String, Map<Object, AtomSpecModel>> theMap,
1742           String modelNumber)
1743   {
1744     /*
1745      * for each sequence residue mapped to a structure position...
1746      */
1747     for (int seqPos : structureMapping.getMapping().keySet())
1748     {
1749       /*
1750        * find visible complementary features at mapped position(s)
1751        */
1752       MappedFeatures mf = complementRenderer
1753               .findComplementFeaturesAtResidue(seq, seqPos);
1754       if (mf != null)
1755       {
1756         for (SequenceFeature sf : mf.features)
1757         {
1758           String type = sf.getType();
1759
1760           /*
1761            * Don't copy features which originated from Chimera
1762            */
1763           if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
1764                   .equals(sf.getFeatureGroup()))
1765           {
1766             continue;
1767           }
1768
1769           /*
1770            * record feature 'value' (score/description/type) as at the
1771            * corresponding structure position
1772            */
1773           List<int[]> mappedRanges = structureMapping
1774                   .getPDBResNumRanges(seqPos, seqPos);
1775
1776           if (!mappedRanges.isEmpty())
1777           {
1778             String value = sf.getDescription();
1779             if (value == null || value.length() == 0)
1780             {
1781               value = type;
1782             }
1783             float score = sf.getScore();
1784             if (score != 0f && !Float.isNaN(score))
1785             {
1786               value = Float.toString(score);
1787             }
1788             Map<Object, AtomSpecModel> featureValues = theMap.get(type);
1789             if (featureValues == null)
1790             {
1791               featureValues = new HashMap<>();
1792               theMap.put(type, featureValues);
1793             }
1794             for (int[] range : mappedRanges)
1795             {
1796               addAtomSpecRange(featureValues, value, modelNumber, range[0],
1797                       range[1], structureMapping.getChain());
1798             }
1799           }
1800         }
1801       }
1802     }
1803   }
1804
1805   /**
1806    * Inspect features on the sequence; for each feature that is visible,
1807    * determine its mapped ranges in the structure (if any) according to the
1808    * given mapping, and add them to the map.
1809    * 
1810    * @param visibleFeatures
1811    * @param mapping
1812    * @param seq
1813    * @param theMap
1814    * @param modelId
1815    */
1816   protected static void scanSequenceFeatures(List<String> visibleFeatures,
1817           StructureMapping mapping, SequenceI seq,
1818           Map<String, Map<Object, AtomSpecModel>> theMap, String modelId)
1819   {
1820     List<SequenceFeature> sfs = seq.getFeatures().getPositionalFeatures(
1821             visibleFeatures.toArray(new String[visibleFeatures.size()]));
1822     for (SequenceFeature sf : sfs)
1823     {
1824       String type = sf.getType();
1825
1826       /*
1827        * Don't copy features which originated from Chimera
1828        */
1829       if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
1830               .equals(sf.getFeatureGroup()))
1831       {
1832         continue;
1833       }
1834
1835       List<int[]> mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(),
1836               sf.getEnd());
1837
1838       if (!mappedRanges.isEmpty())
1839       {
1840         String value = sf.getDescription();
1841         if (value == null || value.length() == 0)
1842         {
1843           value = type;
1844         }
1845         float score = sf.getScore();
1846         if (score != 0f && !Float.isNaN(score))
1847         {
1848           value = Float.toString(score);
1849         }
1850         Map<Object, AtomSpecModel> featureValues = theMap.get(type);
1851         if (featureValues == null)
1852         {
1853           featureValues = new HashMap<>();
1854           theMap.put(type, featureValues);
1855         }
1856         for (int[] range : mappedRanges)
1857         {
1858           addAtomSpecRange(featureValues, value, modelId, range[0],
1859                   range[1], mapping.getChain());
1860         }
1861       }
1862     }
1863   }
1864
1865   /**
1866    * Returns the number of structure files in the structure viewer and mapped to
1867    * Jalview. This may be zero if the files are still in the process of loading
1868    * in the viewer.
1869    * 
1870    * @return
1871    */
1872   public int getMappedStructureCount()
1873   {
1874     String[] files = getStructureFiles();
1875     return files == null ? 0 : files.length;
1876   }
1877
1878   /**
1879    * Starts a thread that waits for the external viewer program process to
1880    * finish, so that we can then close the associated resources. This avoids
1881    * leaving orphaned viewer panels in Jalview if the user closes the external
1882    * viewer.
1883    * 
1884    * @param p
1885    */
1886   protected void startExternalViewerMonitor(Process p)
1887   {
1888     externalViewerMonitor = new Thread(new Runnable()
1889     {
1890
1891       @Override
1892       public void run()
1893       {
1894         try
1895         {
1896           p.waitFor();
1897           JalviewStructureDisplayI display = getViewer();
1898           if (display != null)
1899           {
1900             display.closeViewer(false);
1901           }
1902         } catch (InterruptedException e)
1903         {
1904           // exit thread if Chimera Viewer is closed in Jalview
1905         }
1906       }
1907     });
1908     externalViewerMonitor.start();
1909   }
1910
1911   /**
1912    * If supported by the external structure viewer, sends it commands to notify
1913    * model or selection changes to the specified URL (where Jalview has started
1914    * a listener)
1915    * 
1916    * @param uri
1917    */
1918   protected void startListening(String uri)
1919   {
1920     List<StructureCommandI> commands = getCommandGenerator()
1921             .startNotifications(uri);
1922     if (commands != null)
1923     {
1924       executeCommands(commands, false, null);
1925     }
1926   }
1927
1928   /**
1929    * If supported by the external structure viewer, sends it commands to stop
1930    * notifying model or selection changes
1931    */
1932   protected void stopListening()
1933   {
1934     List<StructureCommandI> commands = getCommandGenerator()
1935             .stopNotifications();
1936     if (commands != null)
1937     {
1938       executeCommands(commands, false, null);
1939     }
1940   }
1941
1942   /**
1943    * If supported by the structure viewer, queries it for all residue attributes
1944    * with the given attribute name, and creates features on corresponding
1945    * residues of the alignment. Returns the number of features added.
1946    * 
1947    * @param attName
1948    * @param alignmentPanel
1949    * @return
1950    */
1951   public int copyStructureAttributesToFeatures(String attName,
1952           AlignmentPanel alignmentPanel)
1953   {
1954     StructureCommandI cmd = getCommandGenerator()
1955             .getResidueAttributes(attName);
1956     if (cmd == null)
1957     {
1958       return 0;
1959     }
1960     List<String> residueAttributes = executeCommand(cmd, true);
1961
1962     int featuresAdded = createFeaturesForAttributes(attName,
1963             residueAttributes);
1964     if (featuresAdded > 0)
1965     {
1966       alignmentPanel.getFeatureRenderer().featuresAdded();
1967     }
1968     return featuresAdded;
1969   }
1970
1971   /**
1972    * Parses {@code residueAttributes} and creates sequence features on any
1973    * mapped alignment residues. Returns the number of features created.
1974    * <p>
1975    * {@code residueAttributes} is the reply from the structure viewer to a
1976    * command to list any residue attributes for the given attribute name. Syntax
1977    * and parsing of this is viewer-specific.
1978    * 
1979    * @param attName
1980    * @param residueAttributes
1981    * @return
1982    */
1983   protected int createFeaturesForAttributes(String attName,
1984           List<String> residueAttributes)
1985   {
1986     return 0;
1987   }
1988 }