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