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