JAL-2422 pull up refactoring in preparation for ChimeraX subclasses
[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.ColorUtils;
45 import jalview.util.MessageManager;
46
47 import java.awt.Color;
48 import java.io.File;
49 import java.io.FileOutputStream;
50 import java.io.IOException;
51 import java.io.PrintWriter;
52 import java.net.BindException;
53 import java.util.ArrayList;
54 import java.util.BitSet;
55 import java.util.Collections;
56 import java.util.Hashtable;
57 import java.util.Iterator;
58 import java.util.LinkedHashMap;
59 import java.util.List;
60 import java.util.Map;
61
62 import ext.edu.ucsf.rbvi.strucviz2.ChimeraManager;
63 import ext.edu.ucsf.rbvi.strucviz2.ChimeraModel;
64 import ext.edu.ucsf.rbvi.strucviz2.StructureManager;
65 import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType;
66
67 public abstract class JalviewChimeraBinding extends AAStructureBindingModel
68 {
69   public static final String CHIMERA_FEATURE_GROUP = "Chimera";
70
71   // Chimera clause to exclude alternate locations in atom selection
72   private static final String NO_ALTLOCS = "&~@.B-Z&~@.2-9";
73
74   private static final String COLOURING_CHIMERA = MessageManager
75           .getString("status.colouring_chimera");
76
77   private static final boolean debug = false;
78
79   private static final String PHOSPHORUS = "P";
80
81   private static final String ALPHACARBON = "CA";
82
83   private List<String> chainNames = new ArrayList<>();
84
85   private Hashtable<String, String> chainFile = new Hashtable<>();
86
87   /*
88    * Object through which we talk to Chimera
89    */
90   private ChimeraManager chimeraManager;
91
92   /*
93    * Object which listens to Chimera notifications
94    */
95   private AbstractRequestHandler chimeraListener;
96
97   /*
98    * set if chimera state is being restored from some source - instructs binding
99    * not to apply default display style when structure set is updated for first
100    * time.
101    */
102   private boolean loadingFromArchive = false;
103
104   /*
105    * flag to indicate if the Chimera viewer should ignore sequence colouring
106    * events from the structure manager because the GUI is still setting up
107    */
108   private boolean loadingFinished = true;
109
110   /*
111    * Map of ChimeraModel objects keyed by PDB full local file name
112    */
113   private Map<String, List<ChimeraModel>> chimeraMaps = new LinkedHashMap<>();
114
115   String lastHighlightCommand;
116
117   /*
118    * incremented every time a load notification is successfully handled -
119    * lightweight mechanism for other threads to detect when they can start
120    * referring to new structures.
121    */
122   private long loadNotifiesHandled = 0;
123
124   private Thread chimeraMonitor;
125
126   /**
127    * Open a PDB structure file in Chimera and set up mappings from Jalview.
128    * 
129    * We check if the PDB model id is already loaded in Chimera, if so don't reopen
130    * it. This is the case if Chimera has opened a saved session file.
131    * 
132    * @param pe
133    * @return
134    */
135   public boolean openFile(PDBEntry pe)
136   {
137     String file = pe.getFile();
138     try
139     {
140       List<ChimeraModel> modelsToMap = new ArrayList<>();
141       List<ChimeraModel> oldList = chimeraManager.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         chimeraManager.openModel(file, pe.getId(), ModelType.PDB_MODEL);
163         if (chimeraManager.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 = chimeraManager.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     chimeraManager = new ChimeraManager(new StructureManager(true));
223     String viewerType = Cache.getProperty(Preferences.STRUCTURE_DISPLAY);
224     chimeraManager.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 = chimeraManager.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       chimeraManager.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       chimeraManager.exitChimera();
323     }
324     if (this.chimeraListener != null)
325     {
326       chimeraListener.shutdown();
327       chimeraListener = null;
328     }
329     chimeraManager = 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 = chimeraManager.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 = chimeraManager.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 (chimeraManager.isChimeraLaunched())
680     {
681       return true;
682     }
683
684     boolean launched = chimeraManager.launchChimera(
685             StructureManager.getChimeraPaths(chimeraManager.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 chimeraManager.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 (chimeraManager == 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 = chimeraManager.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, chimeraManager.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 (chimeraManager != null && chimeraManager.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 (chimeraManager == 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 = chimeraManager.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       chimeraManager.sendChimeraCommand("~" + lastHighlightCommand, false);
910     }
911     if (found)
912     {
913       chimeraManager.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 = chimeraManager.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 = chimeraManager.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 colorCode ::VAL
1029      * ChimeraX format: color :VAL colourCode
1030      */
1031     boolean chimeraX = chimeraManager.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 = ColorUtils.toTkCode(col);
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 " + ColorUtils.toTkCode(col);
1104     chimeraManager.sendChimeraCommand(command, false);
1105     viewerCommandHistory(true);
1106   }
1107
1108   /**
1109    * Ask Chimera to save its session to the given file. Returns true if
1110    * successful, else false.
1111    * 
1112    * @param filepath
1113    * @return
1114    */
1115   public boolean saveSession(String filepath)
1116   {
1117     if (isChimeraRunning())
1118     {
1119       /*
1120        * Chimera:  https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/save.html
1121        * ChimeraX: https://www.cgl.ucsf.edu/chimerax/docs/user/commands/save.html
1122        */
1123       String command = isChimeraX() ? "save session " : "save ";
1124       List<String> reply = chimeraManager.sendChimeraCommand(command + filepath,
1125               true);
1126       if (reply.contains("Session written"))
1127       {
1128         return true;
1129       }
1130       else
1131       {
1132         Cache.log
1133                 .error("Error saving Chimera session: " + reply.toString());
1134       }
1135     }
1136     return false;
1137   }
1138
1139   /**
1140    * Ask Chimera to open a session file. Returns true if successful, else false.
1141    * The filename must have a .py (Chimera) or .cxs (ChimeraX) extension for
1142    * this command to work.
1143    * 
1144    * @param filepath
1145    * @return
1146    */
1147   public boolean openSession(String filepath)
1148   {
1149     /*
1150      * Chimera:  https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/open.html
1151      * ChimeraX: https://www.cgl.ucsf.edu/chimerax/docs/user/commands/open.html
1152      */
1153     sendChimeraCommand("open " + filepath, true);
1154     // todo: test for failure - how?
1155     return true;
1156   }
1157
1158   /**
1159    * Returns a list of chains mapped in this viewer. Note this list is not
1160    * currently scoped per structure.
1161    * 
1162    * @return
1163    */
1164   @Override
1165   public List<String> getChainNames()
1166   {
1167     return chainNames;
1168   }
1169
1170   /**
1171    * Send a 'focus' command to Chimera to recentre the visible display
1172    */
1173   public void focusView()
1174   {
1175     sendChimeraCommand(chimeraManager.isChimeraX() ? "view" : "focus", false);
1176   }
1177
1178   /**
1179    * Send a 'show' command for all atoms in the currently selected columns
1180    * 
1181    * TODO: pull up to abstract structure viewer interface
1182    * 
1183    * @param vp
1184    */
1185   public void highlightSelection(AlignmentViewPanel vp)
1186   {
1187     List<Integer> cols = vp.getAlignViewport().getColumnSelection()
1188             .getSelected();
1189     AlignmentI alignment = vp.getAlignment();
1190     StructureSelectionManager sm = getSsm();
1191     for (SequenceI seq : alignment.getSequences())
1192     {
1193       /*
1194        * convert selected columns into sequence positions
1195        */
1196       int[] positions = new int[cols.size()];
1197       int i = 0;
1198       for (Integer col : cols)
1199       {
1200         positions[i++] = seq.findPosition(col);
1201       }
1202       sm.highlightStructure(this, seq, positions);
1203     }
1204   }
1205
1206   /**
1207    * Constructs and send commands to Chimera to set attributes on residues for
1208    * features visible in Jalview
1209    * 
1210    * @param avp
1211    * @return
1212    */
1213   public int sendFeaturesToViewer(AlignmentViewPanel avp)
1214   {
1215     // TODO refactor as required to pull up to an interface
1216     AlignmentI alignment = avp.getAlignment();
1217
1218     String[] files = getStructureFiles();
1219     if (files == null)
1220     {
1221       return 0;
1222     }
1223
1224     StructureMappingcommandSet commandSet = ChimeraCommands
1225             .getSetAttributeCommandsForFeatures(getSsm(), files,
1226                     getSequence(), avp, chimeraManager.isChimeraX());
1227     String[] commands = commandSet.commands;
1228     if (commands.length > 10)
1229     {
1230       sendCommandsByFile(commands);
1231     }
1232     else
1233     {
1234       for (String command : commands)
1235       {
1236         sendAsynchronousCommand(command, null);
1237       }
1238     }
1239     return commands.length;
1240   }
1241
1242   /**
1243    * Write commands to a temporary file, and send a command to Chimera to open the
1244    * file as a commands script. For use when sending a large number of separate
1245    * commands would overload the REST interface mechanism.
1246    * 
1247    * @param commands
1248    */
1249   protected void sendCommandsByFile(String[] commands)
1250   {
1251     boolean toChimeraX = chimeraManager.isChimeraX();
1252     try
1253     {
1254       File tmp = File.createTempFile("chim", toChimeraX ? ".cxc" : ".com");
1255       tmp.deleteOnExit();
1256       PrintWriter out = new PrintWriter(new FileOutputStream(tmp));
1257       for (String command : commands)
1258       {
1259         out.println(command);
1260       }
1261       out.flush();
1262       out.close();
1263       String path = tmp.getAbsolutePath();
1264       String command = "open " + (toChimeraX ? "" : "cmd:") + path;
1265       sendAsynchronousCommand(command, null);
1266     } catch (IOException e)
1267     {
1268       System.err.println("Sending commands to Chimera via file failed with "
1269               + e.getMessage());
1270     }
1271   }
1272
1273   /**
1274    * Get Chimera residues which have the named attribute, find the mapped
1275    * positions in the Jalview sequence(s), and set as sequence features
1276    * 
1277    * @param attName
1278    * @param alignmentPanel
1279    */
1280   public void copyStructureAttributesToFeatures(String attName,
1281           AlignmentViewPanel alignmentPanel)
1282   {
1283     // todo pull up to AAStructureBindingModel (and interface?)
1284
1285     /*
1286      * ask Chimera to list residues with the attribute, reporting its value
1287      */
1288     // this alternative command
1289     // list residues spec ':*/attName' attr attName
1290     // doesn't report 'None' values (which is good), but
1291     // fails for 'average.bfactor' (which is bad):
1292
1293     String cmd = "list residues attr '" + attName + "'";
1294     List<String> residues = sendChimeraCommand(cmd, true);
1295
1296     boolean featureAdded = createFeaturesForAttributes(attName, residues);
1297     if (featureAdded)
1298     {
1299       alignmentPanel.getFeatureRenderer().featuresAdded();
1300     }
1301   }
1302
1303   /**
1304    * Create features in Jalview for the given attribute name and structure
1305    * residues.
1306    * 
1307    * <pre>
1308    * The residue list should be 0, 1 or more reply lines of the format: 
1309    *     residue id #0:5.A isHelix -155.000836316 index 5 
1310    * or 
1311    *     residue id #0:6.A isHelix None
1312    * </pre>
1313    * 
1314    * @param attName
1315    * @param residues
1316    * @return
1317    */
1318   protected boolean createFeaturesForAttributes(String attName,
1319           List<String> residues)
1320   {
1321     boolean featureAdded = false;
1322     String featureGroup = getViewerFeatureGroup();
1323     boolean chimeraX = chimeraManager.isChimeraX();
1324
1325     for (String residue : residues)
1326     {
1327       AtomSpec spec = null;
1328       String[] tokens = residue.split(" ");
1329       if (tokens.length < 5)
1330       {
1331         continue;
1332       }
1333       String atomSpec = tokens[2];
1334       String attValue = tokens[4];
1335
1336       /*
1337        * ignore 'None' (e.g. for phi) or 'False' (e.g. for isHelix)
1338        */
1339       if ("None".equalsIgnoreCase(attValue)
1340               || "False".equalsIgnoreCase(attValue))
1341       {
1342         continue;
1343       }
1344
1345       try
1346       {
1347         spec = AtomSpec.fromChimeraAtomspec(atomSpec, chimeraX);
1348       } catch (IllegalArgumentException e)
1349       {
1350         System.err.println("Problem parsing atomspec " + atomSpec);
1351         continue;
1352       }
1353
1354       String chainId = spec.getChain();
1355       String description = attValue;
1356       float score = Float.NaN;
1357       try
1358       {
1359         score = Float.valueOf(attValue);
1360         description = chainId;
1361       } catch (NumberFormatException e)
1362       {
1363         // was not a float value
1364       }
1365
1366       String pdbFile = getPdbFileForModel(spec.getModelNumber());
1367       spec.setPdbFile(pdbFile);
1368
1369       List<AtomSpec> atoms = Collections.singletonList(spec);
1370
1371       /*
1372        * locate the mapped position in the alignment (if any)
1373        */
1374       SearchResultsI sr = getSsm()
1375               .findAlignmentPositionsForStructurePositions(atoms);
1376
1377       /*
1378        * expect one matched alignment position, or none 
1379        * (if the structure position is not mapped)
1380        */
1381       for (SearchResultMatchI m : sr.getResults())
1382       {
1383         SequenceI seq = m.getSequence();
1384         int start = m.getStart();
1385         int end = m.getEnd();
1386         SequenceFeature sf = new SequenceFeature(attName, description,
1387                 start, end, score, featureGroup);
1388         // todo: should SequenceFeature have an explicit property for chain?
1389         // note: repeating the action shouldn't duplicate features
1390         featureAdded |= seq.addSequenceFeature(sf);
1391       }
1392     }
1393     return featureAdded;
1394   }
1395
1396   /**
1397    * Answers the feature group name to apply to features created in Jalview from
1398    * Chimera attributes
1399    * 
1400    * @return
1401    */
1402   protected String getViewerFeatureGroup()
1403   {
1404     // todo pull up to interface
1405     return CHIMERA_FEATURE_GROUP;
1406   }
1407
1408   public Hashtable<String, String> getChainFile()
1409   {
1410     return chainFile;
1411   }
1412
1413   public List<ChimeraModel> getChimeraModelByChain(String chain)
1414   {
1415     return chimeraMaps.get(chainFile.get(chain));
1416   }
1417
1418   public int getModelNoForChain(String chain)
1419   {
1420     List<ChimeraModel> foundModels = getChimeraModelByChain(chain);
1421     if (foundModels != null && !foundModels.isEmpty())
1422     {
1423       return foundModels.get(0).getModelNumber();
1424     }
1425     return -1;
1426   }
1427
1428   /**
1429    * Answers a (possibly empty) list of attribute names in Chimera[X], excluding
1430    * any which were added from Jalview
1431    * 
1432    * @return
1433    */
1434   public List<String> getChimeraAttributes()
1435   {
1436     List<String> atts = chimeraManager.getAttrList();
1437     Iterator<String> it = atts.iterator();
1438     while (it.hasNext())
1439     {
1440       if (it.next().startsWith(ChimeraCommands.NAMESPACE_PREFIX))
1441       {
1442         /*
1443          * attribute added from Jalview - exclude it
1444          */
1445         it.remove();
1446       }
1447     }
1448     return atts;
1449   }
1450
1451   public boolean isChimeraX()
1452   {
1453     return chimeraManager.isChimeraX();
1454   }
1455 }