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