JAL-2422 general pull-up/removal of common or unused fields and methods
[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   // End StructureListener
677   // //////////////////////////
678
679   /**
680    * map between index of model filename returned from getPdbFile and the first
681    * index of models from this file in the viewer. Note - this is not trimmed -
682    * use getPdbFile to get number of unique models.
683    */
684   private int _modelFileNameMap[];
685
686   // ////////////////////////////////
687   // /StructureListener
688   @Override
689   public synchronized String[] getStructureFiles()
690   {
691     if (chimeraManager == null)
692     {
693       return new String[0];
694     }
695
696     return chimeraMaps.keySet()
697             .toArray(modelFileNames = new String[chimeraMaps.size()]);
698   }
699
700   /**
701    * Construct and send a command to highlight zero, one or more atoms. We do this
702    * by sending an "rlabel" command to show the residue label at that position.
703    */
704   @Override
705   public void highlightAtoms(List<AtomSpec> atoms)
706   {
707     if (atoms == null || atoms.size() == 0)
708     {
709       return;
710     }
711
712     boolean forChimeraX = chimeraManager.isChimeraX();
713     StringBuilder cmd = new StringBuilder(128);
714     boolean first = true;
715     boolean found = false;
716
717     for (AtomSpec atom : atoms)
718     {
719       int pdbResNum = atom.getPdbResNum();
720       String chain = atom.getChain();
721       String pdbfile = atom.getPdbFile();
722       List<ChimeraModel> cms = chimeraMaps.get(pdbfile);
723       if (cms != null && !cms.isEmpty())
724       {
725         if (first)
726         {
727           cmd.append(forChimeraX ? "label #" : "rlabel #");
728         }
729         else
730         {
731           cmd.append(",");
732         }
733         first = false;
734         if (forChimeraX)
735         {
736           cmd.append(cms.get(0).getModelNumber())
737                   .append("/").append(chain).append(":").append(pdbResNum);
738         }
739         else
740         {
741           cmd.append(cms.get(0).getModelNumber())
742                   .append(":").append(pdbResNum);
743           if (!chain.equals(" ") && !forChimeraX)
744           {
745             cmd.append(".").append(chain);
746           }
747         }
748         found = true;
749       }
750     }
751     String command = cmd.toString();
752
753     /*
754      * avoid repeated commands for the same residue
755      */
756     if (command.equals(lastHighlightCommand))
757     {
758       return;
759     }
760
761     /*
762      * unshow the label for the previous residue
763      */
764     if (lastHighlightCommand != null)
765     {
766       chimeraManager.sendChimeraCommand("~" + lastHighlightCommand, false);
767     }
768     if (found)
769     {
770       chimeraManager.sendChimeraCommand(command, false);
771     }
772     this.lastHighlightCommand = command;
773   }
774
775   /**
776    * Query Chimera for its current selection, and highlight it on the alignment
777    */
778   public void highlightChimeraSelection()
779   {
780     /*
781      * Ask Chimera for its current selection
782      */
783     List<String> selection = chimeraManager.getSelectedResidueSpecs();
784
785     /*
786      * Parse model number, residue and chain for each selected position,
787      * formatted as #0:123.A or #1.2:87.B (#model.submodel:residue.chain)
788      */
789     List<AtomSpec> atomSpecs = convertStructureResiduesToAlignment(
790             selection);
791
792     /*
793      * Broadcast the selection (which may be empty, if the user just cleared all
794      * selections)
795      */
796     getSsm().mouseOverStructure(atomSpecs);
797   }
798
799   /**
800    * Converts a list of Chimera atomspecs to a list of AtomSpec representing the
801    * corresponding residues (if any) in Jalview
802    * 
803    * @param structureSelection
804    * @return
805    */
806   protected List<AtomSpec> convertStructureResiduesToAlignment(
807           List<String> structureSelection)
808   {
809     boolean chimeraX = chimeraManager.isChimeraX();
810     List<AtomSpec> atomSpecs = new ArrayList<>();
811     for (String atomSpec : structureSelection)
812     {
813       try
814       {
815         AtomSpec spec = AtomSpec.fromChimeraAtomspec(atomSpec, chimeraX);
816         String pdbfilename = getPdbFileForModel(spec.getModelNumber());
817         spec.setPdbFile(pdbfilename);
818         atomSpecs.add(spec);
819       } catch (IllegalArgumentException e)
820       {
821         System.err.println("Failed to parse atomspec: " + atomSpec);
822       }
823     }
824     return atomSpecs;
825   }
826
827   /**
828    * @param modelId
829    * @return
830    */
831   protected String getPdbFileForModel(int modelId)
832   {
833     /*
834      * Work out the pdbfilename from the model number
835      */
836     String pdbfilename = modelFileNames[0];
837     findfileloop: for (String pdbfile : this.chimeraMaps.keySet())
838     {
839       for (ChimeraModel cm : chimeraMaps.get(pdbfile))
840       {
841         if (cm.getModelNumber() == modelId)
842         {
843           pdbfilename = pdbfile;
844           break findfileloop;
845         }
846       }
847     }
848     return pdbfilename;
849   }
850
851   private void log(String message)
852   {
853     System.err.println("## Chimera log: " + message);
854   }
855
856   /**
857    * Ask Chimera to save its session to the given file. Returns true if
858    * successful, else false.
859    * 
860    * @param filepath
861    * @return
862    */
863   public boolean saveSession(String filepath)
864   {
865     if (isChimeraRunning())
866     {
867       /*
868        * Chimera:  https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/save.html
869        * ChimeraX: https://www.cgl.ucsf.edu/chimerax/docs/user/commands/save.html
870        */
871       String command = isChimeraX() ? "save session " : "save ";
872       List<String> reply = chimeraManager.sendChimeraCommand(command + filepath,
873               true);
874       if (reply.contains("Session written"))
875       {
876         return true;
877       }
878       else
879       {
880         Cache.log
881                 .error("Error saving Chimera session: " + reply.toString());
882       }
883     }
884     return false;
885   }
886
887   /**
888    * Ask Chimera to open a session file. Returns true if successful, else false.
889    * The filename must have a .py (Chimera) or .cxs (ChimeraX) extension for
890    * this command to work.
891    * 
892    * @param filepath
893    * @return
894    */
895   public boolean openSession(String filepath)
896   {
897     /*
898      * Chimera:  https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/open.html
899      * ChimeraX: https://www.cgl.ucsf.edu/chimerax/docs/user/commands/open.html
900      */
901     executeCommand("open " + filepath, true);
902     // todo: test for failure - how?
903     return true;
904   }
905
906   /**
907    * Send a 'show' command for all atoms in the currently selected columns
908    * 
909    * TODO: pull up to abstract structure viewer interface
910    * 
911    * @param vp
912    */
913   public void highlightSelection(AlignmentViewPanel vp)
914   {
915     List<Integer> cols = vp.getAlignViewport().getColumnSelection()
916             .getSelected();
917     AlignmentI alignment = vp.getAlignment();
918     StructureSelectionManager sm = getSsm();
919     for (SequenceI seq : alignment.getSequences())
920     {
921       /*
922        * convert selected columns into sequence positions
923        */
924       int[] positions = new int[cols.size()];
925       int i = 0;
926       for (Integer col : cols)
927       {
928         positions[i++] = seq.findPosition(col);
929       }
930       sm.highlightStructure(this, seq, positions);
931     }
932   }
933
934   /**
935    * Constructs and send commands to Chimera to set attributes on residues for
936    * features visible in Jalview
937    * 
938    * @param avp
939    * @return
940    */
941   public int sendFeaturesToViewer(AlignmentViewPanel avp)
942   {
943     // TODO refactor as required to pull up to an interface
944     AlignmentI alignment = avp.getAlignment();
945
946     String[] files = getStructureFiles();
947     if (files == null)
948     {
949       return 0;
950     }
951
952     StructureMappingcommandSet commandSet = ChimeraCommands
953             .getSetAttributeCommandsForFeatures(getSsm(), files,
954                     getSequence(), avp, chimeraManager.isChimeraX());
955     String[] commands = commandSet.commands;
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     boolean toChimeraX = chimeraManager.isChimeraX();
980     try
981     {
982       File tmp = File.createTempFile("chim", toChimeraX ? ".cxc" : ".com");
983       tmp.deleteOnExit();
984       PrintWriter out = new PrintWriter(new FileOutputStream(tmp));
985       for (String command : commands)
986       {
987         out.println(command);
988       }
989       out.flush();
990       out.close();
991       String path = tmp.getAbsolutePath();
992       String command = "open " + (toChimeraX ? "" : "cmd:") + path;
993       sendAsynchronousCommand(command, null);
994     } catch (IOException e)
995     {
996       System.err.println("Sending commands to Chimera via file failed with "
997               + e.getMessage());
998     }
999   }
1000
1001   /**
1002    * Get Chimera residues which have the named attribute, find the mapped
1003    * positions in the Jalview sequence(s), and set as sequence features
1004    * 
1005    * @param attName
1006    * @param alignmentPanel
1007    */
1008   public void copyStructureAttributesToFeatures(String attName,
1009           AlignmentViewPanel alignmentPanel)
1010   {
1011     // todo pull up to AAStructureBindingModel (and interface?)
1012
1013     /*
1014      * ask Chimera to list residues with the attribute, reporting its value
1015      */
1016     // this alternative command
1017     // list residues spec ':*/attName' attr attName
1018     // doesn't report 'None' values (which is good), but
1019     // fails for 'average.bfactor' (which is bad):
1020
1021     String cmd = "list residues attr '" + attName + "'";
1022     List<String> residues = executeCommand(cmd, true);
1023
1024     boolean featureAdded = createFeaturesForAttributes(attName, residues);
1025     if (featureAdded)
1026     {
1027       alignmentPanel.getFeatureRenderer().featuresAdded();
1028     }
1029   }
1030
1031   /**
1032    * Create features in Jalview for the given attribute name and structure
1033    * residues.
1034    * 
1035    * <pre>
1036    * The residue list should be 0, 1 or more reply lines of the format: 
1037    *     residue id #0:5.A isHelix -155.000836316 index 5 
1038    * or 
1039    *     residue id #0:6.A isHelix None
1040    * </pre>
1041    * 
1042    * @param attName
1043    * @param residues
1044    * @return
1045    */
1046   protected boolean createFeaturesForAttributes(String attName,
1047           List<String> residues)
1048   {
1049     boolean featureAdded = false;
1050     String featureGroup = getViewerFeatureGroup();
1051     boolean chimeraX = chimeraManager.isChimeraX();
1052
1053     for (String residue : residues)
1054     {
1055       AtomSpec spec = null;
1056       String[] tokens = residue.split(" ");
1057       if (tokens.length < 5)
1058       {
1059         continue;
1060       }
1061       String atomSpec = tokens[2];
1062       String attValue = tokens[4];
1063
1064       /*
1065        * ignore 'None' (e.g. for phi) or 'False' (e.g. for isHelix)
1066        */
1067       if ("None".equalsIgnoreCase(attValue)
1068               || "False".equalsIgnoreCase(attValue))
1069       {
1070         continue;
1071       }
1072
1073       try
1074       {
1075         spec = AtomSpec.fromChimeraAtomspec(atomSpec, chimeraX);
1076       } catch (IllegalArgumentException e)
1077       {
1078         System.err.println("Problem parsing atomspec " + atomSpec);
1079         continue;
1080       }
1081
1082       String chainId = spec.getChain();
1083       String description = attValue;
1084       float score = Float.NaN;
1085       try
1086       {
1087         score = Float.valueOf(attValue);
1088         description = chainId;
1089       } catch (NumberFormatException e)
1090       {
1091         // was not a float value
1092       }
1093
1094       String pdbFile = getPdbFileForModel(spec.getModelNumber());
1095       spec.setPdbFile(pdbFile);
1096
1097       List<AtomSpec> atoms = Collections.singletonList(spec);
1098
1099       /*
1100        * locate the mapped position in the alignment (if any)
1101        */
1102       SearchResultsI sr = getSsm()
1103               .findAlignmentPositionsForStructurePositions(atoms);
1104
1105       /*
1106        * expect one matched alignment position, or none 
1107        * (if the structure position is not mapped)
1108        */
1109       for (SearchResultMatchI m : sr.getResults())
1110       {
1111         SequenceI seq = m.getSequence();
1112         int start = m.getStart();
1113         int end = m.getEnd();
1114         SequenceFeature sf = new SequenceFeature(attName, description,
1115                 start, end, score, featureGroup);
1116         // todo: should SequenceFeature have an explicit property for chain?
1117         // note: repeating the action shouldn't duplicate features
1118         featureAdded |= seq.addSequenceFeature(sf);
1119       }
1120     }
1121     return featureAdded;
1122   }
1123
1124   /**
1125    * Answers the feature group name to apply to features created in Jalview from
1126    * Chimera attributes
1127    * 
1128    * @return
1129    */
1130   protected String getViewerFeatureGroup()
1131   {
1132     // todo pull up to interface
1133     return CHIMERA_FEATURE_GROUP;
1134   }
1135
1136   @Override
1137   public int getModelNoForFile(String pdbFile)
1138   {
1139     List<ChimeraModel> foundModels = chimeraMaps.get(pdbFile);
1140     if (foundModels != null && !foundModels.isEmpty())
1141     {
1142       return foundModels.get(0).getModelNumber();
1143     }
1144     return -1;
1145   }
1146
1147   /**
1148    * Answers a (possibly empty) list of attribute names in Chimera[X], excluding
1149    * any which were added from Jalview
1150    * 
1151    * @return
1152    */
1153   public List<String> getChimeraAttributes()
1154   {
1155     List<String> atts = chimeraManager.getAttrList();
1156     Iterator<String> it = atts.iterator();
1157     while (it.hasNext())
1158     {
1159       if (it.next().startsWith(ChimeraCommands.NAMESPACE_PREFIX))
1160       {
1161         /*
1162          * attribute added from Jalview - exclude it
1163          */
1164         it.remove();
1165       }
1166     }
1167     return atts;
1168   }
1169
1170   public boolean isChimeraX()
1171   {
1172     return chimeraManager.isChimeraX();
1173   }
1174 }