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