Merge branch 'features/JAL-2379pcaMemory' into develop
[jalview.git] / src / jalview / ext / rbvi / chimera / JalviewChimeraBinding.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.ext.rbvi.chimera;
22
23 import jalview.api.AlignmentViewPanel;
24 import jalview.api.FeatureRenderer;
25 import jalview.api.SequenceRenderer;
26 import jalview.api.structures.JalviewStructureDisplayI;
27 import jalview.bin.Cache;
28 import jalview.datamodel.AlignmentI;
29 import jalview.datamodel.ColumnSelection;
30 import jalview.datamodel.PDBEntry;
31 import jalview.datamodel.SequenceI;
32 import jalview.httpserver.AbstractRequestHandler;
33 import jalview.io.DataSourceType;
34 import jalview.schemes.ColourSchemeI;
35 import jalview.schemes.ResidueProperties;
36 import jalview.structure.AtomSpec;
37 import jalview.structure.StructureMappingcommandSet;
38 import jalview.structure.StructureSelectionManager;
39 import jalview.structures.models.AAStructureBindingModel;
40 import jalview.util.MessageManager;
41
42 import java.awt.Color;
43 import java.net.BindException;
44 import java.util.ArrayList;
45 import java.util.Hashtable;
46 import java.util.LinkedHashMap;
47 import java.util.List;
48 import java.util.Map;
49
50 import ext.edu.ucsf.rbvi.strucviz2.ChimeraManager;
51 import ext.edu.ucsf.rbvi.strucviz2.ChimeraModel;
52 import ext.edu.ucsf.rbvi.strucviz2.StructureManager;
53 import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType;
54
55 public abstract class JalviewChimeraBinding extends AAStructureBindingModel
56 {
57   // Chimera clause to exclude alternate locations in atom selection
58   private static final String NO_ALTLOCS = "&~@.B-Z&~@.2-9";
59
60   private static final String COLOURING_CHIMERA = MessageManager
61           .getString("status.colouring_chimera");
62
63   private static final boolean debug = false;
64
65   private static final String PHOSPHORUS = "P";
66
67   private static final String ALPHACARBON = "CA";
68
69   private List<String> chainNames = new ArrayList<String>();
70
71   private Hashtable<String, String> chainFile = new Hashtable<String, String>();
72   
73   /*
74    * Object through which we talk to Chimera
75    */
76   private ChimeraManager viewer;
77
78   /*
79    * Object which listens to Chimera notifications
80    */
81   private AbstractRequestHandler chimeraListener;
82
83   /*
84    * set if chimera state is being restored from some source - instructs binding
85    * not to apply default display style when structure set is updated for first
86    * time.
87    */
88   private boolean loadingFromArchive = false;
89
90   /*
91    * flag to indicate if the Chimera viewer should ignore sequence colouring
92    * events from the structure manager because the GUI is still setting up
93    */
94   private boolean loadingFinished = true;
95
96   /*
97    * Map of ChimeraModel objects keyed by PDB full local file name
98    */
99   private Map<String, List<ChimeraModel>> chimeraMaps = new LinkedHashMap<String, List<ChimeraModel>>();
100
101   /*
102    * the default or current model displayed if the model cannot be identified
103    * from the selection message
104    */
105   private int frameNo = 0;
106
107   private String lastCommand;
108
109   String lastHighlightCommand;
110
111   /*
112    * incremented every time a load notification is successfully handled -
113    * lightweight mechanism for other threads to detect when they can start
114    * referring to new structures.
115    */
116   private long loadNotifiesHandled = 0;
117
118   private Thread chimeraMonitor;
119
120   /**
121    * Open a PDB structure file in Chimera and set up mappings from Jalview.
122    * 
123    * We check if the PDB model id is already loaded in Chimera, if so don't
124    * reopen it. This is the case if Chimera has opened a saved session file.
125    * 
126    * @param pe
127    * @return
128    */
129   public boolean openFile(PDBEntry pe)
130   {
131     String file = pe.getFile();
132     try
133     {
134       List<ChimeraModel> modelsToMap = new ArrayList<ChimeraModel>();
135       List<ChimeraModel> oldList = viewer.getModelList();
136       boolean alreadyOpen = false;
137
138       /*
139        * If Chimera already has this model, don't reopen it, but do remap it.
140        */
141       for (ChimeraModel open : oldList)
142       {
143         if (open.getModelName().equals(pe.getId()))
144         {
145           alreadyOpen = true;
146           modelsToMap.add(open);
147         }
148       }
149
150       /*
151        * If Chimera doesn't yet have this model, ask it to open it, and retrieve
152        * the model name(s) added by Chimera.
153        */
154       if (!alreadyOpen)
155       {
156         viewer.openModel(file, pe.getId(), ModelType.PDB_MODEL);
157         List<ChimeraModel> newList = viewer.getModelList();
158         // JAL-1728 newList.removeAll(oldList) does not work
159         for (ChimeraModel cm : newList)
160         {
161           if (cm.getModelName().equals(pe.getId()))
162           {
163             modelsToMap.add(cm);
164           }
165         }
166       }
167
168       chimeraMaps.put(file, modelsToMap);
169
170       if (getSsm() != null)
171       {
172         getSsm().addStructureViewerListener(this);
173         // ssm.addSelectionListener(this);
174         FeatureRenderer fr = getFeatureRenderer(null);
175         if (fr != null)
176         {
177           fr.featuresAdded();
178         }
179         refreshGUI();
180       }
181       return true;
182     } catch (Exception q)
183     {
184       log("Exception when trying to open model " + file + "\n"
185               + q.toString());
186       q.printStackTrace();
187     }
188     return false;
189   }
190
191   /**
192    * Constructor
193    * 
194    * @param ssm
195    * @param pdbentry
196    * @param sequenceIs
197    * @param protocol
198    */
199   public JalviewChimeraBinding(StructureSelectionManager ssm,
200           PDBEntry[] pdbentry, SequenceI[][] sequenceIs, DataSourceType protocol)
201   {
202     super(ssm, pdbentry, sequenceIs, protocol);
203     viewer = new ChimeraManager(new StructureManager(true));
204   }
205
206   /**
207    * Starts a thread that waits for the Chimera process to finish, so that we
208    * can then close the associated resources. This avoids leaving orphaned
209    * Chimera viewer panels in Jalview if the user closes Chimera.
210    */
211   protected void startChimeraProcessMonitor()
212   {
213     final Process p = viewer.getChimeraProcess();
214     chimeraMonitor = new Thread(new Runnable()
215     {
216
217       @Override
218       public void run()
219       {
220         try
221         {
222           p.waitFor();
223           JalviewStructureDisplayI display = getViewer();
224           if (display != null)
225           {
226             display.closeViewer(false);
227           }
228         } catch (InterruptedException e)
229         {
230           // exit thread if Chimera Viewer is closed in Jalview
231         }
232       }
233     });
234     chimeraMonitor.start();
235   }
236
237   /**
238    * Start a dedicated HttpServer to listen for Chimera notifications, and tell
239    * it to start listening
240    */
241   public void startChimeraListener()
242   {
243     try
244     {
245       chimeraListener = new ChimeraListener(this);
246       viewer.startListening(chimeraListener.getUri());
247     } catch (BindException e)
248     {
249       System.err.println("Failed to start Chimera listener: "
250               + e.getMessage());
251     }
252   }
253
254   /**
255    * Tells Chimera to display only the specified chains
256    * 
257    * @param toshow
258    */
259   public void showChains(List<String> toshow)
260   {
261     /*
262      * Construct a chimera command like
263      * 
264      * ~display #*;~ribbon #*;ribbon :.A,:.B
265      */
266     StringBuilder cmd = new StringBuilder(64);
267     boolean first = true;
268     for (String chain : toshow)
269     {
270       int modelNumber = getModelNoForChain(chain);
271       String showChainCmd = modelNumber == -1 ? "" : modelNumber + ":."
272               + chain.split(":")[1];
273       if (!first)
274       {
275         cmd.append(",");
276       }
277       cmd.append(showChainCmd);
278       first = false;
279     }
280
281     /*
282      * could append ";focus" to this command to resize the display to fill the
283      * window, but it looks more helpful not to (easier to relate chains to the
284      * whole)
285      */
286     final String command = "~display #*; ~ribbon #*; ribbon :"
287             + cmd.toString();
288     sendChimeraCommand(command, false);
289   }
290
291   /**
292    * Close down the Jalview viewer and listener, and (optionally) the associated
293    * Chimera window.
294    */
295   public void closeViewer(boolean closeChimera)
296   {
297     getSsm().removeStructureViewerListener(this, this.getPdbFile());
298     if (closeChimera)
299     {
300       viewer.exitChimera();
301     }
302     if (this.chimeraListener != null)
303     {
304       chimeraListener.shutdown();
305       chimeraListener = null;
306     }
307     lastCommand = null;
308     viewer = null;
309
310     if (chimeraMonitor != null)
311     {
312       chimeraMonitor.interrupt();
313     }
314     releaseUIResources();
315   }
316
317   @Override
318   public void colourByChain()
319   {
320     colourBySequence = false;
321     sendAsynchronousCommand("rainbow chain", COLOURING_CHIMERA);
322   }
323
324   /**
325    * Constructs and sends a Chimera command to colour by charge
326    * <ul>
327    * <li>Aspartic acid and Glutamic acid (negative charge) red</li>
328    * <li>Lysine and Arginine (positive charge) blue</li>
329    * <li>Cysteine - yellow</li>
330    * <li>all others - white</li>
331    * </ul>
332    */
333   @Override
334   public void colourByCharge()
335   {
336     colourBySequence = false;
337     String command = "color white;color red ::ASP;color red ::GLU;color blue ::LYS;color blue ::ARG;color yellow ::CYS";
338     sendAsynchronousCommand(command, COLOURING_CHIMERA);
339   }
340
341   /**
342    * Construct and send a command to align structures against a reference
343    * structure, based on one or more sequence alignments
344    * 
345    * @param _alignment
346    *          an array of alignments to process
347    * @param _refStructure
348    *          an array of corresponding reference structures (index into pdb
349    *          file array); if a negative value is passed, the first PDB file
350    *          mapped to an alignment sequence is used as the reference for
351    *          superposition
352    * @param _hiddenCols
353    *          an array of corresponding hidden columns for each alignment
354    */
355   @Override
356   public void superposeStructures(AlignmentI[] _alignment,
357           int[] _refStructure, ColumnSelection[] _hiddenCols)
358   {
359     StringBuilder allComs = new StringBuilder(128);
360     String[] files = getPdbFile();
361
362     if (!waitForFileLoad(files))
363     {
364       return;
365     }
366
367     refreshPdbEntries();
368     StringBuilder selectioncom = new StringBuilder(256);
369     for (int a = 0; a < _alignment.length; a++)
370     {
371       int refStructure = _refStructure[a];
372       AlignmentI alignment = _alignment[a];
373       ColumnSelection hiddenCols = _hiddenCols[a];
374
375       if (refStructure >= files.length)
376       {
377         System.err.println("Ignoring invalid reference structure value "
378                 + refStructure);
379         refStructure = -1;
380       }
381
382       /*
383        * 'matched' array will hold 'true' for visible alignment columns where
384        * all sequences have a residue with a mapping to the PDB structure
385        */
386       boolean matched[] = new boolean[alignment.getWidth()];
387       for (int m = 0; m < matched.length; m++)
388       {
389         matched[m] = (hiddenCols != null) ? hiddenCols.isVisible(m) : true;
390       }
391
392       SuperposeData[] structures = new SuperposeData[files.length];
393       for (int f = 0; f < files.length; f++)
394       {
395         structures[f] = new SuperposeData(alignment.getWidth());
396       }
397
398       /*
399        * Calculate the superposable alignment columns ('matched'), and the
400        * corresponding structure residue positions (structures.pdbResNo)
401        */
402       int candidateRefStructure = findSuperposableResidues(alignment,
403               matched, structures);
404       if (refStructure < 0)
405       {
406         /*
407          * If no reference structure was specified, pick the first one that has
408          * a mapping in the alignment
409          */
410         refStructure = candidateRefStructure;
411       }
412
413       int nmatched = 0;
414       for (boolean b : matched)
415       {
416         if (b)
417         {
418           nmatched++;
419         }
420       }
421       if (nmatched < 4)
422       {
423         // TODO: bail out here because superposition illdefined?
424       }
425
426       /*
427        * Generate select statements to select regions to superimpose structures
428        */
429       String[] selcom = new String[files.length];
430       for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
431       {
432         String chainCd = "." + structures[pdbfnum].chain;
433         int lpos = -1;
434         boolean run = false;
435         StringBuilder molsel = new StringBuilder();
436         for (int r = 0; r < matched.length; r++)
437         {
438           if (matched[r])
439           {
440             int pdbResNum = structures[pdbfnum].pdbResNo[r];
441             if (lpos != pdbResNum - 1)
442             {
443               /*
444                * discontiguous - append last residue now
445                */
446               if (lpos != -1)
447               {
448                 molsel.append(String.valueOf(lpos));
449                 molsel.append(chainCd);
450                 molsel.append(",");
451               }
452               run = false;
453             }
454             else
455             {
456               /*
457                * extending a contiguous run
458                */
459               if (!run)
460               {
461                 /*
462                  * start the range selection
463                  */
464                 molsel.append(String.valueOf(lpos));
465                 molsel.append("-");
466               }
467               run = true;
468             }
469             lpos = pdbResNum;
470           }
471         }
472
473         /*
474          * and terminate final selection
475          */
476         if (lpos != -1)
477         {
478           molsel.append(String.valueOf(lpos));
479           molsel.append(chainCd);
480         }
481         if (molsel.length() > 1)
482         {
483           selcom[pdbfnum] = molsel.toString();
484           selectioncom.append("#").append(String.valueOf(pdbfnum))
485                   .append(":");
486           selectioncom.append(selcom[pdbfnum]);
487           selectioncom.append(" ");
488           if (pdbfnum < files.length - 1)
489           {
490             selectioncom.append("| ");
491           }
492         }
493         else
494         {
495           selcom[pdbfnum] = null;
496         }
497       }
498
499       StringBuilder command = new StringBuilder(256);
500       for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
501       {
502         if (pdbfnum == refStructure || selcom[pdbfnum] == null
503                 || selcom[refStructure] == null)
504         {
505           continue;
506         }
507         if (command.length() > 0)
508         {
509           command.append(";");
510         }
511
512         /*
513          * Form Chimera match command, from the 'new' structure to the
514          * 'reference' structure e.g. (50 residues, chain B/A, alphacarbons):
515          * 
516          * match #1:1-30.B,81-100.B@CA #0:21-40.A,61-90.A@CA
517          * 
518          * @see
519          * https://www.cgl.ucsf.edu/chimera/docs/UsersGuide/midas/match.html
520          */
521         command.append("match ").append(getModelSpec(pdbfnum)).append(":");
522         command.append(selcom[pdbfnum]);
523         command.append("@").append(
524                 structures[pdbfnum].isRna ? PHOSPHORUS : ALPHACARBON);
525         // JAL-1757 exclude alternate CA locations
526         command.append(NO_ALTLOCS);
527         command.append(" ").append(getModelSpec(refStructure)).append(":");
528         command.append(selcom[refStructure]);
529         command.append("@").append(
530                 structures[refStructure].isRna ? PHOSPHORUS : ALPHACARBON);
531         command.append(NO_ALTLOCS);
532       }
533       if (selectioncom.length() > 0)
534       {
535         if (debug)
536         {
537           System.out.println("Select regions:\n" + selectioncom.toString());
538           System.out.println("Superimpose command(s):\n"
539                   + command.toString());
540         }
541         allComs.append("~display all; chain @CA|P; ribbon ")
542                 .append(selectioncom.toString())
543                 .append(";" + command.toString());
544       }
545     }
546     if (selectioncom.length() > 0)
547     {
548       // TODO: visually distinguish regions that were superposed
549       if (selectioncom.substring(selectioncom.length() - 1).equals("|"))
550       {
551         selectioncom.setLength(selectioncom.length() - 1);
552       }
553       if (debug)
554       {
555         System.out.println("Select regions:\n" + selectioncom.toString());
556       }
557       allComs.append("; ~display all; chain @CA|P; ribbon ")
558               .append(selectioncom.toString()).append("; focus");
559       sendChimeraCommand(allComs.toString(), false);
560     }
561
562   }
563
564   /**
565    * Helper method to construct model spec in Chimera format:
566    * <ul>
567    * <li>#0 (#1 etc) for a PDB file with no sub-models</li>
568    * <li>#0.1 (#1.1 etc) for a PDB file with sub-models</li>
569    * <ul>
570    * Note for now we only ever choose the first of multiple models. This
571    * corresponds to the hard-coded Jmol equivalent (compare {1.1}). Refactor in
572    * future if there is a need to select specific sub-models.
573    * 
574    * @param pdbfnum
575    * @return
576    */
577   protected String getModelSpec(int pdbfnum)
578   {
579     if (pdbfnum < 0 || pdbfnum >= getPdbCount())
580     {
581       return "";
582     }
583
584     /*
585      * For now, the test for having sub-models is whether multiple Chimera
586      * models are mapped for the PDB file; the models are returned as a response
587      * to the Chimera command 'list models type molecule', see
588      * ChimeraManager.getModelList().
589      */
590     List<ChimeraModel> maps = chimeraMaps.get(getPdbFile()[pdbfnum]);
591     boolean hasSubModels = maps != null && maps.size() > 1;
592     return "#" + String.valueOf(pdbfnum) + (hasSubModels ? ".1" : "");
593   }
594
595   /**
596    * Launch Chimera, unless an instance linked to this object is already
597    * running. Returns true if Chimera is successfully launched, or already
598    * running, else false.
599    * 
600    * @return
601    */
602   public boolean launchChimera()
603   {
604     if (viewer.isChimeraLaunched())
605     {
606       return true;
607     }
608
609     boolean launched = viewer.launchChimera(StructureManager
610             .getChimeraPaths());
611     if (launched)
612     {
613       startChimeraProcessMonitor();
614     }
615     else
616     {
617       log("Failed to launch Chimera!");
618     }
619     return launched;
620   }
621
622   /**
623    * Answers true if the Chimera process is still running, false if ended or not
624    * started.
625    * 
626    * @return
627    */
628   public boolean isChimeraRunning()
629   {
630     return viewer.isChimeraLaunched();
631   }
632
633   /**
634    * Send a command to Chimera, and optionally log any responses.
635    * 
636    * @param command
637    * @param logResponse
638    */
639   public void sendChimeraCommand(final String command, boolean logResponse)
640   {
641     if (viewer == null)
642     {
643       // ? thread running after viewer shut down
644       return;
645     }
646     viewerCommandHistory(false);
647     if (lastCommand == null || !lastCommand.equals(command))
648     {
649       // trim command or it may never find a match in the replyLog!!
650       List<String> lastReply = viewer.sendChimeraCommand(command.trim(),
651               logResponse);
652       if (logResponse && debug)
653       {
654         log("Response from command ('" + command + "') was:\n" + lastReply);
655       }
656     }
657     viewerCommandHistory(true);
658     lastCommand = command;
659   }
660
661   /**
662    * Send a Chimera command asynchronously in a new thread. If the progress
663    * message is not null, display this message while the command is executing.
664    * 
665    * @param command
666    * @param progressMsg
667    */
668   protected abstract void sendAsynchronousCommand(String command,
669           String progressMsg);
670
671   /**
672    * Sends a set of colour commands to the structure viewer
673    * 
674    * @param colourBySequenceCommands
675    */
676   @Override
677   protected void colourBySequence(
678           StructureMappingcommandSet[] colourBySequenceCommands)
679   {
680     for (StructureMappingcommandSet cpdbbyseq : colourBySequenceCommands)
681     {
682       for (String command : cpdbbyseq.commands)
683       {
684         sendAsynchronousCommand(command, COLOURING_CHIMERA);
685       }
686     }
687   }
688
689   /**
690    * @param files
691    * @param sr
692    * @param fr
693    * @param alignment
694    * @return
695    */
696   @Override
697   protected StructureMappingcommandSet[] getColourBySequenceCommands(
698           String[] files, SequenceRenderer sr, FeatureRenderer fr,
699           AlignmentI alignment)
700   {
701     return ChimeraCommands.getColourBySequenceCommand(getSsm(), files,
702             getSequence(), sr, fr, alignment);
703   }
704
705   /**
706    * @param command
707    */
708   protected void executeWhenReady(String command)
709   {
710     waitForChimera();
711     sendChimeraCommand(command, false);
712     waitForChimera();
713   }
714
715   private void waitForChimera()
716   {
717     while (viewer != null && viewer.isBusy())
718     {
719       try
720       {
721         Thread.sleep(15);
722       } catch (InterruptedException q)
723       {
724       }
725     }
726   }
727
728   // End StructureListener
729   // //////////////////////////
730
731   /**
732    * instruct the Jalview binding to update the pdbentries vector if necessary
733    * prior to matching the viewer's contents to the list of structure files
734    * Jalview knows about.
735    */
736   public abstract void refreshPdbEntries();
737
738   /**
739    * map between index of model filename returned from getPdbFile and the first
740    * index of models from this file in the viewer. Note - this is not trimmed -
741    * use getPdbFile to get number of unique models.
742    */
743   private int _modelFileNameMap[];
744
745   // ////////////////////////////////
746   // /StructureListener
747   @Override
748   public synchronized String[] getPdbFile()
749   {
750     if (viewer == null)
751     {
752       return new String[0];
753     }
754
755     return chimeraMaps.keySet().toArray(
756             modelFileNames = new String[chimeraMaps.size()]);
757   }
758
759   /**
760    * Construct and send a command to highlight zero, one or more atoms. We do
761    * this by sending an "rlabel" command to show the residue label at that
762    * position.
763    */
764   @Override
765   public void highlightAtoms(List<AtomSpec> atoms)
766   {
767     if (atoms == null || atoms.size() == 0)
768     {
769       return;
770     }
771
772     StringBuilder cmd = new StringBuilder(128);
773     boolean first = true;
774     boolean found = false;
775
776     for (AtomSpec atom : atoms)
777     {
778       int pdbResNum = atom.getPdbResNum();
779       String chain = atom.getChain();
780       String pdbfile = atom.getPdbFile();
781       List<ChimeraModel> cms = chimeraMaps.get(pdbfile);
782       if (cms != null && !cms.isEmpty())
783       {
784         if (first)
785         {
786           cmd.append("rlabel #").append(cms.get(0).getModelNumber())
787                   .append(":");
788         }
789         else
790         {
791           cmd.append(",");
792         }
793         first = false;
794         cmd.append(pdbResNum);
795         if (!chain.equals(" "))
796         {
797           cmd.append(".").append(chain);
798         }
799         found = true;
800       }
801     }
802     String command = cmd.toString();
803
804     /*
805      * avoid repeated commands for the same residue
806      */
807     if (command.equals(lastHighlightCommand))
808     {
809       return;
810     }
811
812     /*
813      * unshow the label for the previous residue
814      */
815     if (lastHighlightCommand != null)
816     {
817       viewer.sendChimeraCommand("~" + lastHighlightCommand, false);
818     }
819     if (found)
820     {
821       viewer.sendChimeraCommand(command, false);
822     }
823     this.lastHighlightCommand = command;
824   }
825
826   /**
827    * Query Chimera for its current selection, and highlight it on the alignment
828    */
829   public void highlightChimeraSelection()
830   {
831     /*
832      * Ask Chimera for its current selection
833      */
834     List<String> selection = viewer.getSelectedResidueSpecs();
835
836     /*
837      * Parse model number, residue and chain for each selected position,
838      * formatted as #0:123.A or #1.2:87.B (#model.submodel:residue.chain)
839      */
840     List<AtomSpec> atomSpecs = new ArrayList<AtomSpec>();
841     for (String atomSpec : selection)
842     {
843       int colonPos = atomSpec.indexOf(":");
844       if (colonPos == -1)
845       {
846         continue; // malformed
847       }
848
849       int hashPos = atomSpec.indexOf("#");
850       String modelSubmodel = atomSpec.substring(hashPos + 1, colonPos);
851       int dotPos = modelSubmodel.indexOf(".");
852       int modelId = 0;
853       try
854       {
855         modelId = Integer.valueOf(dotPos == -1 ? modelSubmodel
856                 : modelSubmodel.substring(0, dotPos));
857       } catch (NumberFormatException e)
858       {
859         // ignore, default to model 0
860       }
861
862       String residueChain = atomSpec.substring(colonPos + 1);
863       dotPos = residueChain.indexOf(".");
864       int pdbResNum = Integer.parseInt(dotPos == -1 ? residueChain
865               : residueChain.substring(0, dotPos));
866
867       String chainId = dotPos == -1 ? "" : residueChain
868               .substring(dotPos + 1);
869
870       /*
871        * Work out the pdbfilename from the model number
872        */
873       String pdbfilename = modelFileNames[frameNo];
874       findfileloop: for (String pdbfile : this.chimeraMaps.keySet())
875       {
876         for (ChimeraModel cm : chimeraMaps.get(pdbfile))
877         {
878           if (cm.getModelNumber() == modelId)
879           {
880             pdbfilename = pdbfile;
881             break findfileloop;
882           }
883         }
884       }
885       atomSpecs.add(new AtomSpec(pdbfilename, chainId, pdbResNum, 0));
886     }
887
888     /*
889      * Broadcast the selection (which may be empty, if the user just cleared all
890      * selections)
891      */
892     getSsm().mouseOverStructure(atomSpecs);
893   }
894
895   private void log(String message)
896   {
897     System.err.println("## Chimera log: " + message);
898   }
899
900   private void viewerCommandHistory(boolean enable)
901   {
902     // log("(Not yet implemented) History "
903     // + ((debug || enable) ? "on" : "off"));
904   }
905
906   public long getLoadNotifiesHandled()
907   {
908     return loadNotifiesHandled;
909   }
910
911   @Override
912   public void setJalviewColourScheme(ColourSchemeI cs)
913   {
914     colourBySequence = false;
915
916     if (cs == null)
917     {
918       return;
919     }
920
921     // Chimera expects RBG values in the range 0-1
922     final double normalise = 255D;
923     viewerCommandHistory(false);
924     StringBuilder command = new StringBuilder(128);
925
926     List<String> residueSet = ResidueProperties.getResidues(isNucleotide(),
927             false);
928     for (String resName : residueSet)
929     {
930       char res = resName.length() == 3 ? ResidueProperties
931               .getSingleCharacterCode(resName) : resName.charAt(0);
932       Color col = cs.findColour(res, 0, null, null, 0f);
933       command.append("color " + col.getRed() / normalise + ","
934               + col.getGreen() / normalise + "," + col.getBlue()
935               / normalise + " ::" + resName + ";");
936     }
937
938     sendAsynchronousCommand(command.toString(), COLOURING_CHIMERA);
939     viewerCommandHistory(true);
940   }
941
942   /**
943    * called when the binding thinks the UI needs to be refreshed after a Chimera
944    * state change. this could be because structures were loaded, or because an
945    * error has occurred.
946    */
947   public abstract void refreshGUI();
948
949   @Override
950   public void setLoadingFromArchive(boolean loadingFromArchive)
951   {
952     this.loadingFromArchive = loadingFromArchive;
953   }
954
955   /**
956    * 
957    * @return true if Chimeral is still restoring state or loading is still going
958    *         on (see setFinsihedLoadingFromArchive)
959    */
960   @Override
961   public boolean isLoadingFromArchive()
962   {
963     return loadingFromArchive && !loadingFinished;
964   }
965
966   /**
967    * modify flag which controls if sequence colouring events are honoured by the
968    * binding. Should be true for normal operation
969    * 
970    * @param finishedLoading
971    */
972   @Override
973   public void setFinishedLoadingFromArchive(boolean finishedLoading)
974   {
975     loadingFinished = finishedLoading;
976   }
977
978   /**
979    * Send the Chimera 'background solid <color>" command.
980    * 
981    * @see https 
982    *      ://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/background
983    *      .html
984    * @param col
985    */
986   @Override
987   public void setBackgroundColour(Color col)
988   {
989     viewerCommandHistory(false);
990     double normalise = 255D;
991     final String command = "background solid " + col.getRed() / normalise
992             + "," + col.getGreen() / normalise + "," + col.getBlue()
993             / normalise + ";";
994     viewer.sendChimeraCommand(command, false);
995     viewerCommandHistory(true);
996   }
997
998   /**
999    * Ask Chimera to save its session to the given file. Returns true if
1000    * successful, else false.
1001    * 
1002    * @param filepath
1003    * @return
1004    */
1005   public boolean saveSession(String filepath)
1006   {
1007     if (isChimeraRunning())
1008     {
1009       List<String> reply = viewer.sendChimeraCommand("save " + filepath,
1010               true);
1011       if (reply.contains("Session written"))
1012       {
1013         return true;
1014       }
1015       else
1016       {
1017         Cache.log
1018                 .error("Error saving Chimera session: " + reply.toString());
1019       }
1020     }
1021     return false;
1022   }
1023
1024   /**
1025    * Ask Chimera to open a session file. Returns true if successful, else false.
1026    * The filename must have a .py extension for this command to work.
1027    * 
1028    * @param filepath
1029    * @return
1030    */
1031   public boolean openSession(String filepath)
1032   {
1033     sendChimeraCommand("open " + filepath, true);
1034     // todo: test for failure - how?
1035     return true;
1036   }
1037
1038   /**
1039    * Send a 'focus' command to Chimera to recentre the visible display
1040    */
1041   public void focusView()
1042   {
1043     sendChimeraCommand("focus", false);
1044   }
1045
1046   /**
1047    * Send a 'show' command for all atoms in the currently selected columns
1048    * 
1049    * TODO: pull up to abstract structure viewer interface
1050    * 
1051    * @param vp
1052    */
1053   public void highlightSelection(AlignmentViewPanel vp)
1054   {
1055     List<Integer> cols = vp.getAlignViewport().getColumnSelection()
1056             .getSelected();
1057     AlignmentI alignment = vp.getAlignment();
1058     StructureSelectionManager sm = getSsm();
1059     for (SequenceI seq : alignment.getSequences())
1060     {
1061       /*
1062        * convert selected columns into sequence positions
1063        */
1064       int[] positions = new int[cols.size()];
1065       int i = 0;
1066       for (Integer col : cols)
1067       {
1068         positions[i++] = seq.findPosition(col);
1069       }
1070       sm.highlightStructure(this, seq, positions);
1071     }
1072   }
1073
1074
1075   @Override
1076   public List<String> getChainNames()
1077   {
1078     return chainNames;
1079   }
1080
1081   public Hashtable<String, String> getChainFile()
1082   {
1083     return chainFile;
1084   }
1085
1086   public List<ChimeraModel> getChimeraModelByChain(String chain)
1087   {
1088     return chimeraMaps.get(chainFile.get(chain));
1089   }
1090
1091   public int getModelNoForChain(String chain)
1092   {
1093     List<ChimeraModel> foundModels = getChimeraModelByChain(chain);
1094     if (foundModels != null && !foundModels.isEmpty())
1095     {
1096       return foundModels.get(0).getModelNumber();
1097     }
1098     return -1;
1099   }
1100 }