JAL-2422 proof of concept adaptations for ChimeraX commands
[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.SequenceRenderer;
25 import jalview.api.structures.JalviewStructureDisplayI;
26 import jalview.bin.Cache;
27 import jalview.datamodel.AlignmentI;
28 import jalview.datamodel.HiddenColumns;
29 import jalview.datamodel.PDBEntry;
30 import jalview.datamodel.SearchResultMatchI;
31 import jalview.datamodel.SearchResultsI;
32 import jalview.datamodel.SequenceFeature;
33 import jalview.datamodel.SequenceI;
34 import jalview.httpserver.AbstractRequestHandler;
35 import jalview.io.DataSourceType;
36 import jalview.schemes.ColourSchemeI;
37 import jalview.schemes.ResidueProperties;
38 import jalview.structure.AtomSpec;
39 import jalview.structure.StructureMappingcommandSet;
40 import jalview.structure.StructureSelectionManager;
41 import jalview.structures.models.AAStructureBindingModel;
42 import jalview.util.MessageManager;
43
44 import java.awt.Color;
45 import java.io.File;
46 import java.io.FileOutputStream;
47 import java.io.IOException;
48 import java.io.PrintWriter;
49 import java.net.BindException;
50 import java.util.ArrayList;
51 import java.util.BitSet;
52 import java.util.Collections;
53 import java.util.Hashtable;
54 import java.util.Iterator;
55 import java.util.LinkedHashMap;
56 import java.util.List;
57 import java.util.Map;
58
59 import ext.edu.ucsf.rbvi.strucviz2.ChimeraManager;
60 import ext.edu.ucsf.rbvi.strucviz2.ChimeraModel;
61 import ext.edu.ucsf.rbvi.strucviz2.StructureManager;
62 import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType;
63
64 public abstract class JalviewChimeraBinding extends AAStructureBindingModel
65 {
66   public static final String CHIMERA_FEATURE_GROUP = "Chimera";
67
68   // Chimera clause to exclude alternate locations in atom selection
69   private static final String NO_ALTLOCS = "&~@.B-Z&~@.2-9";
70
71   private static final String COLOURING_CHIMERA = MessageManager
72           .getString("status.colouring_chimera");
73
74   private static final boolean debug = false;
75
76   private static final String PHOSPHORUS = "P";
77
78   private static final String ALPHACARBON = "CA";
79
80   private List<String> chainNames = new ArrayList<>();
81
82   private Hashtable<String, String> chainFile = new Hashtable<>();
83
84   /*
85    * Object through which we talk to Chimera
86    */
87   private ChimeraManager viewer;
88
89   /*
90    * Object which listens to Chimera notifications
91    */
92   private AbstractRequestHandler chimeraListener;
93
94   /*
95    * set if chimera state is being restored from some source - instructs binding
96    * not to apply default display style when structure set is updated for first
97    * time.
98    */
99   private boolean loadingFromArchive = false;
100
101   /*
102    * flag to indicate if the Chimera viewer should ignore sequence colouring
103    * events from the structure manager because the GUI is still setting up
104    */
105   private boolean loadingFinished = true;
106
107   /*
108    * Map of ChimeraModel objects keyed by PDB full local file name
109    */
110   private Map<String, List<ChimeraModel>> chimeraMaps = new LinkedHashMap<>();
111
112   String lastHighlightCommand;
113
114   /*
115    * incremented every time a load notification is successfully handled -
116    * lightweight mechanism for other threads to detect when they can start
117    * referring to new structures.
118    */
119   private long loadNotifiesHandled = 0;
120
121   private Thread chimeraMonitor;
122
123   /**
124    * Open a PDB structure file in Chimera and set up mappings from Jalview.
125    * 
126    * We check if the PDB model id is already loaded in Chimera, if so don't reopen
127    * it. This is the case if Chimera has opened a saved session file.
128    * 
129    * @param pe
130    * @return
131    */
132   public boolean openFile(PDBEntry pe)
133   {
134     String file = pe.getFile();
135     try
136     {
137       List<ChimeraModel> modelsToMap = new ArrayList<>();
138       List<ChimeraModel> oldList = viewer.isChimeraX() ? new ArrayList<>()
139               : viewer.getModelList();
140       boolean alreadyOpen = false;
141
142       /*
143        * If Chimera already has this model, don't reopen it, but do remap it.
144        */
145       for (ChimeraModel open : oldList)
146       {
147         if (open.getModelName().equals(pe.getId()))
148         {
149           alreadyOpen = true;
150           modelsToMap.add(open);
151         }
152       }
153
154       /*
155        * If Chimera doesn't yet have this model, ask it to open it, and retrieve
156        * the model name(s) added by Chimera.
157        */
158       if (!alreadyOpen)
159       {
160         viewer.openModel(file, pe.getId(), ModelType.PDB_MODEL);
161         if (viewer.isChimeraX())
162         {
163           /*
164            * ChimeraX hack: force chimera model name to pdbId
165            */
166           int modelNumber = chimeraMaps.size() + 1;
167           String command = "setattr #" + modelNumber + " models name "
168                   + pe.getId();
169           sendChimeraCommand(command, false);
170           modelsToMap.add(new ChimeraModel(pe.getId(), ModelType.PDB_MODEL,
171                   modelNumber, 0));
172         }
173         else
174         {
175           /*
176            * Chimera: query for actual models and find the one with
177            * matching model name - set in viewer.openModel()
178            */
179           List<ChimeraModel> newList = viewer.getModelList();
180           // JAL-1728 newList.removeAll(oldList) does not work
181           for (ChimeraModel cm : newList)
182           {
183             if (cm.getModelName().equals(pe.getId()))
184             {
185               modelsToMap.add(cm);
186             }
187           }
188         }
189       }
190
191       chimeraMaps.put(file, modelsToMap);
192
193       if (getSsm() != null)
194       {
195         getSsm().addStructureViewerListener(this);
196       }
197       return true;
198     } catch (Exception q)
199     {
200       log("Exception when trying to open model " + file + "\n"
201               + q.toString());
202       q.printStackTrace();
203     }
204     return false;
205   }
206
207   /**
208    * Constructor
209    * 
210    * @param ssm
211    * @param pdbentry
212    * @param sequenceIs
213    * @param protocol
214    */
215   public JalviewChimeraBinding(StructureSelectionManager ssm,
216           PDBEntry[] pdbentry, SequenceI[][] sequenceIs,
217           DataSourceType protocol)
218   {
219     super(ssm, pdbentry, sequenceIs, protocol);
220     viewer = new ChimeraManager(new StructureManager(true));
221   }
222
223   /**
224    * Starts a thread that waits for the Chimera process to finish, so that we can
225    * then close the associated resources. This avoids leaving orphaned Chimera
226    * viewer panels in Jalview if the user closes Chimera.
227    */
228   protected void startChimeraProcessMonitor()
229   {
230     final Process p = viewer.getChimeraProcess();
231     chimeraMonitor = new Thread(new Runnable()
232     {
233
234       @Override
235       public void run()
236       {
237         try
238         {
239           p.waitFor();
240           JalviewStructureDisplayI display = getViewer();
241           if (display != null)
242           {
243             display.closeViewer(false);
244           }
245         } catch (InterruptedException e)
246         {
247           // exit thread if Chimera Viewer is closed in Jalview
248         }
249       }
250     });
251     chimeraMonitor.start();
252   }
253
254   /**
255    * Start a dedicated HttpServer to listen for Chimera notifications, and tell it
256    * to start listening
257    */
258   public void startChimeraListener()
259   {
260     try
261     {
262       chimeraListener = new ChimeraListener(this);
263       viewer.startListening(chimeraListener.getUri());
264     } catch (BindException e)
265     {
266       System.err.println(
267               "Failed to start Chimera listener: " + e.getMessage());
268     }
269   }
270
271   /**
272    * Tells Chimera to display only the specified chains
273    * 
274    * @param toshow
275    */
276   public void showChains(List<String> toshow)
277   {
278     /*
279      * Construct a chimera command like
280      * 
281      * ~display #*;~ribbon #*;ribbon :.A,:.B
282      */
283     StringBuilder cmd = new StringBuilder(64);
284     boolean first = true;
285     for (String chain : toshow)
286     {
287       int modelNumber = getModelNoForChain(chain);
288       String showChainCmd = modelNumber == -1 ? ""
289               : modelNumber + ":." + chain.split(":")[1];
290       if (!first)
291       {
292         cmd.append(",");
293       }
294       cmd.append(showChainCmd);
295       first = false;
296     }
297
298     /*
299      * could append ";focus" to this command to resize the display to fill the
300      * window, but it looks more helpful not to (easier to relate chains to the
301      * whole)
302      */
303     final String command = "~display #*; ~ribbon #*; ribbon :"
304             + cmd.toString();
305     sendChimeraCommand(command, false);
306   }
307
308   /**
309    * Close down the Jalview viewer and listener, and (optionally) the associated
310    * Chimera window.
311    */
312   public void closeViewer(boolean closeChimera)
313   {
314     getSsm().removeStructureViewerListener(this, this.getStructureFiles());
315     if (closeChimera)
316     {
317       viewer.exitChimera();
318     }
319     if (this.chimeraListener != null)
320     {
321       chimeraListener.shutdown();
322       chimeraListener = null;
323     }
324     viewer = null;
325
326     if (chimeraMonitor != null)
327     {
328       chimeraMonitor.interrupt();
329     }
330     releaseUIResources();
331   }
332
333   @Override
334   public void colourByChain()
335   {
336     colourBySequence = false;
337     sendAsynchronousCommand("rainbow chain", COLOURING_CHIMERA);
338   }
339
340   /**
341    * Constructs and sends a Chimera command to colour by charge
342    * <ul>
343    * <li>Aspartic acid and Glutamic acid (negative charge) red</li>
344    * <li>Lysine and Arginine (positive charge) blue</li>
345    * <li>Cysteine - yellow</li>
346    * <li>all others - white</li>
347    * </ul>
348    */
349   @Override
350   public void colourByCharge()
351   {
352     colourBySequence = false;
353     String command = viewer.isChimeraX()
354             ? "color white;color :ASP,GLU red;color :LYS,ARG blue;color :CYS yellow"
355             : "color white;color red ::ASP;color red ::GLU;color blue ::LYS;color blue ::ARG;color yellow ::CYS";
356     sendAsynchronousCommand(command, COLOURING_CHIMERA);
357   }
358
359   /**
360    * {@inheritDoc}
361    */
362   @Override
363   public String superposeStructures(AlignmentI[] _alignment,
364           int[] _refStructure, HiddenColumns[] _hiddenCols)
365   {
366     StringBuilder allComs = new StringBuilder(128);
367     String[] files = getStructureFiles();
368
369     if (!waitForFileLoad(files))
370     {
371       return null;
372     }
373
374     refreshPdbEntries();
375     StringBuilder selectioncom = new StringBuilder(256);
376     boolean chimeraX = viewer.isChimeraX();
377     for (int a = 0; a < _alignment.length; a++)
378     {
379       int refStructure = _refStructure[a];
380       AlignmentI alignment = _alignment[a];
381       HiddenColumns hiddenCols = _hiddenCols[a];
382
383       if (refStructure >= files.length)
384       {
385         System.err.println("Ignoring invalid reference structure value "
386                 + refStructure);
387         refStructure = -1;
388       }
389
390       /*
391        * 'matched' bit i will be set for visible alignment columns i where
392        * all sequences have a residue with a mapping to the PDB structure
393        */
394       BitSet matched = new BitSet();
395       for (int m = 0; m < alignment.getWidth(); m++)
396       {
397         if (hiddenCols == null || hiddenCols.isVisible(m))
398         {
399           matched.set(m);
400         }
401       }
402
403       SuperposeData[] structures = new SuperposeData[files.length];
404       for (int f = 0; f < files.length; f++)
405       {
406         structures[f] = new SuperposeData(alignment.getWidth());
407       }
408
409       /*
410        * Calculate the superposable alignment columns ('matched'), and the
411        * corresponding structure residue positions (structures.pdbResNo)
412        */
413       int candidateRefStructure = findSuperposableResidues(alignment,
414               matched, structures);
415       if (refStructure < 0)
416       {
417         /*
418          * If no reference structure was specified, pick the first one that has
419          * a mapping in the alignment
420          */
421         refStructure = candidateRefStructure;
422       }
423
424       int nmatched = matched.cardinality();
425       if (nmatched < 4)
426       {
427         return MessageManager.formatMessage("label.insufficient_residues",
428                 nmatched);
429       }
430
431       /*
432        * Generate select statements to select regions to superimpose structures
433        */
434       String[] selcom = new String[files.length];
435       for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
436       {
437         final int modelNo = pdbfnum + (chimeraX ? 1 : 0);
438         // todo correct resolution to model number
439         String chainCd = "." + structures[pdbfnum].chain;
440         int lpos = -1;
441         boolean run = false;
442         StringBuilder molsel = new StringBuilder();
443         if (chimeraX)
444         {
445           molsel.append("/" + structures[pdbfnum].chain + ":");
446         }
447
448         int nextColumnMatch = matched.nextSetBit(0);
449         while (nextColumnMatch != -1)
450         {
451           int pdbResNum = structures[pdbfnum].pdbResNo[nextColumnMatch];
452           if (lpos != pdbResNum - 1)
453           {
454             /*
455              * discontiguous - append last residue now
456              */
457             if (lpos != -1)
458             {
459               molsel.append(String.valueOf(lpos));
460               if (!chimeraX)
461               {
462                 molsel.append(chainCd);
463               }
464               molsel.append(",");
465             }
466             run = false;
467           }
468           else
469           {
470             /*
471              * extending a contiguous run
472              */
473             if (!run)
474             {
475               /*
476                * start the range selection
477                */
478               molsel.append(String.valueOf(lpos));
479               molsel.append("-");
480             }
481             run = true;
482           }
483           lpos = pdbResNum;
484           nextColumnMatch = matched.nextSetBit(nextColumnMatch + 1);
485         }
486
487         /*
488          * and terminate final selection
489          */
490         if (lpos != -1)
491         {
492           molsel.append(String.valueOf(lpos));
493           if (!chimeraX)
494           {
495             molsel.append(chainCd);
496           }
497         }
498         if (molsel.length() > 1)
499         {
500           selcom[pdbfnum] = molsel.toString();
501           selectioncom.append("#").append(String.valueOf(modelNo));
502           if (!chimeraX)
503           {
504             selectioncom.append(":");
505           }
506           selectioncom.append(selcom[pdbfnum]);
507           // selectioncom.append(" ");
508           if (pdbfnum < files.length - 1)
509           {
510             selectioncom.append("|");
511           }
512         }
513         else
514         {
515           selcom[pdbfnum] = null;
516         }
517       }
518
519       StringBuilder command = new StringBuilder(256);
520       for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
521       {
522         final int modelNo = pdbfnum + (chimeraX ? 1 : 0);
523         if (pdbfnum == refStructure || selcom[pdbfnum] == null
524                 || selcom[refStructure] == null)
525         {
526           continue;
527         }
528         if (command.length() > 0)
529         {
530           command.append(";");
531         }
532
533         /*
534          * Form Chimera match command, from the 'new' structure to the
535          * 'reference' structure e.g. (50 residues, chain B/A, alphacarbons):
536          * 
537          * match #1:1-30.B,81-100.B@CA #0:21-40.A,61-90.A@CA
538          * 
539          * @see
540          * https://www.cgl.ucsf.edu/chimera/docs/UsersGuide/midas/match.html
541          */
542         command.append(chimeraX ? "align " : "match ");
543         command.append(getModelSpec(modelNo));
544         if (!chimeraX)
545         {
546           command.append(":");
547         }
548         command.append(selcom[pdbfnum]);
549         command.append("@").append(
550                 structures[pdbfnum].isRna ? PHOSPHORUS : ALPHACARBON);
551         // JAL-1757 exclude alternate CA locations - ChimeraX syntax tbd
552         if (!chimeraX)
553         {
554           command.append(NO_ALTLOCS);
555         }
556         command.append(chimeraX ? " toAtoms " : " ")
557                 .append(getModelSpec(refStructure + (chimeraX ? 1 : 0)));
558         if (!chimeraX)
559         {
560           command.append(":");
561         }
562         command.append(selcom[refStructure]);
563         command.append("@").append(
564                 structures[refStructure].isRna ? PHOSPHORUS : ALPHACARBON);
565         if (!chimeraX)
566         {
567           command.append(NO_ALTLOCS);
568         }
569       }
570       if (selectioncom.length() > 0)
571       {
572         if (debug)
573         {
574           System.out.println("Select regions:\n" + selectioncom.toString());
575           System.out.println(
576                   "Superimpose command(s):\n" + command.toString());
577         }
578         // allComs.append("~display all; ");
579         // if (chimeraX)
580         // {
581         // allComs.append("show ").append(selectioncom.toString())
582         // .append(" pbonds");
583         // }
584         // else
585         // {
586         // allComs.append("chain @CA|P; ribbon ");
587         // allComs.append(selectioncom.toString());
588         // }
589         if (allComs.length() > 0) {
590           allComs.append(";");
591         }
592         allComs.append(command.toString());
593       }
594     }
595
596     String error = null;
597     if (selectioncom.length() > 0)
598     {
599       // TODO: visually distinguish regions that were superposed
600       if (selectioncom.substring(selectioncom.length() - 1).equals("|"))
601       {
602         selectioncom.setLength(selectioncom.length() - 1);
603       }
604       if (debug)
605       {
606         System.out.println("Select regions:\n" + selectioncom.toString());
607       }
608       allComs.append(";~display all; ");
609       if (chimeraX)
610       {
611         allComs.append("show @CA|P pbonds; show ")
612                 .append(selectioncom.toString()).append(" ribbons; view");
613       }
614       else
615       {
616         allComs.append("chain @CA|P; ribbon ; focus");
617         allComs.append(selectioncom.toString());
618       }
619       // allComs.append("; ~display all; chain @CA|P; ribbon ")
620       // .append(selectioncom.toString()).append("; focus");
621       List<String> chimeraReplies = sendChimeraCommand(allComs.toString(),
622               true);
623       for (String reply : chimeraReplies)
624       {
625         if (reply.toLowerCase().contains("unequal numbers of atoms"))
626         {
627           error = reply;
628         }
629       }
630     }
631     return error;
632   }
633
634   /**
635    * Helper method to construct model spec in Chimera format:
636    * <ul>
637    * <li>#0 (#1 etc) for a PDB file with no sub-models</li>
638    * <li>#0.1 (#1.1 etc) for a PDB file with sub-models</li>
639    * <ul>
640    * Note for now we only ever choose the first of multiple models. This
641    * corresponds to the hard-coded Jmol equivalent (compare {1.1}). Refactor in
642    * future if there is a need to select specific sub-models.
643    * 
644    * @param pdbfnum
645    * @return
646    */
647   protected String getModelSpec(int pdbfnum)
648   {
649     if (pdbfnum < 0 || pdbfnum >= getPdbCount())
650     {
651       return "#" + pdbfnum; // temp hack for ChimeraX
652     }
653
654     /*
655      * For now, the test for having sub-models is whether multiple Chimera
656      * models are mapped for the PDB file; the models are returned as a response
657      * to the Chimera command 'list models type molecule', see
658      * ChimeraManager.getModelList().
659      */
660     List<ChimeraModel> maps = chimeraMaps.get(getStructureFiles()[pdbfnum]);
661     boolean hasSubModels = maps != null && maps.size() > 1;
662     return "#" + String.valueOf(pdbfnum) + (hasSubModels ? ".1" : "");
663   }
664
665   /**
666    * Launch Chimera, unless an instance linked to this object is already
667    * running. Returns true if Chimera is successfully launched, or already
668    * running, else false.
669    * 
670    * @return
671    */
672   public boolean launchChimera()
673   {
674     if (viewer.isChimeraLaunched())
675     {
676       return true;
677     }
678
679     boolean launched = viewer
680             .launchChimera(StructureManager.getChimeraPaths());
681     if (launched)
682     {
683       startChimeraProcessMonitor();
684     }
685     else
686     {
687       log("Failed to launch Chimera!");
688     }
689     return launched;
690   }
691
692   /**
693    * Answers true if the Chimera process is still running, false if ended or not
694    * started.
695    * 
696    * @return
697    */
698   public boolean isChimeraRunning()
699   {
700     return viewer.isChimeraLaunched();
701   }
702
703   /**
704    * Send a command to Chimera, and optionally log and return any responses.
705    * <p>
706    * Does nothing, and returns null, if the command is the same as the last one
707    * sent [why?].
708    * 
709    * @param command
710    * @param getResponse
711    */
712   public List<String> sendChimeraCommand(final String command,
713           boolean getResponse)
714   {
715     if (viewer == null)
716     {
717       // ? thread running after viewer shut down
718       return null;
719     }
720     List<String> reply = null;
721     viewerCommandHistory(false);
722     if (true /*lastCommand == null || !lastCommand.equals(command)*/)
723     {
724       // trim command or it may never find a match in the replyLog!!
725       List<String> lastReply = viewer.sendChimeraCommand(command.trim(),
726               getResponse);
727       if (getResponse)
728       {
729         reply = lastReply;
730         if (debug)
731         {
732           log("Response from command ('" + command + "') was:\n"
733                   + lastReply);
734         }
735       }
736     }
737     viewerCommandHistory(true);
738
739     return reply;
740   }
741
742   /**
743    * Send a Chimera command asynchronously in a new thread. If the progress
744    * message is not null, display this message while the command is executing.
745    * 
746    * @param command
747    * @param progressMsg
748    */
749   protected abstract void sendAsynchronousCommand(String command,
750           String progressMsg);
751
752   /**
753    * Sends a set of colour commands to the structure viewer
754    * 
755    * @param colourBySequenceCommands
756    */
757   @Override
758   protected void colourBySequence(
759           StructureMappingcommandSet[] colourBySequenceCommands)
760   {
761     for (StructureMappingcommandSet cpdbbyseq : colourBySequenceCommands)
762     {
763       for (String command : cpdbbyseq.commands)
764       {
765         sendAsynchronousCommand(command, COLOURING_CHIMERA);
766       }
767     }
768   }
769
770   /**
771    * @param files
772    * @param sr
773    * @param viewPanel
774    * @return
775    */
776   @Override
777   protected StructureMappingcommandSet[] getColourBySequenceCommands(
778           String[] files, SequenceRenderer sr, AlignmentViewPanel viewPanel)
779   {
780     return ChimeraCommands.getColourBySequenceCommand(getSsm(), files,
781             getSequence(), sr, viewPanel, viewer.isChimeraX());
782   }
783
784   /**
785    * @param command
786    */
787   protected void executeWhenReady(String command)
788   {
789     waitForChimera();
790     sendChimeraCommand(command, false);
791     waitForChimera();
792   }
793
794   private void waitForChimera()
795   {
796     while (viewer != null && viewer.isBusy())
797     {
798       try
799       {
800         Thread.sleep(15);
801       } catch (InterruptedException q)
802       {
803       }
804     }
805   }
806
807   // End StructureListener
808   // //////////////////////////
809
810   /**
811    * instruct the Jalview binding to update the pdbentries vector if necessary
812    * prior to matching the viewer's contents to the list of structure files
813    * Jalview knows about.
814    */
815   public abstract void refreshPdbEntries();
816
817   /**
818    * map between index of model filename returned from getPdbFile and the first
819    * index of models from this file in the viewer. Note - this is not trimmed -
820    * use getPdbFile to get number of unique models.
821    */
822   private int _modelFileNameMap[];
823
824   // ////////////////////////////////
825   // /StructureListener
826   @Override
827   public synchronized String[] getStructureFiles()
828   {
829     if (viewer == null)
830     {
831       return new String[0];
832     }
833
834     return chimeraMaps.keySet()
835             .toArray(modelFileNames = new String[chimeraMaps.size()]);
836   }
837
838   /**
839    * Construct and send a command to highlight zero, one or more atoms. We do this
840    * by sending an "rlabel" command to show the residue label at that position.
841    */
842   @Override
843   public void highlightAtoms(List<AtomSpec> atoms)
844   {
845     if (atoms == null || atoms.size() == 0)
846     {
847       return;
848     }
849
850     boolean forChimeraX = viewer.isChimeraX();
851     StringBuilder cmd = new StringBuilder(128);
852     boolean first = true;
853     boolean found = false;
854
855     for (AtomSpec atom : atoms)
856     {
857       int pdbResNum = atom.getPdbResNum();
858       String chain = atom.getChain();
859       String pdbfile = atom.getPdbFile();
860       List<ChimeraModel> cms = chimeraMaps.get(pdbfile);
861       if (cms != null && !cms.isEmpty())
862       {
863         if (first)
864         {
865           cmd.append(forChimeraX ? "label #" : "rlabel #");
866         }
867         else
868         {
869           cmd.append(",");
870         }
871         first = false;
872         if (forChimeraX)
873         {
874           cmd.append(cms.get(0).getModelNumber())
875                   .append("/").append(chain).append(":").append(pdbResNum);
876         }
877         else
878         {
879           cmd.append(cms.get(0).getModelNumber())
880                   .append(":").append(pdbResNum);
881           if (!chain.equals(" ") && !forChimeraX)
882           {
883             cmd.append(".").append(chain);
884           }
885         }
886         found = true;
887       }
888     }
889     String command = cmd.toString();
890
891     /*
892      * avoid repeated commands for the same residue
893      */
894     if (command.equals(lastHighlightCommand))
895     {
896       return;
897     }
898
899     /*
900      * unshow the label for the previous residue
901      */
902     if (lastHighlightCommand != null)
903     {
904       viewer.sendChimeraCommand("~" + lastHighlightCommand, false);
905     }
906     if (found)
907     {
908       viewer.sendChimeraCommand(command, false);
909     }
910     this.lastHighlightCommand = command;
911   }
912
913   /**
914    * Query Chimera for its current selection, and highlight it on the alignment
915    */
916   public void highlightChimeraSelection()
917   {
918     /*
919      * Ask Chimera for its current selection
920      */
921     List<String> selection = viewer.getSelectedResidueSpecs();
922
923     /*
924      * Parse model number, residue and chain for each selected position,
925      * formatted as #0:123.A or #1.2:87.B (#model.submodel:residue.chain)
926      */
927     List<AtomSpec> atomSpecs = convertStructureResiduesToAlignment(
928             selection);
929
930     /*
931      * Broadcast the selection (which may be empty, if the user just cleared all
932      * selections)
933      */
934     getSsm().mouseOverStructure(atomSpecs);
935   }
936
937   /**
938    * Converts a list of Chimera atomspecs to a list of AtomSpec representing the
939    * corresponding residues (if any) in Jalview
940    * 
941    * @param structureSelection
942    * @return
943    */
944   protected List<AtomSpec> convertStructureResiduesToAlignment(
945           List<String> structureSelection)
946   {
947     boolean chimeraX = viewer.isChimeraX();
948     List<AtomSpec> atomSpecs = new ArrayList<>();
949     for (String atomSpec : structureSelection)
950     {
951       try
952       {
953         AtomSpec spec = AtomSpec.fromChimeraAtomspec(atomSpec, chimeraX);
954         String pdbfilename = getPdbFileForModel(spec.getModelNumber());
955         spec.setPdbFile(pdbfilename);
956         atomSpecs.add(spec);
957       } catch (IllegalArgumentException e)
958       {
959         System.err.println("Failed to parse atomspec: " + atomSpec);
960       }
961     }
962     return atomSpecs;
963   }
964
965   /**
966    * @param modelId
967    * @return
968    */
969   protected String getPdbFileForModel(int modelId)
970   {
971     /*
972      * Work out the pdbfilename from the model number
973      */
974     String pdbfilename = modelFileNames[0];
975     findfileloop: for (String pdbfile : this.chimeraMaps.keySet())
976     {
977       for (ChimeraModel cm : chimeraMaps.get(pdbfile))
978       {
979         if (cm.getModelNumber() == modelId)
980         {
981           pdbfilename = pdbfile;
982           break findfileloop;
983         }
984       }
985     }
986     return pdbfilename;
987   }
988
989   private void log(String message)
990   {
991     System.err.println("## Chimera log: " + message);
992   }
993
994   private void viewerCommandHistory(boolean enable)
995   {
996     // log("(Not yet implemented) History "
997     // + ((debug || enable) ? "on" : "off"));
998   }
999
1000   public long getLoadNotifiesHandled()
1001   {
1002     return loadNotifiesHandled;
1003   }
1004
1005   @Override
1006   public void setJalviewColourScheme(ColourSchemeI cs)
1007   {
1008     colourBySequence = false;
1009
1010     if (cs == null)
1011     {
1012       return;
1013     }
1014
1015     viewerCommandHistory(false);
1016     StringBuilder command = new StringBuilder(128);
1017
1018     List<String> residueSet = ResidueProperties.getResidues(isNucleotide(),
1019             false);
1020
1021     /*
1022      * concatenate colour commands, one per residue symbol
1023      * Chimera format:  color 0.000000,0.372549,0.627451 ::VAL
1024      * ChimeraX format: color :VAL rgb(73,73,182)
1025      */
1026     boolean chimeraX = viewer.isChimeraX();
1027     for (String resName : residueSet)
1028     {
1029       char res = resName.length() == 3
1030               ? ResidueProperties.getSingleCharacterCode(resName)
1031               : resName.charAt(0);
1032       Color col = cs.findColour(res, 0, null, null, 0f);
1033       command.append("color ");
1034       String colorSpec = getRgbDescriptor(col, chimeraX);
1035       if (chimeraX)
1036       {
1037         command.append(":").append(resName).append(" ").append(colorSpec);
1038       }
1039       else
1040       {
1041         command.append(colorSpec).append(" ::").append(resName);
1042       }
1043       command.append(";");
1044     }
1045
1046     sendAsynchronousCommand(command.toString(), COLOURING_CHIMERA);
1047     viewerCommandHistory(true);
1048   }
1049
1050   /**
1051    * called when the binding thinks the UI needs to be refreshed after a Chimera
1052    * state change. this could be because structures were loaded, or because an
1053    * error has occurred.
1054    */
1055   public abstract void refreshGUI();
1056
1057   @Override
1058   public void setLoadingFromArchive(boolean loadingFromArchive)
1059   {
1060     this.loadingFromArchive = loadingFromArchive;
1061   }
1062
1063   /**
1064    * 
1065    * @return true if Chimeral is still restoring state or loading is still going
1066    *         on (see setFinsihedLoadingFromArchive)
1067    */
1068   @Override
1069   public boolean isLoadingFromArchive()
1070   {
1071     return loadingFromArchive && !loadingFinished;
1072   }
1073
1074   /**
1075    * modify flag which controls if sequence colouring events are honoured by the
1076    * binding. Should be true for normal operation
1077    * 
1078    * @param finishedLoading
1079    */
1080   @Override
1081   public void setFinishedLoadingFromArchive(boolean finishedLoading)
1082   {
1083     loadingFinished = finishedLoading;
1084   }
1085
1086   /**
1087    * Send the Chimera 'background solid <color>" command.
1088    * 
1089    * @see https
1090    *      ://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/background
1091    *      .html
1092    * @param col
1093    */
1094   @Override
1095   public void setBackgroundColour(Color col)
1096   {
1097     viewerCommandHistory(false);
1098     String command = "set bgColor "
1099             + getRgbDescriptor(col, viewer.isChimeraX());
1100     viewer.sendChimeraCommand(command, false);
1101     viewerCommandHistory(true);
1102   }
1103
1104   /**
1105    * Answers the Chimera/X format for RGB values of the given colour.
1106    * 
1107    * <pre>
1108    * Chimera: r,g,b with values scaled [0=1]
1109    * ChimeraX: rgb(r,g,b) with values scaled 0-255
1110    * </pre>
1111    * 
1112    * @param col
1113    * @param chimeraX
1114    * @return
1115    */
1116   private static String getRgbDescriptor(Color col, boolean chimeraX)
1117   {
1118     if (chimeraX)
1119     {
1120       return String.format("rgb(%d,%d,%d)", col.getRed(), col.getGreen(),
1121               col.getBlue());
1122     }
1123     else
1124     {
1125       double scale = 255D;
1126       return String.format("%f,%f,%f", col.getRed() / scale,
1127               col.getGreen() / scale, col.getBlue() / scale);
1128     }
1129   }
1130
1131   /**
1132    * Ask Chimera to save its session to the given file. Returns true if
1133    * successful, else false.
1134    * 
1135    * @param filepath
1136    * @return
1137    */
1138   public boolean saveSession(String filepath)
1139   {
1140     if (isChimeraRunning())
1141     {
1142       List<String> reply = viewer.sendChimeraCommand("save " + filepath,
1143               true);
1144       if (reply.contains("Session written"))
1145       {
1146         return true;
1147       }
1148       else
1149       {
1150         Cache.log
1151                 .error("Error saving Chimera session: " + reply.toString());
1152       }
1153     }
1154     return false;
1155   }
1156
1157   /**
1158    * Ask Chimera to open a session file. Returns true if successful, else false.
1159    * The filename must have a .py extension for this command to work.
1160    * 
1161    * @param filepath
1162    * @return
1163    */
1164   public boolean openSession(String filepath)
1165   {
1166     sendChimeraCommand("open " + filepath, true);
1167     // todo: test for failure - how?
1168     return true;
1169   }
1170
1171   /**
1172    * Returns a list of chains mapped in this viewer. Note this list is not
1173    * currently scoped per structure.
1174    * 
1175    * @return
1176    */
1177   @Override
1178   public List<String> getChainNames()
1179   {
1180     return chainNames;
1181   }
1182
1183   /**
1184    * Send a 'focus' command to Chimera to recentre the visible display
1185    */
1186   public void focusView()
1187   {
1188     sendChimeraCommand(viewer.isChimeraX() ? "view" : "focus", false);
1189   }
1190
1191   /**
1192    * Send a 'show' command for all atoms in the currently selected columns
1193    * 
1194    * TODO: pull up to abstract structure viewer interface
1195    * 
1196    * @param vp
1197    */
1198   public void highlightSelection(AlignmentViewPanel vp)
1199   {
1200     List<Integer> cols = vp.getAlignViewport().getColumnSelection()
1201             .getSelected();
1202     AlignmentI alignment = vp.getAlignment();
1203     StructureSelectionManager sm = getSsm();
1204     for (SequenceI seq : alignment.getSequences())
1205     {
1206       /*
1207        * convert selected columns into sequence positions
1208        */
1209       int[] positions = new int[cols.size()];
1210       int i = 0;
1211       for (Integer col : cols)
1212       {
1213         positions[i++] = seq.findPosition(col);
1214       }
1215       sm.highlightStructure(this, seq, positions);
1216     }
1217   }
1218
1219   /**
1220    * Constructs and send commands to Chimera to set attributes on residues for
1221    * features visible in Jalview
1222    * 
1223    * @param avp
1224    * @return
1225    */
1226   public int sendFeaturesToViewer(AlignmentViewPanel avp)
1227   {
1228     // TODO refactor as required to pull up to an interface
1229     AlignmentI alignment = avp.getAlignment();
1230
1231     String[] files = getStructureFiles();
1232     if (files == null)
1233     {
1234       return 0;
1235     }
1236
1237     StructureMappingcommandSet commandSet = ChimeraCommands
1238             .getSetAttributeCommandsForFeatures(getSsm(), files,
1239                     getSequence(), avp, viewer.isChimeraX());
1240     String[] commands = commandSet.commands;
1241     if (commands.length > 10)
1242     {
1243       sendCommandsByFile(commands);
1244     }
1245     else
1246     {
1247       for (String command : commands)
1248       {
1249         sendAsynchronousCommand(command, null);
1250       }
1251     }
1252     return commands.length;
1253   }
1254
1255   /**
1256    * Write commands to a temporary file, and send a command to Chimera to open the
1257    * file as a commands script. For use when sending a large number of separate
1258    * commands would overload the REST interface mechanism.
1259    * 
1260    * @param commands
1261    */
1262   protected void sendCommandsByFile(String[] commands)
1263   {
1264     boolean toChimeraX = viewer.isChimeraX();
1265     try
1266     {
1267       File tmp = File.createTempFile("chim", toChimeraX ? ".cxc" : ".com");
1268       tmp.deleteOnExit();
1269       PrintWriter out = new PrintWriter(new FileOutputStream(tmp));
1270       for (String command : commands)
1271       {
1272         out.println(command);
1273       }
1274       out.flush();
1275       out.close();
1276       String path = tmp.getAbsolutePath();
1277       String command = "open " + (toChimeraX ? "" : "cmd:") + path;
1278       sendAsynchronousCommand(command, null);
1279     } catch (IOException e)
1280     {
1281       System.err.println("Sending commands to Chimera via file failed with "
1282               + e.getMessage());
1283     }
1284   }
1285
1286   /**
1287    * Get Chimera residues which have the named attribute, find the mapped
1288    * positions in the Jalview sequence(s), and set as sequence features
1289    * 
1290    * @param attName
1291    * @param alignmentPanel
1292    */
1293   public void copyStructureAttributesToFeatures(String attName,
1294           AlignmentViewPanel alignmentPanel)
1295   {
1296     // todo pull up to AAStructureBindingModel (and interface?)
1297
1298     /*
1299      * ask Chimera to list residues with the attribute, reporting its value
1300      */
1301     // this alternative command
1302     // list residues spec ':*/attName' attr attName
1303     // doesn't report 'None' values (which is good), but
1304     // fails for 'average.bfactor' (which is bad):
1305
1306     String cmd = "list residues attr '" + attName + "'";
1307     List<String> residues = sendChimeraCommand(cmd, true);
1308
1309     boolean featureAdded = createFeaturesForAttributes(attName, residues);
1310     if (featureAdded)
1311     {
1312       alignmentPanel.getFeatureRenderer().featuresAdded();
1313     }
1314   }
1315
1316   /**
1317    * Create features in Jalview for the given attribute name and structure
1318    * residues.
1319    * 
1320    * <pre>
1321    * The residue list should be 0, 1 or more reply lines of the format: 
1322    *     residue id #0:5.A isHelix -155.000836316 index 5 
1323    * or 
1324    *     residue id #0:6.A isHelix None
1325    * </pre>
1326    * 
1327    * @param attName
1328    * @param residues
1329    * @return
1330    */
1331   protected boolean createFeaturesForAttributes(String attName,
1332           List<String> residues)
1333   {
1334     boolean featureAdded = false;
1335     String featureGroup = getViewerFeatureGroup();
1336     boolean chimeraX = viewer.isChimeraX();
1337
1338     for (String residue : residues)
1339     {
1340       AtomSpec spec = null;
1341       String[] tokens = residue.split(" ");
1342       if (tokens.length < 5)
1343       {
1344         continue;
1345       }
1346       String atomSpec = tokens[2];
1347       String attValue = tokens[4];
1348
1349       /*
1350        * ignore 'None' (e.g. for phi) or 'False' (e.g. for isHelix)
1351        */
1352       if ("None".equalsIgnoreCase(attValue)
1353               || "False".equalsIgnoreCase(attValue))
1354       {
1355         continue;
1356       }
1357
1358       try
1359       {
1360         spec = AtomSpec.fromChimeraAtomspec(atomSpec, chimeraX);
1361       } catch (IllegalArgumentException e)
1362       {
1363         System.err.println("Problem parsing atomspec " + atomSpec);
1364         continue;
1365       }
1366
1367       String chainId = spec.getChain();
1368       String description = attValue;
1369       float score = Float.NaN;
1370       try
1371       {
1372         score = Float.valueOf(attValue);
1373         description = chainId;
1374       } catch (NumberFormatException e)
1375       {
1376         // was not a float value
1377       }
1378
1379       String pdbFile = getPdbFileForModel(spec.getModelNumber());
1380       spec.setPdbFile(pdbFile);
1381
1382       List<AtomSpec> atoms = Collections.singletonList(spec);
1383
1384       /*
1385        * locate the mapped position in the alignment (if any)
1386        */
1387       SearchResultsI sr = getSsm()
1388               .findAlignmentPositionsForStructurePositions(atoms);
1389
1390       /*
1391        * expect one matched alignment position, or none 
1392        * (if the structure position is not mapped)
1393        */
1394       for (SearchResultMatchI m : sr.getResults())
1395       {
1396         SequenceI seq = m.getSequence();
1397         int start = m.getStart();
1398         int end = m.getEnd();
1399         SequenceFeature sf = new SequenceFeature(attName, description,
1400                 start, end, score, featureGroup);
1401         // todo: should SequenceFeature have an explicit property for chain?
1402         // note: repeating the action shouldn't duplicate features
1403         featureAdded |= seq.addSequenceFeature(sf);
1404       }
1405     }
1406     return featureAdded;
1407   }
1408
1409   /**
1410    * Answers the feature group name to apply to features created in Jalview from
1411    * Chimera attributes
1412    * 
1413    * @return
1414    */
1415   protected String getViewerFeatureGroup()
1416   {
1417     // todo pull up to interface
1418     return CHIMERA_FEATURE_GROUP;
1419   }
1420
1421   public Hashtable<String, String> getChainFile()
1422   {
1423     return chainFile;
1424   }
1425
1426   public List<ChimeraModel> getChimeraModelByChain(String chain)
1427   {
1428     return chimeraMaps.get(chainFile.get(chain));
1429   }
1430
1431   public int getModelNoForChain(String chain)
1432   {
1433     List<ChimeraModel> foundModels = getChimeraModelByChain(chain);
1434     if (foundModels != null && !foundModels.isEmpty())
1435     {
1436       return foundModels.get(0).getModelNumber();
1437     }
1438     return -1;
1439   }
1440
1441   /**
1442    * Answers a (possibly empty) list of attribute names in Chimera[X], excluding
1443    * any which were added from Jalview
1444    * 
1445    * @return
1446    */
1447   public List<String> getChimeraAttributes()
1448   {
1449     List<String> atts = viewer.getAttrList();
1450     Iterator<String> it = atts.iterator();
1451     while (it.hasNext())
1452     {
1453       if (it.next().startsWith(ChimeraCommands.NAMESPACE_PREFIX))
1454       {
1455         /*
1456          * attribute added from Jalview - exclude it
1457          */
1458         it.remove();
1459       }
1460     }
1461     return atts;
1462   }
1463 }