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