JAL-3518 basic refactoring / pull up of superposeStructures; to tidy!
[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.structures.JalviewStructureDisplayI;
25 import jalview.bin.Cache;
26 import jalview.datamodel.AlignmentI;
27 import jalview.datamodel.HiddenColumns;
28 import jalview.datamodel.PDBEntry;
29 import jalview.datamodel.SearchResultMatchI;
30 import jalview.datamodel.SearchResultsI;
31 import jalview.datamodel.SequenceFeature;
32 import jalview.datamodel.SequenceI;
33 import jalview.gui.StructureViewer.ViewerType;
34 import jalview.httpserver.AbstractRequestHandler;
35 import jalview.io.DataSourceType;
36 import jalview.structure.AtomSpec;
37 import jalview.structure.StructureCommandsI.SuperposeData;
38 import jalview.structure.StructureSelectionManager;
39 import jalview.structures.models.AAStructureBindingModel;
40 import jalview.util.MessageManager;
41
42 import java.io.File;
43 import java.io.FileOutputStream;
44 import java.io.IOException;
45 import java.io.PrintWriter;
46 import java.net.BindException;
47 import java.util.ArrayList;
48 import java.util.BitSet;
49 import java.util.Collections;
50 import java.util.Iterator;
51 import java.util.LinkedHashMap;
52 import java.util.List;
53 import java.util.Map;
54
55 import ext.edu.ucsf.rbvi.strucviz2.ChimeraManager;
56 import ext.edu.ucsf.rbvi.strucviz2.ChimeraModel;
57 import ext.edu.ucsf.rbvi.strucviz2.StructureManager;
58 import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType;
59
60 public abstract class JalviewChimeraBinding extends AAStructureBindingModel
61 {
62   public static final String CHIMERA_FEATURE_GROUP = "Chimera";
63
64   // Chimera clause to exclude alternate locations in atom selection
65   private static final String NO_ALTLOCS = "&~@.B-Z&~@.2-9";
66
67   private static final boolean debug = false;
68
69   private static final String PHOSPHORUS = "P";
70
71   private static final String ALPHACARBON = "CA";
72
73   /*
74    * Object through which we talk to Chimera
75    */
76   private ChimeraManager chimeraManager;
77
78   /*
79    * Object which listens to Chimera notifications
80    */
81   private AbstractRequestHandler chimeraListener;
82
83   /*
84    * Map of ChimeraModel objects keyed by PDB full local file name
85    */
86   protected Map<String, List<ChimeraModel>> chimeraMaps = new LinkedHashMap<>();
87
88   String lastHighlightCommand;
89
90   private Thread chimeraMonitor;
91
92   /**
93    * Open a PDB structure file in Chimera and set up mappings from Jalview.
94    * 
95    * We check if the PDB model id is already loaded in Chimera, if so don't reopen
96    * it. This is the case if Chimera has opened a saved session file.
97    * 
98    * @param pe
99    * @return
100    */
101   public boolean openFile(PDBEntry pe)
102   {
103     String file = pe.getFile();
104     try
105     {
106       List<ChimeraModel> modelsToMap = new ArrayList<>();
107       List<ChimeraModel> oldList = chimeraManager.getModelList();
108       boolean alreadyOpen = false;
109
110       /*
111        * If Chimera already has this model, don't reopen it, but do remap it.
112        */
113       for (ChimeraModel open : oldList)
114       {
115         if (open.getModelName().equals(pe.getId()))
116         {
117           alreadyOpen = true;
118           modelsToMap.add(open);
119         }
120       }
121
122       /*
123        * If Chimera doesn't yet have this model, ask it to open it, and retrieve
124        * the model name(s) added by Chimera.
125        */
126       if (!alreadyOpen)
127       {
128         chimeraManager.openModel(file, pe.getId(), ModelType.PDB_MODEL);
129         addChimeraModel(pe, modelsToMap);
130       }
131
132       chimeraMaps.put(file, modelsToMap);
133
134       if (getSsm() != null)
135       {
136         getSsm().addStructureViewerListener(this);
137       }
138       return true;
139     } catch (Exception q)
140     {
141       log("Exception when trying to open model " + file + "\n"
142               + q.toString());
143       q.printStackTrace();
144     }
145     return false;
146   }
147
148   /**
149    * Adds the ChimeraModel corresponding to the given PDBEntry, based on model
150    * name matching PDB id
151    * 
152    * @param pe
153    * @param modelsToMap
154    */
155   protected void addChimeraModel(PDBEntry pe,
156           List<ChimeraModel> modelsToMap)
157   {
158     /*
159      * Chimera: query for actual models and find the one with
160      * matching model name - already set in viewer.openModel()
161      */
162     List<ChimeraModel> newList = chimeraManager.getModelList();
163     // JAL-1728 newList.removeAll(oldList) does not work
164     for (ChimeraModel cm : newList)
165     {
166       if (cm.getModelName().equals(pe.getId()))
167       {
168         modelsToMap.add(cm);
169       }
170     }
171   }
172
173   /**
174    * Constructor
175    * 
176    * @param ssm
177    * @param pdbentry
178    * @param sequenceIs
179    * @param protocol
180    */
181   public JalviewChimeraBinding(StructureSelectionManager ssm,
182           PDBEntry[] pdbentry, SequenceI[][] sequenceIs,
183           DataSourceType protocol)
184   {
185     super(ssm, pdbentry, sequenceIs, protocol);
186     chimeraManager = new ChimeraManager(new StructureManager(true));
187     chimeraManager.setChimeraX(ViewerType.CHIMERAX.equals(getViewerType()));
188     setStructureCommands(new ChimeraCommands());
189   }
190
191   @Override
192   protected ViewerType getViewerType()
193   {
194     return ViewerType.CHIMERA;
195   }
196
197   /**
198    * Starts a thread that waits for the Chimera process to finish, so that we can
199    * then close the associated resources. This avoids leaving orphaned Chimera
200    * viewer panels in Jalview if the user closes Chimera.
201    */
202   protected void startChimeraProcessMonitor()
203   {
204     final Process p = chimeraManager.getChimeraProcess();
205     chimeraMonitor = new Thread(new Runnable()
206     {
207
208       @Override
209       public void run()
210       {
211         try
212         {
213           p.waitFor();
214           JalviewStructureDisplayI display = getViewer();
215           if (display != null)
216           {
217             display.closeViewer(false);
218           }
219         } catch (InterruptedException e)
220         {
221           // exit thread if Chimera Viewer is closed in Jalview
222         }
223       }
224     });
225     chimeraMonitor.start();
226   }
227
228   /**
229    * Start a dedicated HttpServer to listen for Chimera notifications, and tell it
230    * to start listening
231    */
232   public void startChimeraListener()
233   {
234     try
235     {
236       chimeraListener = new ChimeraListener(this);
237       chimeraManager.startListening(chimeraListener.getUri());
238     } catch (BindException e)
239     {
240       System.err.println(
241               "Failed to start Chimera listener: " + e.getMessage());
242     }
243   }
244
245   /**
246    * Close down the Jalview viewer and listener, and (optionally) the associated
247    * Chimera window.
248    */
249   public void closeViewer(boolean closeChimera)
250   {
251     getSsm().removeStructureViewerListener(this, this.getStructureFiles());
252     if (closeChimera)
253     {
254       chimeraManager.exitChimera();
255     }
256     if (this.chimeraListener != null)
257     {
258       chimeraListener.shutdown();
259       chimeraListener = null;
260     }
261     chimeraManager = null;
262
263     if (chimeraMonitor != null)
264     {
265       chimeraMonitor.interrupt();
266     }
267     releaseUIResources();
268   }
269
270   /**
271    * {@inheritDoc}
272    */
273   public String superposeStructures(AlignmentI[] _alignment,
274           int[] _refStructure, HiddenColumns[] _hiddenCols)
275   {
276     StringBuilder allComs = new StringBuilder(128);
277     String[] files = getStructureFiles();
278
279     if (!waitForFileLoad(files))
280     {
281       return null;
282     }
283
284     refreshPdbEntries();
285     StringBuilder selectioncom = new StringBuilder(256);
286     boolean chimeraX = chimeraManager.isChimeraX();
287     for (int a = 0; a < _alignment.length; a++)
288     {
289       int refStructure = _refStructure[a];
290       AlignmentI alignment = _alignment[a];
291       HiddenColumns hiddenCols = _hiddenCols[a];
292
293       if (refStructure >= files.length)
294       {
295         System.err.println("Ignoring invalid reference structure value "
296                 + refStructure);
297         refStructure = -1;
298       }
299
300       /*
301        * 'matched' bit i will be set for visible alignment columns i where
302        * all sequences have a residue with a mapping to the PDB structure
303        */
304       BitSet matched = new BitSet();
305       for (int m = 0; m < alignment.getWidth(); m++)
306       {
307         if (hiddenCols == null || hiddenCols.isVisible(m))
308         {
309           matched.set(m);
310         }
311       }
312
313       SuperposeData[] structures = new SuperposeData[files.length];
314       for (int f = 0; f < files.length; f++)
315       {
316         structures[f] = new SuperposeData(alignment.getWidth(), f);
317       }
318
319       /*
320        * Calculate the superposable alignment columns ('matched'), and the
321        * corresponding structure residue positions (structures.pdbResNo)
322        */
323       int candidateRefStructure = findSuperposableResidues(alignment,
324               matched, structures);
325       if (refStructure < 0)
326       {
327         /*
328          * If no reference structure was specified, pick the first one that has
329          * a mapping in the alignment
330          */
331         refStructure = candidateRefStructure;
332       }
333
334       int nmatched = matched.cardinality();
335       if (nmatched < 4)
336       {
337         return MessageManager.formatMessage("label.insufficient_residues",
338                 nmatched);
339       }
340
341       /*
342        * Generate select statements to select regions to superimpose structures
343        */
344       String[] selcom = new String[files.length];
345       for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
346       {
347         final int modelNo = pdbfnum + (chimeraX ? 1 : 0);
348         // todo correct resolution to model number
349         String chainCd = "." + structures[pdbfnum].chain;
350         int lpos = -1;
351         boolean run = false;
352         StringBuilder molsel = new StringBuilder();
353         if (chimeraX)
354         {
355           molsel.append("/" + structures[pdbfnum].chain + ":");
356         }
357
358         int nextColumnMatch = matched.nextSetBit(0);
359         while (nextColumnMatch != -1)
360         {
361           int pdbResNum = structures[pdbfnum].pdbResNo[nextColumnMatch];
362           if (lpos != pdbResNum - 1)
363           {
364             /*
365              * discontiguous - append last residue now
366              */
367             if (lpos != -1)
368             {
369               molsel.append(String.valueOf(lpos));
370               if (!chimeraX)
371               {
372                 molsel.append(chainCd);
373               }
374               molsel.append(",");
375             }
376             run = false;
377           }
378           else
379           {
380             /*
381              * extending a contiguous run
382              */
383             if (!run)
384             {
385               /*
386                * start the range selection
387                */
388               molsel.append(String.valueOf(lpos));
389               molsel.append("-");
390             }
391             run = true;
392           }
393           lpos = pdbResNum;
394           nextColumnMatch = matched.nextSetBit(nextColumnMatch + 1);
395         }
396
397         /*
398          * and terminate final selection
399          */
400         if (lpos != -1)
401         {
402           molsel.append(String.valueOf(lpos));
403           if (!chimeraX)
404           {
405             molsel.append(chainCd);
406           }
407         }
408         if (molsel.length() > 1)
409         {
410           selcom[pdbfnum] = molsel.toString();
411           selectioncom.append("#").append(String.valueOf(modelNo));
412           if (!chimeraX)
413           {
414             selectioncom.append(":");
415           }
416           selectioncom.append(selcom[pdbfnum]);
417           // selectioncom.append(" ");
418           if (pdbfnum < files.length - 1)
419           {
420             selectioncom.append("|");
421           }
422         }
423         else
424         {
425           selcom[pdbfnum] = null;
426         }
427       }
428
429       StringBuilder command = new StringBuilder(256);
430       for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
431       {
432         final int modelNo = pdbfnum + (chimeraX ? 1 : 0);
433         if (pdbfnum == refStructure || selcom[pdbfnum] == null
434                 || selcom[refStructure] == null)
435         {
436           continue;
437         }
438         if (command.length() > 0)
439         {
440           command.append(";");
441         }
442
443         /*
444          * Form Chimera match command, from the 'new' structure to the
445          * 'reference' structure e.g. (50 residues, chain B/A, alphacarbons):
446          * 
447          * match #1:1-30.B,81-100.B@CA #0:21-40.A,61-90.A@CA
448          * 
449          * @see
450          * https://www.cgl.ucsf.edu/chimera/docs/UsersGuide/midas/match.html
451          */
452         command.append(chimeraX ? "align " : "match ");
453         command.append(getModelSpec(modelNo));
454         if (!chimeraX)
455         {
456           command.append(":");
457         }
458         command.append(selcom[pdbfnum]);
459         command.append("@").append(
460                 structures[pdbfnum].isRna ? PHOSPHORUS : ALPHACARBON);
461         // JAL-1757 exclude alternate CA locations - ChimeraX syntax tbd
462         if (!chimeraX)
463         {
464           command.append(NO_ALTLOCS);
465         }
466         command.append(chimeraX ? " toAtoms " : " ")
467                 .append(getModelSpec(refStructure + (chimeraX ? 1 : 0)));
468         if (!chimeraX)
469         {
470           command.append(":");
471         }
472         command.append(selcom[refStructure]);
473         command.append("@").append(
474                 structures[refStructure].isRna ? PHOSPHORUS : ALPHACARBON);
475         if (!chimeraX)
476         {
477           command.append(NO_ALTLOCS);
478         }
479       }
480       if (selectioncom.length() > 0)
481       {
482         if (debug)
483         {
484           System.out.println("Select regions:\n" + selectioncom.toString());
485           System.out.println(
486                   "Superimpose command(s):\n" + command.toString());
487         }
488         // allComs.append("~display all; ");
489         // if (chimeraX)
490         // {
491         // allComs.append("show ").append(selectioncom.toString())
492         // .append(" pbonds");
493         // }
494         // else
495         // {
496         // allComs.append("chain @CA|P; ribbon ");
497         // allComs.append(selectioncom.toString());
498         // }
499         if (allComs.length() > 0) {
500           allComs.append(";");
501         }
502         allComs.append(command.toString());
503       }
504     }
505
506     String error = null;
507     if (selectioncom.length() > 0)
508     {
509       // TODO: visually distinguish regions that were superposed
510       if (selectioncom.substring(selectioncom.length() - 1).equals("|"))
511       {
512         selectioncom.setLength(selectioncom.length() - 1);
513       }
514       if (debug)
515       {
516         System.out.println("Select regions:\n" + selectioncom.toString());
517       }
518       allComs.append(";~display all; ");
519       if (chimeraX)
520       {
521         allComs.append("show @CA|P pbonds; show ")
522                 .append(selectioncom.toString()).append(" ribbons; view");
523       }
524       else
525       {
526         allComs.append("chain @CA|P; ribbon ");
527         allComs.append(selectioncom.toString()).append("; focus");
528       }
529       // allComs.append("; ~display all; chain @CA|P; ribbon ")
530       // .append(selectioncom.toString()).append("; focus");
531       List<String> chimeraReplies = executeCommand(allComs.toString(),
532               true);
533       for (String reply : chimeraReplies)
534       {
535         if (reply.toLowerCase().contains("unequal numbers of atoms"))
536         {
537           error = reply;
538         }
539       }
540     }
541     return error;
542   }
543
544   /**
545    * Helper method to construct model spec in Chimera format:
546    * <ul>
547    * <li>#0 (#1 etc) for a PDB file with no sub-models</li>
548    * <li>#0.1 (#1.1 etc) for a PDB file with sub-models</li>
549    * <ul>
550    * Note for now we only ever choose the first of multiple models. This
551    * corresponds to the hard-coded Jmol equivalent (compare {1.1}). Refactor in
552    * future if there is a need to select specific sub-models.
553    * 
554    * @param pdbfnum
555    * @return
556    */
557   protected String getModelSpec(int pdbfnum)
558   {
559     if (pdbfnum < 0 || pdbfnum >= getPdbCount())
560     {
561       return "#" + pdbfnum; // temp hack for ChimeraX
562     }
563
564     /*
565      * For now, the test for having sub-models is whether multiple Chimera
566      * models are mapped for the PDB file; the models are returned as a response
567      * to the Chimera command 'list models type molecule', see
568      * ChimeraManager.getModelList().
569      */
570     List<ChimeraModel> maps = chimeraMaps.get(getStructureFiles()[pdbfnum]);
571     boolean hasSubModels = maps != null && maps.size() > 1;
572     return "#" + String.valueOf(pdbfnum) + (hasSubModels ? ".1" : "");
573   }
574
575   /**
576    * Launch Chimera, unless an instance linked to this object is already
577    * running. Returns true if Chimera is successfully launched, or already
578    * running, else false.
579    * 
580    * @return
581    */
582   public boolean launchChimera()
583   {
584     if (chimeraManager.isChimeraLaunched())
585     {
586       return true;
587     }
588
589     boolean launched = chimeraManager.launchChimera(getChimeraPaths());
590     if (launched)
591     {
592       startChimeraProcessMonitor();
593     }
594     else
595     {
596       log("Failed to launch Chimera!");
597     }
598     return launched;
599   }
600
601   /**
602    * Returns a list of candidate paths to the Chimera program executable
603    * 
604    * @return
605    */
606   protected List<String> getChimeraPaths()
607   {
608     return StructureManager.getChimeraPaths(false);
609   }
610
611   /**
612    * Answers true if the Chimera process is still running, false if ended or not
613    * started.
614    * 
615    * @return
616    */
617   public boolean isChimeraRunning()
618   {
619     return chimeraManager.isChimeraLaunched();
620   }
621
622   /**
623    * Send a command to Chimera, and optionally log and return any responses.
624    * 
625    * @param command
626    * @param getResponse
627    */
628   @Override
629   public List<String> executeCommand(final String command,
630           boolean getResponse)
631   {
632     if (chimeraManager == null || command == null)
633     {
634       // ? thread running after viewer shut down
635       return null;
636     }
637     List<String> reply = null;
638     // trim command or it may never find a match in the replyLog!!
639     List<String> lastReply = chimeraManager
640             .sendChimeraCommand(command.trim(), getResponse);
641     if (getResponse)
642     {
643       reply = lastReply;
644       if (debug)
645       {
646         log("Response from command ('" + command + "') was:\n" + lastReply);
647       }
648     }
649
650     return reply;
651   }
652
653   /**
654    * @param command
655    */
656   protected void executeWhenReady(String command)
657   {
658     waitForChimera();
659     executeCommand(command, false);
660     waitForChimera();
661   }
662
663   private void waitForChimera()
664   {
665     while (chimeraManager != null && chimeraManager.isBusy())
666     {
667       try
668       {
669         Thread.sleep(15);
670       } catch (InterruptedException q)
671       {
672       }
673     }
674   }
675
676   @Override
677   public synchronized String[] getStructureFiles()
678   {
679     if (chimeraManager == null)
680     {
681       return new String[0];
682     }
683
684     return chimeraMaps.keySet()
685             .toArray(modelFileNames = new String[chimeraMaps.size()]);
686   }
687
688   /**
689    * Construct and send a command to highlight zero, one or more atoms. We do this
690    * by sending an "rlabel" command to show the residue label at that position.
691    */
692   @Override
693   public void highlightAtoms(List<AtomSpec> atoms)
694   {
695     if (atoms == null || atoms.size() == 0)
696     {
697       return;
698     }
699
700     boolean forChimeraX = chimeraManager.isChimeraX();
701     StringBuilder cmd = new StringBuilder(128);
702     boolean first = true;
703     boolean found = false;
704
705     for (AtomSpec atom : atoms)
706     {
707       int pdbResNum = atom.getPdbResNum();
708       String chain = atom.getChain();
709       String pdbfile = atom.getPdbFile();
710       List<ChimeraModel> cms = chimeraMaps.get(pdbfile);
711       if (cms != null && !cms.isEmpty())
712       {
713         if (first)
714         {
715           cmd.append(forChimeraX ? "label #" : "rlabel #");
716         }
717         else
718         {
719           cmd.append(",");
720         }
721         first = false;
722         if (forChimeraX)
723         {
724           cmd.append(cms.get(0).getModelNumber())
725                   .append("/").append(chain).append(":").append(pdbResNum);
726         }
727         else
728         {
729           cmd.append(cms.get(0).getModelNumber())
730                   .append(":").append(pdbResNum);
731           if (!chain.equals(" ") && !forChimeraX)
732           {
733             cmd.append(".").append(chain);
734           }
735         }
736         found = true;
737       }
738     }
739     String command = cmd.toString();
740
741     /*
742      * avoid repeated commands for the same residue
743      */
744     if (command.equals(lastHighlightCommand))
745     {
746       return;
747     }
748
749     /*
750      * unshow the label for the previous residue
751      */
752     if (lastHighlightCommand != null)
753     {
754       chimeraManager.sendChimeraCommand("~" + lastHighlightCommand, false);
755     }
756     if (found)
757     {
758       chimeraManager.sendChimeraCommand(command, false);
759     }
760     this.lastHighlightCommand = command;
761   }
762
763   /**
764    * Query Chimera for its current selection, and highlight it on the alignment
765    */
766   public void highlightChimeraSelection()
767   {
768     /*
769      * Ask Chimera for its current selection
770      */
771     List<String> selection = chimeraManager.getSelectedResidueSpecs();
772
773     /*
774      * Parse model number, residue and chain for each selected position,
775      * formatted as #0:123.A or #1.2:87.B (#model.submodel:residue.chain)
776      */
777     List<AtomSpec> atomSpecs = convertStructureResiduesToAlignment(
778             selection);
779
780     /*
781      * Broadcast the selection (which may be empty, if the user just cleared all
782      * selections)
783      */
784     getSsm().mouseOverStructure(atomSpecs);
785   }
786
787   /**
788    * Converts a list of Chimera atomspecs to a list of AtomSpec representing the
789    * corresponding residues (if any) in Jalview
790    * 
791    * @param structureSelection
792    * @return
793    */
794   protected List<AtomSpec> convertStructureResiduesToAlignment(
795           List<String> structureSelection)
796   {
797     boolean chimeraX = chimeraManager.isChimeraX();
798     List<AtomSpec> atomSpecs = new ArrayList<>();
799     for (String atomSpec : structureSelection)
800     {
801       try
802       {
803         AtomSpec spec = AtomSpec.fromChimeraAtomspec(atomSpec, chimeraX);
804         String pdbfilename = getPdbFileForModel(spec.getModelNumber());
805         spec.setPdbFile(pdbfilename);
806         atomSpecs.add(spec);
807       } catch (IllegalArgumentException e)
808       {
809         System.err.println("Failed to parse atomspec: " + atomSpec);
810       }
811     }
812     return atomSpecs;
813   }
814
815   /**
816    * @param modelId
817    * @return
818    */
819   protected String getPdbFileForModel(int modelId)
820   {
821     /*
822      * Work out the pdbfilename from the model number
823      */
824     String pdbfilename = modelFileNames[0];
825     findfileloop: for (String pdbfile : this.chimeraMaps.keySet())
826     {
827       for (ChimeraModel cm : chimeraMaps.get(pdbfile))
828       {
829         if (cm.getModelNumber() == modelId)
830         {
831           pdbfilename = pdbfile;
832           break findfileloop;
833         }
834       }
835     }
836     return pdbfilename;
837   }
838
839   private void log(String message)
840   {
841     System.err.println("## Chimera log: " + message);
842   }
843
844   /**
845    * Ask Chimera to save its session to the given file. Returns true if
846    * successful, else false.
847    * 
848    * @param filepath
849    * @return
850    */
851   public boolean saveSession(String filepath)
852   {
853     if (isChimeraRunning())
854     {
855       /*
856        * Chimera:  https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/save.html
857        * ChimeraX: https://www.cgl.ucsf.edu/chimerax/docs/user/commands/save.html
858        */
859       String command = getCommandGenerator().saveSession(filepath);
860       List<String> reply = chimeraManager.sendChimeraCommand(command, true);
861       if (reply.contains("Session written"))
862       {
863         return true;
864       }
865       else
866       {
867         Cache.log
868                 .error("Error saving Chimera session: " + reply.toString());
869       }
870     }
871     return false;
872   }
873
874   /**
875    * Ask Chimera to open a session file. Returns true if successful, else false.
876    * The filename must have a .py (Chimera) or .cxs (ChimeraX) extension for
877    * this command to work.
878    * 
879    * @param filepath
880    * @return
881    */
882   public boolean openSession(String filepath)
883   {
884     /*
885      * Chimera:  https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/open.html
886      * ChimeraX: https://www.cgl.ucsf.edu/chimerax/docs/user/commands/open.html
887      */
888     executeCommand("open " + filepath, true);
889     // todo: test for failure - how?
890     return true;
891   }
892
893   /**
894    * Send a 'show' command for all atoms in the currently selected columns
895    * 
896    * TODO: pull up to abstract structure viewer interface
897    * 
898    * @param vp
899    */
900   public void highlightSelection(AlignmentViewPanel vp)
901   {
902     List<Integer> cols = vp.getAlignViewport().getColumnSelection()
903             .getSelected();
904     AlignmentI alignment = vp.getAlignment();
905     StructureSelectionManager sm = getSsm();
906     for (SequenceI seq : alignment.getSequences())
907     {
908       /*
909        * convert selected columns into sequence positions
910        */
911       int[] positions = new int[cols.size()];
912       int i = 0;
913       for (Integer col : cols)
914       {
915         positions[i++] = seq.findPosition(col);
916       }
917       sm.highlightStructure(this, seq, positions);
918     }
919   }
920
921   /**
922    * Constructs and send commands to Chimera to set attributes on residues for
923    * features visible in Jalview
924    * 
925    * @param avp
926    * @return
927    */
928   public int sendFeaturesToViewer(AlignmentViewPanel avp)
929   {
930     // TODO refactor as required to pull up to an interface
931     String[] files = getStructureFiles();
932     if (files == null)
933     {
934       return 0;
935     }
936
937     String[] commands = getCommandGenerator()
938             .setAttributesForFeatures(getSsm(), files, getSequence(), avp);
939     if (commands.length > 10)
940     {
941       sendCommandsByFile(commands);
942     }
943     else
944     {
945       for (String command : commands)
946       {
947         sendAsynchronousCommand(command, null);
948       }
949     }
950     return commands.length;
951   }
952
953   /**
954    * Write commands to a temporary file, and send a command to Chimera to open the
955    * file as a commands script. For use when sending a large number of separate
956    * commands would overload the REST interface mechanism.
957    * 
958    * @param commands
959    */
960   protected void sendCommandsByFile(String[] commands)
961   {
962     try
963     {
964       File tmp = File.createTempFile("chim", getCommandFileExtension());
965       tmp.deleteOnExit();
966       PrintWriter out = new PrintWriter(new FileOutputStream(tmp));
967       for (String command : commands)
968       {
969         out.println(command);
970       }
971       out.flush();
972       out.close();
973       String path = tmp.getAbsolutePath();
974       String command = getCommandGenerator().openCommandFile(path);
975       sendAsynchronousCommand(command, null);
976     } catch (IOException e)
977     {
978       System.err.println("Sending commands to Chimera via file failed with "
979               + e.getMessage());
980     }
981   }
982
983   /**
984    * Returns the file extension required for a file of commands to be read by
985    * the structure viewer
986    * @return
987    */
988   protected String getCommandFileExtension()
989   {
990     return ".com";
991   }
992
993   /**
994    * Get Chimera residues which have the named attribute, find the mapped
995    * positions in the Jalview sequence(s), and set as sequence features
996    * 
997    * @param attName
998    * @param alignmentPanel
999    */
1000   public void copyStructureAttributesToFeatures(String attName,
1001           AlignmentViewPanel alignmentPanel)
1002   {
1003     // todo pull up to AAStructureBindingModel (and interface?)
1004
1005     /*
1006      * ask Chimera to list residues with the attribute, reporting its value
1007      */
1008     // this alternative command
1009     // list residues spec ':*/attName' attr attName
1010     // doesn't report 'None' values (which is good), but
1011     // fails for 'average.bfactor' (which is bad):
1012
1013     String cmd = "list residues attr '" + attName + "'";
1014     List<String> residues = executeCommand(cmd, true);
1015
1016     boolean featureAdded = createFeaturesForAttributes(attName, residues);
1017     if (featureAdded)
1018     {
1019       alignmentPanel.getFeatureRenderer().featuresAdded();
1020     }
1021   }
1022
1023   /**
1024    * Create features in Jalview for the given attribute name and structure
1025    * residues.
1026    * 
1027    * <pre>
1028    * The residue list should be 0, 1 or more reply lines of the format: 
1029    *     residue id #0:5.A isHelix -155.000836316 index 5 
1030    * or 
1031    *     residue id #0:6.A isHelix None
1032    * </pre>
1033    * 
1034    * @param attName
1035    * @param residues
1036    * @return
1037    */
1038   protected boolean createFeaturesForAttributes(String attName,
1039           List<String> residues)
1040   {
1041     boolean featureAdded = false;
1042     String featureGroup = getViewerFeatureGroup();
1043     boolean chimeraX = chimeraManager.isChimeraX();
1044
1045     for (String residue : residues)
1046     {
1047       AtomSpec spec = null;
1048       String[] tokens = residue.split(" ");
1049       if (tokens.length < 5)
1050       {
1051         continue;
1052       }
1053       String atomSpec = tokens[2];
1054       String attValue = tokens[4];
1055
1056       /*
1057        * ignore 'None' (e.g. for phi) or 'False' (e.g. for isHelix)
1058        */
1059       if ("None".equalsIgnoreCase(attValue)
1060               || "False".equalsIgnoreCase(attValue))
1061       {
1062         continue;
1063       }
1064
1065       try
1066       {
1067         spec = AtomSpec.fromChimeraAtomspec(atomSpec, chimeraX);
1068       } catch (IllegalArgumentException e)
1069       {
1070         System.err.println("Problem parsing atomspec " + atomSpec);
1071         continue;
1072       }
1073
1074       String chainId = spec.getChain();
1075       String description = attValue;
1076       float score = Float.NaN;
1077       try
1078       {
1079         score = Float.valueOf(attValue);
1080         description = chainId;
1081       } catch (NumberFormatException e)
1082       {
1083         // was not a float value
1084       }
1085
1086       String pdbFile = getPdbFileForModel(spec.getModelNumber());
1087       spec.setPdbFile(pdbFile);
1088
1089       List<AtomSpec> atoms = Collections.singletonList(spec);
1090
1091       /*
1092        * locate the mapped position in the alignment (if any)
1093        */
1094       SearchResultsI sr = getSsm()
1095               .findAlignmentPositionsForStructurePositions(atoms);
1096
1097       /*
1098        * expect one matched alignment position, or none 
1099        * (if the structure position is not mapped)
1100        */
1101       for (SearchResultMatchI m : sr.getResults())
1102       {
1103         SequenceI seq = m.getSequence();
1104         int start = m.getStart();
1105         int end = m.getEnd();
1106         SequenceFeature sf = new SequenceFeature(attName, description,
1107                 start, end, score, featureGroup);
1108         // todo: should SequenceFeature have an explicit property for chain?
1109         // note: repeating the action shouldn't duplicate features
1110         featureAdded |= seq.addSequenceFeature(sf);
1111       }
1112     }
1113     return featureAdded;
1114   }
1115
1116   /**
1117    * Answers the feature group name to apply to features created in Jalview from
1118    * Chimera attributes
1119    * 
1120    * @return
1121    */
1122   protected String getViewerFeatureGroup()
1123   {
1124     // todo pull up to interface
1125     return CHIMERA_FEATURE_GROUP;
1126   }
1127
1128   @Override
1129   public int getModelNoForFile(String pdbFile)
1130   {
1131     List<ChimeraModel> foundModels = chimeraMaps.get(pdbFile);
1132     if (foundModels != null && !foundModels.isEmpty())
1133     {
1134       return foundModels.get(0).getModelNumber();
1135     }
1136     return -1;
1137   }
1138
1139   /**
1140    * Answers a (possibly empty) list of attribute names in Chimera[X], excluding
1141    * any which were added from Jalview
1142    * 
1143    * @return
1144    */
1145   public List<String> getChimeraAttributes()
1146   {
1147     List<String> atts = chimeraManager.getAttrList();
1148     Iterator<String> it = atts.iterator();
1149     while (it.hasNext())
1150     {
1151       if (it.next().startsWith(ChimeraCommands.NAMESPACE_PREFIX))
1152       {
1153         /*
1154          * attribute added from Jalview - exclude it
1155          */
1156         it.remove();
1157       }
1158     }
1159     return atts;
1160   }
1161
1162   /**
1163    * Returns the file extension to use for a saved viewer session file
1164    * 
1165    * @return
1166    */
1167   public String getSessionFileExtension()
1168   {
1169     return ".py";
1170   }
1171
1172   public String getHelpURL()
1173   {
1174     return "https://www.cgl.ucsf.edu/chimera/docs/UsersGuide";
1175   }
1176 }