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