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