01deea8d3646d1557e785191a87a3a9a2d86a3b2
[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     boolean forChimeraX = viewer.isChimeraX();
792     StringBuilder cmd = new StringBuilder(128);
793     boolean first = true;
794     boolean found = false;
795
796     for (AtomSpec atom : atoms)
797     {
798       int pdbResNum = atom.getPdbResNum();
799       String chain = atom.getChain();
800       String pdbfile = atom.getPdbFile();
801       List<ChimeraModel> cms = chimeraMaps.get(pdbfile);
802       if (cms != null && !cms.isEmpty())
803       {
804         if (first)
805         {
806           cmd.append(forChimeraX ? "label #" : "rlabel #");
807         }
808         else
809         {
810           cmd.append(",");
811         }
812         first = false;
813         if (forChimeraX)
814         {
815           cmd.append(cms.get(0).getModelNumber())
816                   .append("/").append(chain).append(":").append(pdbResNum);
817         }
818         else
819         {
820           cmd.append(cms.get(0).getModelNumber())
821                   .append(":").append(pdbResNum);
822           if (!chain.equals(" ") && !forChimeraX)
823           {
824             cmd.append(".").append(chain);
825           }
826         }
827         found = true;
828       }
829     }
830     String command = cmd.toString();
831
832     /*
833      * avoid repeated commands for the same residue
834      */
835     if (command.equals(lastHighlightCommand))
836     {
837       return;
838     }
839
840     /*
841      * unshow the label for the previous residue
842      */
843     if (lastHighlightCommand != null)
844     {
845       viewer.sendChimeraCommand("~" + lastHighlightCommand, false);
846     }
847     if (found)
848     {
849       viewer.sendChimeraCommand(command, false);
850     }
851     this.lastHighlightCommand = command;
852   }
853
854   /**
855    * Query Chimera for its current selection, and highlight it on the alignment
856    */
857   public void highlightChimeraSelection()
858   {
859     /*
860      * Ask Chimera for its current selection
861      */
862     List<String> selection = viewer.getSelectedResidueSpecs();
863
864     /*
865      * Parse model number, residue and chain for each selected position,
866      * formatted as #0:123.A or #1.2:87.B (#model.submodel:residue.chain)
867      */
868     List<AtomSpec> atomSpecs = convertStructureResiduesToAlignment(
869             selection);
870
871     /*
872      * Broadcast the selection (which may be empty, if the user just cleared all
873      * selections)
874      */
875     getSsm().mouseOverStructure(atomSpecs);
876   }
877
878   /**
879    * Converts a list of Chimera atomspecs to a list of AtomSpec representing the
880    * corresponding residues (if any) in Jalview
881    * 
882    * @param structureSelection
883    * @return
884    */
885   protected List<AtomSpec> convertStructureResiduesToAlignment(
886           List<String> structureSelection)
887   {
888     List<AtomSpec> atomSpecs = new ArrayList<>();
889     for (String atomSpec : structureSelection)
890     {
891       try
892       {
893         AtomSpec spec = AtomSpec.fromChimeraAtomspec(atomSpec);
894         String pdbfilename = getPdbFileForModel(spec.getModelNumber());
895         spec.setPdbFile(pdbfilename);
896         atomSpecs.add(spec);
897       } catch (IllegalArgumentException e)
898       {
899         System.err.println("Failed to parse atomspec: " + atomSpec);
900       }
901     }
902     return atomSpecs;
903   }
904
905   /**
906    * @param modelId
907    * @return
908    */
909   protected String getPdbFileForModel(int modelId)
910   {
911     /*
912      * Work out the pdbfilename from the model number
913      */
914     String pdbfilename = modelFileNames[0];
915     findfileloop: for (String pdbfile : this.chimeraMaps.keySet())
916     {
917       for (ChimeraModel cm : chimeraMaps.get(pdbfile))
918       {
919         if (cm.getModelNumber() == modelId)
920         {
921           pdbfilename = pdbfile;
922           break findfileloop;
923         }
924       }
925     }
926     return pdbfilename;
927   }
928
929   private void log(String message)
930   {
931     System.err.println("## Chimera log: " + message);
932   }
933
934   private void viewerCommandHistory(boolean enable)
935   {
936     // log("(Not yet implemented) History "
937     // + ((debug || enable) ? "on" : "off"));
938   }
939
940   public long getLoadNotifiesHandled()
941   {
942     return loadNotifiesHandled;
943   }
944
945   @Override
946   public void setJalviewColourScheme(ColourSchemeI cs)
947   {
948     colourBySequence = false;
949
950     if (cs == null)
951     {
952       return;
953     }
954
955     // Chimera expects RBG values in the range 0-1
956     final double normalise = 255D;
957     viewerCommandHistory(false);
958     StringBuilder command = new StringBuilder(128);
959
960     List<String> residueSet = ResidueProperties.getResidues(isNucleotide(),
961             false);
962     for (String resName : residueSet)
963     {
964       char res = resName.length() == 3
965               ? ResidueProperties.getSingleCharacterCode(resName)
966               : resName.charAt(0);
967       Color col = cs.findColour(res, 0, null, null, 0f);
968       command.append("color " + col.getRed() / normalise + ","
969               + col.getGreen() / normalise + "," + col.getBlue() / normalise
970               + " ::" + resName + ";");
971     }
972
973     sendAsynchronousCommand(command.toString(), COLOURING_CHIMERA);
974     viewerCommandHistory(true);
975   }
976
977   /**
978    * called when the binding thinks the UI needs to be refreshed after a Chimera
979    * state change. this could be because structures were loaded, or because an
980    * error has occurred.
981    */
982   public abstract void refreshGUI();
983
984   @Override
985   public void setLoadingFromArchive(boolean loadingFromArchive)
986   {
987     this.loadingFromArchive = loadingFromArchive;
988   }
989
990   /**
991    * 
992    * @return true if Chimeral is still restoring state or loading is still going
993    *         on (see setFinsihedLoadingFromArchive)
994    */
995   @Override
996   public boolean isLoadingFromArchive()
997   {
998     return loadingFromArchive && !loadingFinished;
999   }
1000
1001   /**
1002    * modify flag which controls if sequence colouring events are honoured by the
1003    * binding. Should be true for normal operation
1004    * 
1005    * @param finishedLoading
1006    */
1007   @Override
1008   public void setFinishedLoadingFromArchive(boolean finishedLoading)
1009   {
1010     loadingFinished = finishedLoading;
1011   }
1012
1013   /**
1014    * Send the Chimera 'background solid <color>" command.
1015    * 
1016    * @see https
1017    *      ://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/background
1018    *      .html
1019    * @param col
1020    */
1021   @Override
1022   public void setBackgroundColour(Color col)
1023   {
1024     viewerCommandHistory(false);
1025     double normalise = 255D;
1026     final String command = "background solid " + col.getRed() / normalise
1027             + "," + col.getGreen() / normalise + ","
1028             + col.getBlue() / normalise + ";";
1029     viewer.sendChimeraCommand(command, false);
1030     viewerCommandHistory(true);
1031   }
1032
1033   /**
1034    * Ask Chimera to save its session to the given file. Returns true if
1035    * successful, else false.
1036    * 
1037    * @param filepath
1038    * @return
1039    */
1040   public boolean saveSession(String filepath)
1041   {
1042     if (isChimeraRunning())
1043     {
1044       List<String> reply = viewer.sendChimeraCommand("save " + filepath,
1045               true);
1046       if (reply.contains("Session written"))
1047       {
1048         return true;
1049       }
1050       else
1051       {
1052         Cache.log
1053                 .error("Error saving Chimera session: " + reply.toString());
1054       }
1055     }
1056     return false;
1057   }
1058
1059   /**
1060    * Ask Chimera to open a session file. Returns true if successful, else false.
1061    * The filename must have a .py extension for this command to work.
1062    * 
1063    * @param filepath
1064    * @return
1065    */
1066   public boolean openSession(String filepath)
1067   {
1068     sendChimeraCommand("open " + filepath, true);
1069     // todo: test for failure - how?
1070     return true;
1071   }
1072
1073   /**
1074    * Returns a list of chains mapped in this viewer. Note this list is not
1075    * currently scoped per structure.
1076    * 
1077    * @return
1078    */
1079   @Override
1080   public List<String> getChainNames()
1081   {
1082     return chainNames;
1083   }
1084
1085   /**
1086    * Send a 'focus' command to Chimera to recentre the visible display
1087    */
1088   public void focusView()
1089   {
1090     sendChimeraCommand("focus", false);
1091   }
1092
1093   /**
1094    * Send a 'show' command for all atoms in the currently selected columns
1095    * 
1096    * TODO: pull up to abstract structure viewer interface
1097    * 
1098    * @param vp
1099    */
1100   public void highlightSelection(AlignmentViewPanel vp)
1101   {
1102     List<Integer> cols = vp.getAlignViewport().getColumnSelection()
1103             .getSelected();
1104     AlignmentI alignment = vp.getAlignment();
1105     StructureSelectionManager sm = getSsm();
1106     for (SequenceI seq : alignment.getSequences())
1107     {
1108       /*
1109        * convert selected columns into sequence positions
1110        */
1111       int[] positions = new int[cols.size()];
1112       int i = 0;
1113       for (Integer col : cols)
1114       {
1115         positions[i++] = seq.findPosition(col);
1116       }
1117       sm.highlightStructure(this, seq, positions);
1118     }
1119   }
1120
1121   /**
1122    * Constructs and send commands to Chimera to set attributes on residues for
1123    * features visible in Jalview
1124    * 
1125    * @param avp
1126    * @return
1127    */
1128   public int sendFeaturesToViewer(AlignmentViewPanel avp)
1129   {
1130     // TODO refactor as required to pull up to an interface
1131     AlignmentI alignment = avp.getAlignment();
1132
1133     String[] files = getStructureFiles();
1134     if (files == null)
1135     {
1136       return 0;
1137     }
1138
1139     StructureMappingcommandSet commandSet = ChimeraCommands
1140             .getSetAttributeCommandsForFeatures(getSsm(), files,
1141                     getSequence(), avp, viewer.isChimeraX());
1142     String[] commands = commandSet.commands;
1143     if (commands.length > 10)
1144     {
1145       sendCommandsByFile(commands);
1146     }
1147     else
1148     {
1149       for (String command : commands)
1150       {
1151         sendAsynchronousCommand(command, null);
1152       }
1153     }
1154     return commands.length;
1155   }
1156
1157   /**
1158    * Write commands to a temporary file, and send a command to Chimera to open the
1159    * file as a commands script. For use when sending a large number of separate
1160    * commands would overload the REST interface mechanism.
1161    * 
1162    * @param commands
1163    */
1164   protected void sendCommandsByFile(String[] commands)
1165   {
1166     boolean toChimeraX = viewer.isChimeraX();
1167     try
1168     {
1169       File tmp = File.createTempFile("chim", toChimeraX ? ".cxc" : ".com");
1170       tmp.deleteOnExit();
1171       PrintWriter out = new PrintWriter(new FileOutputStream(tmp));
1172       for (String command : commands)
1173       {
1174         out.println(command);
1175       }
1176       out.flush();
1177       out.close();
1178       String path = tmp.getAbsolutePath();
1179       String command = "open " + (toChimeraX ? "" : "cmd:") + path;
1180       sendAsynchronousCommand(command, null);
1181     } catch (IOException e)
1182     {
1183       System.err.println("Sending commands to Chimera via file failed with "
1184               + e.getMessage());
1185     }
1186   }
1187
1188   /**
1189    * Get Chimera residues which have the named attribute, find the mapped
1190    * positions in the Jalview sequence(s), and set as sequence features
1191    * 
1192    * @param attName
1193    * @param alignmentPanel
1194    */
1195   public void copyStructureAttributesToFeatures(String attName,
1196           AlignmentViewPanel alignmentPanel)
1197   {
1198     // todo pull up to AAStructureBindingModel (and interface?)
1199
1200     /*
1201      * ask Chimera to list residues with the attribute, reporting its value
1202      */
1203     // this alternative command
1204     // list residues spec ':*/attName' attr attName
1205     // doesn't report 'None' values (which is good), but
1206     // fails for 'average.bfactor' (which is bad):
1207
1208     String cmd = "list residues attr '" + attName + "'";
1209     List<String> residues = sendChimeraCommand(cmd, true);
1210
1211     boolean featureAdded = createFeaturesForAttributes(attName, residues);
1212     if (featureAdded)
1213     {
1214       alignmentPanel.getFeatureRenderer().featuresAdded();
1215     }
1216   }
1217
1218   /**
1219    * Create features in Jalview for the given attribute name and structure
1220    * residues.
1221    * 
1222    * <pre>
1223    * The residue list should be 0, 1 or more reply lines of the format: 
1224    *     residue id #0:5.A isHelix -155.000836316 index 5 
1225    * or 
1226    *     residue id #0:6.A isHelix None
1227    * </pre>
1228    * 
1229    * @param attName
1230    * @param residues
1231    * @return
1232    */
1233   protected boolean createFeaturesForAttributes(String attName,
1234           List<String> residues)
1235   {
1236     boolean featureAdded = false;
1237     String featureGroup = getViewerFeatureGroup();
1238
1239     for (String residue : residues)
1240     {
1241       AtomSpec spec = null;
1242       String[] tokens = residue.split(" ");
1243       if (tokens.length < 5)
1244       {
1245         continue;
1246       }
1247       String atomSpec = tokens[2];
1248       String attValue = tokens[4];
1249
1250       /*
1251        * ignore 'None' (e.g. for phi) or 'False' (e.g. for isHelix)
1252        */
1253       if ("None".equalsIgnoreCase(attValue)
1254               || "False".equalsIgnoreCase(attValue))
1255       {
1256         continue;
1257       }
1258
1259       try
1260       {
1261         spec = AtomSpec.fromChimeraAtomspec(atomSpec);
1262       } catch (IllegalArgumentException e)
1263       {
1264         System.err.println("Problem parsing atomspec " + atomSpec);
1265         continue;
1266       }
1267
1268       String chainId = spec.getChain();
1269       String description = attValue;
1270       float score = Float.NaN;
1271       try
1272       {
1273         score = Float.valueOf(attValue);
1274         description = chainId;
1275       } catch (NumberFormatException e)
1276       {
1277         // was not a float value
1278       }
1279
1280       String pdbFile = getPdbFileForModel(spec.getModelNumber());
1281       spec.setPdbFile(pdbFile);
1282
1283       List<AtomSpec> atoms = Collections.singletonList(spec);
1284
1285       /*
1286        * locate the mapped position in the alignment (if any)
1287        */
1288       SearchResultsI sr = getSsm()
1289               .findAlignmentPositionsForStructurePositions(atoms);
1290
1291       /*
1292        * expect one matched alignment position, or none 
1293        * (if the structure position is not mapped)
1294        */
1295       for (SearchResultMatchI m : sr.getResults())
1296       {
1297         SequenceI seq = m.getSequence();
1298         int start = m.getStart();
1299         int end = m.getEnd();
1300         SequenceFeature sf = new SequenceFeature(attName, description,
1301                 start, end, score, featureGroup);
1302         // todo: should SequenceFeature have an explicit property for chain?
1303         // note: repeating the action shouldn't duplicate features
1304         featureAdded |= seq.addSequenceFeature(sf);
1305       }
1306     }
1307     return featureAdded;
1308   }
1309
1310   /**
1311    * Answers the feature group name to apply to features created in Jalview from
1312    * Chimera attributes
1313    * 
1314    * @return
1315    */
1316   protected String getViewerFeatureGroup()
1317   {
1318     // todo pull up to interface
1319     return CHIMERA_FEATURE_GROUP;
1320   }
1321
1322   public Hashtable<String, String> getChainFile()
1323   {
1324     return chainFile;
1325   }
1326
1327   public List<ChimeraModel> getChimeraModelByChain(String chain)
1328   {
1329     return chimeraMaps.get(chainFile.get(chain));
1330   }
1331
1332   public int getModelNoForChain(String chain)
1333   {
1334     List<ChimeraModel> foundModels = getChimeraModelByChain(chain);
1335     if (foundModels != null && !foundModels.isEmpty())
1336     {
1337       return foundModels.get(0).getModelNumber();
1338     }
1339     return -1;
1340   }
1341 }