f511a6551378fc51917ec42552f3a9066953aff8
[jalview.git] / src / jalview / ext / rbvi / chimera / JalviewChimeraBinding.java
1 package jalview.ext.rbvi.chimera;
2
3 import jalview.api.AlignmentViewPanel;
4 import jalview.api.FeatureRenderer;
5 import jalview.api.SequenceRenderer;
6 import jalview.bin.Cache;
7 import jalview.datamodel.AlignmentI;
8 import jalview.datamodel.ColumnSelection;
9 import jalview.datamodel.PDBEntry;
10 import jalview.datamodel.SequenceI;
11 import jalview.schemes.ColourSchemeI;
12 import jalview.schemes.ResidueProperties;
13 import jalview.structure.StructureMapping;
14 import jalview.structure.StructureMappingcommandSet;
15 import jalview.structure.StructureSelectionManager;
16 import jalview.structures.models.AAStructureBindingModel;
17 import jalview.util.Comparison;
18 import jalview.util.MessageManager;
19
20 import java.awt.Color;
21 import java.util.ArrayList;
22 import java.util.HashMap;
23 import java.util.LinkedHashMap;
24 import java.util.List;
25 import java.util.Map;
26
27 import ext.edu.ucsf.rbvi.strucviz2.ChimeraManager;
28 import ext.edu.ucsf.rbvi.strucviz2.ChimeraModel;
29 import ext.edu.ucsf.rbvi.strucviz2.StructureManager;
30 import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType;
31
32 public abstract class JalviewChimeraBinding extends AAStructureBindingModel
33 {
34
35   private static final boolean debug = false;
36
37   private static final String PHOSPHORUS = "P";
38
39   private static final String ALPHACARBON = "CA";
40
41   private StructureManager csm;
42
43   private ChimeraManager viewer;
44
45   /*
46    * set if chimera state is being restored from some source - instructs binding
47    * not to apply default display style when structure set is updated for first
48    * time.
49    */
50   private boolean loadingFromArchive = false;
51
52   /*
53    * flag to indicate if the Chimera viewer should ignore sequence colouring
54    * events from the structure manager because the GUI is still setting up
55    */
56   private boolean loadingFinished = true;
57
58   /*
59    * state flag used to check if the Chimera viewer's paint method can be called
60    */
61   private boolean finishedInit = false;
62
63   private List<String> atomsPicked = new ArrayList<String>();
64
65   private List<String> chainNames;
66
67   private Map<String, String> chainFile;
68
69   private StringBuffer eval = new StringBuffer();
70
71   public String fileLoadingError;
72
73   /*
74    * Map of ChimeraModel objects keyed by PDB full local file name
75    */
76   private Map<String, List<ChimeraModel>> chimeraMaps = new LinkedHashMap<String, List<ChimeraModel>>();
77
78   /*
79    * the default or current model displayed if the model cannot be identified
80    * from the selection message
81    */
82   private int frameNo = 0;
83
84   private String lastCommand;
85
86   private String lastMessage;
87
88   private boolean loadedInline;
89
90   /**
91    * Open a PDB structure file in Chimera and set up mappings from Jalview.
92    * 
93    * We check if the PDB model id is already loaded in Chimera, if so don't
94    * reopen it. This is the case if Chimera has opened a saved session file.
95    * 
96    * @param pe
97    * @return
98    */
99   public boolean openFile(PDBEntry pe)
100   {
101     String file = pe.getFile();
102     try
103     {
104       List<ChimeraModel> modelsToMap = new ArrayList<ChimeraModel>();
105       List<ChimeraModel> oldList = viewer.getModelList();
106       boolean alreadyOpen = false;
107
108       /*
109        * If Chimera already has this model, don't reopen it, but do remap it.
110        */
111       for (ChimeraModel open : oldList)
112       {
113         if (open.getModelName().equals(pe.getId()))
114         {
115           alreadyOpen = true;
116           modelsToMap.add(open);
117         }
118       }
119
120       /*
121        * If Chimera doesn't yet have this model, ask it to open it, and retrieve
122        * the model names added by Chimera.
123        */
124       if (!alreadyOpen)
125       {
126         viewer.openModel(file, pe.getId(), ModelType.PDB_MODEL);
127         modelsToMap = viewer.getModelList();
128         modelsToMap.removeAll(oldList);
129       }
130
131       chimeraMaps.put(file, modelsToMap);
132
133       if (getSsm() != null)
134       {
135         getSsm().addStructureViewerListener(this);
136         // ssm.addSelectionListener(this);
137         FeatureRenderer fr = getFeatureRenderer(null);
138         if (fr != null)
139         {
140           fr.featuresAdded();
141         }
142         refreshGUI();
143       }
144       return true;
145     } catch (Exception q)
146     {
147       log("Exception when trying to open model " + file + "\n"
148               + q.toString());
149       q.printStackTrace();
150     }
151     return false;
152   }
153
154   /**
155    * current set of model filenames loaded
156    */
157   String[] modelFileNames = null;
158
159
160   StringBuffer resetLastRes = new StringBuffer();
161
162   private List<String> lastReply;
163
164   /**
165    * Constructor
166    * 
167    * @param ssm
168    * @param pdbentry
169    * @param sequenceIs
170    * @param chains
171    * @param protocol
172    */
173   public JalviewChimeraBinding(StructureSelectionManager ssm,
174           PDBEntry[] pdbentry, SequenceI[][] sequenceIs, String[][] chains,
175           String protocol)
176   {
177     super(ssm, pdbentry, sequenceIs, chains, protocol);
178     viewer = new ChimeraManager(
179             csm = new ext.edu.ucsf.rbvi.strucviz2.StructureManager(true));
180   }
181
182   /**
183    * Constructor
184    * 
185    * @param ssm
186    * @param theViewer
187    */
188   public JalviewChimeraBinding(StructureSelectionManager ssm,
189           ChimeraManager theViewer)
190   {
191     super(ssm, null);
192     viewer = theViewer;
193     csm = viewer.getStructureManager();
194   }
195
196   /**
197    * Construct a title string for the viewer window based on the data Jalview
198    * knows about
199    * 
200    * @param verbose
201    * @return
202    */
203   public String getViewerTitle(boolean verbose)
204   {
205     return getViewerTitle("Chimera", verbose);
206   }
207
208   /**
209    * prepare the view for a given set of models/chains. chainList contains
210    * strings of the form 'pdbfilename:Chaincode'
211    * 
212    * @param toshow
213    *          list of chains to make visible
214    */
215   public void centerViewer(List<String> toshow)
216   {
217     StringBuilder cmd = new StringBuilder(64);
218     int mlength, p;
219     for (String lbl : toshow)
220     {
221       mlength = 0;
222       do
223       {
224         p = mlength;
225         mlength = lbl.indexOf(":", p);
226       } while (p < mlength && mlength < (lbl.length() - 2));
227       // TODO: lookup each pdb id and recover proper model number for it.
228       cmd.append("#" + getModelNum(chainFile.get(lbl)) + "."
229               + lbl.substring(mlength + 1) + " or ");
230     }
231     if (cmd.length() > 0)
232     {
233       cmd.setLength(cmd.length() - 4);
234     }
235     String cmdstring = cmd.toString();
236     evalStateCommand("~display #*; ~ribbon #*; ribbon " + cmdstring
237             + ";focus " + cmdstring, false);
238   }
239
240   /**
241    * Close down the Jalview viewer, and (optionally) the associated Chimera
242    * window.
243    */
244   public void closeViewer(boolean closeChimera)
245   {
246     getSsm().removeStructureViewerListener(this, this.getPdbFile());
247     if (closeChimera)
248     {
249       viewer.exitChimera();
250     }
251     lastCommand = null;
252     viewer = null;
253     releaseUIResources();
254   }
255
256   public void colourByChain()
257   {
258     colourBySequence = false;
259     evalStateCommand("rainbow chain", false);
260   }
261
262   public void colourByCharge()
263   {
264     colourBySequence = false;
265     evalStateCommand(
266             "color white;color red ::ASP;color red ::GLU;color blue ::LYS;color blue ::ARG;color yellow ::CYS",
267             false);
268   }
269
270   /**
271    * superpose the structures associated with sequences in the alignment
272    * according to their corresponding positions.
273    */
274   public void superposeStructures(AlignmentI alignment)
275   {
276     superposeStructures(alignment, -1, null);
277   }
278
279   /**
280    * superpose the structures associated with sequences in the alignment
281    * according to their corresponding positions. ded)
282    * 
283    * @param refStructure
284    *          - select which pdb file to use as reference (default is -1 - the
285    *          first structure in the alignment)
286    */
287   public void superposeStructures(AlignmentI alignment, int refStructure)
288   {
289     superposeStructures(alignment, refStructure, null);
290   }
291
292   /**
293    * superpose the structures associated with sequences in the alignment
294    * according to their corresponding positions. ded)
295    * 
296    * @param refStructure
297    *          - select which pdb file to use as reference (default is -1 - the
298    *          first structure in the alignment)
299    * @param hiddenCols
300    *          TODO
301    */
302   public void superposeStructures(AlignmentI alignment, int refStructure,
303           ColumnSelection hiddenCols)
304   {
305     superposeStructures(new AlignmentI[]
306     { alignment }, new int[]
307     { refStructure }, new ColumnSelection[]
308     { hiddenCols });
309   }
310
311   public void superposeStructures(AlignmentI[] _alignment,
312           int[] _refStructure, ColumnSelection[] _hiddenCols)
313   {
314     assert (_alignment.length == _refStructure.length && _alignment.length != _hiddenCols.length);
315     StringBuilder allComs = new StringBuilder(128); // Chimera superposition cmd
316     String[] files = getPdbFile();
317     // check to see if we are still waiting for Chimera files
318     long starttime = System.currentTimeMillis();
319     boolean waiting = true;
320     do
321     {
322       waiting = false;
323       for (String file : files)
324       {
325         try
326         {
327           // HACK - in Jalview 2.8 this call may not be threadsafe so we catch
328           // every possible exception
329           StructureMapping[] sm = getSsm().getMapping(file);
330           if (sm == null || sm.length == 0)
331           {
332             waiting = true;
333           }
334         } catch (Exception x)
335         {
336           waiting = true;
337         } catch (Error q)
338         {
339           waiting = true;
340         }
341       }
342       // we wait around for a reasonable time before we give up
343     } while (waiting
344             && System.currentTimeMillis() < (10000 + 1000 * files.length + starttime));
345     if (waiting)
346     {
347       System.err
348               .println("RUNTIME PROBLEM: Chimera seems to be taking a long time to process all the structures.");
349       return;
350     }
351     refreshPdbEntries();
352     StringBuffer selectioncom = new StringBuffer();
353     for (int a = 0; a < _alignment.length; a++)
354     {
355       int refStructure = _refStructure[a];
356       AlignmentI alignment = _alignment[a];
357       ColumnSelection hiddenCols = _hiddenCols[a];
358       if (a > 0
359               && selectioncom.length() > 0
360               && !selectioncom.substring(selectioncom.length() - 1).equals(
361                       " "))
362       {
363         selectioncom.append(" ");
364       }
365       // process this alignment
366       if (refStructure >= files.length)
367       {
368         System.err.println("Invalid reference structure value "
369                 + refStructure);
370         refStructure = -1;
371       }
372       if (refStructure < -1)
373       {
374         refStructure = -1;
375       }
376
377       boolean matched[] = new boolean[alignment.getWidth()];
378       for (int m = 0; m < matched.length; m++)
379       {
380
381         matched[m] = (hiddenCols != null) ? hiddenCols.isVisible(m) : true;
382       }
383
384       int commonrpositions[][] = new int[files.length][alignment.getWidth()];
385       String isel[] = new String[files.length];
386       String[] targetC = new String[files.length];
387       String[] chainNames = new String[files.length];
388       String[] atomSpec = new String[files.length];
389       for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
390       {
391         StructureMapping[] mapping = getSsm().getMapping(files[pdbfnum]);
392         // RACE CONDITION - getMapping only returns Jmol loaded filenames once
393         // Jmol callback has completed.
394         if (mapping == null || mapping.length < 1)
395         {
396           throw new Error(MessageManager.getString("error.implementation_error_chimera_getting_data"));
397         }
398         int lastPos = -1;
399         final int seqCountForPdbFile = getSequence()[pdbfnum].length;
400         for (int s = 0; s < seqCountForPdbFile; s++)
401         {
402           for (int sp, m = 0; m < mapping.length; m++)
403           {
404             final SequenceI theSequence = getSequence()[pdbfnum][s];
405             if (mapping[m].getSequence() == theSequence
406                     && (sp = alignment.findIndex(theSequence)) > -1)
407             {
408               if (refStructure == -1)
409               {
410                 refStructure = pdbfnum;
411               }
412               SequenceI asp = alignment.getSequenceAt(sp);
413               for (int r = 0; r < matched.length; r++)
414               {
415                 if (!matched[r])
416                 {
417                   continue;
418                 }
419                 matched[r] = false; // assume this is not a good site
420                 if (r >= asp.getLength())
421                 {
422                   continue;
423                 }
424
425                 if (Comparison.isGap(asp.getCharAt(r)))
426                 {
427                   // no mapping to gaps in sequence
428                   continue;
429                 }
430                 int t = asp.findPosition(r); // sequence position
431                 int apos = mapping[m].getAtomNum(t);
432                 int pos = mapping[m].getPDBResNum(t);
433
434                 if (pos < 1 || pos == lastPos)
435                 {
436                   // can't align unmapped sequence
437                   continue;
438                 }
439                 matched[r] = true; // this is a good ite
440                 lastPos = pos;
441                 // just record this residue position
442                 commonrpositions[pdbfnum][r] = pos;
443               }
444               // create model selection suffix
445               isel[pdbfnum] = "#" + pdbfnum;
446               if (mapping[m].getChain() == null
447                       || mapping[m].getChain().trim().length() == 0)
448               {
449                 targetC[pdbfnum] = "";
450               }
451               else
452               {
453                 targetC[pdbfnum] = "." + mapping[m].getChain();
454               }
455               chainNames[pdbfnum] = mapping[m].getPdbId()
456                       + targetC[pdbfnum];
457               atomSpec[pdbfnum] = asp.getRNA() != null ? PHOSPHORUS : ALPHACARBON;
458               // move on to next pdb file
459               s = seqCountForPdbFile;
460               break;
461             }
462           }
463         }
464       }
465
466       // TODO: consider bailing if nmatched less than 4 because superposition
467       // not
468       // well defined.
469       // TODO: refactor superposable position search (above) from jmol selection
470       // construction (below)
471
472       String[] selcom = new String[files.length];
473       int nmatched = 0;
474       String sep = "";
475       // generate select statements to select regions to superimpose structures
476       {
477         for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
478         {
479           String chainCd = targetC[pdbfnum];
480           int lpos = -1;
481           boolean run = false;
482           StringBuffer molsel = new StringBuffer();
483           for (int r = 0; r < matched.length; r++)
484           {
485             if (matched[r])
486             {
487               if (pdbfnum == 0)
488               {
489                 nmatched++;
490               }
491               if (lpos != commonrpositions[pdbfnum][r] - 1)
492               {
493                 // discontinuity
494                 if (lpos != -1)
495                 {
496                   molsel.append((run ? "" : ":") + lpos);
497                   molsel.append(chainCd);
498                   molsel.append(",");
499                 }
500               }
501               else
502               {
503                 // continuous run - and lpos >-1
504                 if (!run)
505                 {
506                   // at the beginning, so add dash
507                   molsel.append(":" + lpos);
508                   molsel.append("-");
509                 }
510                 run = true;
511               }
512               lpos = commonrpositions[pdbfnum][r];
513               // molsel.append(lpos);
514             }
515           }
516           // add final selection phrase
517           if (lpos != -1)
518           {
519             molsel.append((run ? "" : ":") + lpos);
520             molsel.append(chainCd);
521             // molsel.append("");
522           }
523           if (molsel.length() > 1)
524           {
525             selcom[pdbfnum] = molsel.toString();
526             selectioncom.append("#" + pdbfnum);
527             selectioncom.append(selcom[pdbfnum]);
528             selectioncom.append(" ");
529             if (pdbfnum < files.length - 1)
530             {
531               selectioncom.append("| ");
532             }
533           }
534           else
535           {
536             selcom[pdbfnum] = null;
537           }
538         }
539       }
540       StringBuilder command = new StringBuilder(256);
541       for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
542       {
543         if (pdbfnum == refStructure || selcom[pdbfnum] == null
544                 || selcom[refStructure] == null)
545         {
546           continue;
547         }
548         if (command.length() > 0)
549         {
550           command.append(";");
551         }
552
553         /*
554          * Form Chimera match command, from the 'new' structure to the
555          * 'reference' structure e.g. (residues 1-91, chain B/A, alphacarbons):
556          * 
557          * match #1:1-91.B@CA #0:1-91.A@CA
558          * 
559          * @see
560          * https://www.cgl.ucsf.edu/chimera/docs/UsersGuide/midas/match.html
561          */
562         command.append("match #" + pdbfnum /* +".1" */);
563         // TODO: handle sub-models
564         command.append(selcom[pdbfnum]);
565         command.append("@" + atomSpec[pdbfnum]);
566         command.append(" #" + refStructure /* +".1" */);
567         command.append(selcom[refStructure]);
568         command.append("@" + atomSpec[refStructure]);
569       }
570       if (selectioncom.length() > 0)
571       {
572         if (debug)
573         {
574           System.out.println("Select regions:\n" + selectioncom.toString());
575           System.out.println("Superimpose command(s):\n"
576                   + command.toString());
577         }
578         allComs.append("~display all; chain @CA|P; ribbon "
579                 + selectioncom.toString() + ";"+command.toString());
580         // selcom.append("; ribbons; ");
581       }
582     }
583     if (selectioncom.length() > 0)
584     {// finally, mark all regions that were superposed.
585       if (selectioncom.substring(selectioncom.length() - 1).equals("|"))
586       {
587         selectioncom.setLength(selectioncom.length() - 1);
588       }
589       if (debug)
590       {
591         System.out.println("Select regions:\n" + selectioncom.toString());
592       }
593       allComs.append("; ~display all; chain @CA|P; ribbon "
594               + selectioncom.toString() + "; focus");
595       // evalStateCommand("select *; backbone; select "+selcom.toString()+"; cartoons; center "+selcom.toString());
596       evalStateCommand(allComs.toString(), true /* false */);
597     }
598     
599   }
600
601   private void checkLaunched()
602   {
603     if (!viewer.isChimeraLaunched())
604     {
605       viewer.launchChimera(StructureManager.getChimeraPaths());
606     }
607     if (!viewer.isChimeraLaunched())
608     {
609       log("Failed to launch Chimera!");
610     }
611   }
612
613   /**
614    * Answers true if the Chimera process is still running, false if ended or not
615    * started.
616    * 
617    * @return
618    */
619   public boolean isChimeraRunning()
620   {
621     return viewer.isChimeraLaunched();
622   }
623
624   /**
625    * Send a command to Chimera, launching it first if necessary, and optionally
626    * log any responses.
627    * 
628    * @param command
629    * @param logResponse
630    */
631   public void evalStateCommand(final String command, boolean logResponse)
632   {
633     viewerCommandHistory(false);
634     checkLaunched();
635     if (lastCommand == null || !lastCommand.equals(command))
636     {
637       // trim command or it may never find a match in the replyLog!!
638       lastReply = viewer.sendChimeraCommand(command.trim(), logResponse);
639       if (debug && logResponse)
640       {
641         log("Response from command ('" + command + "') was:\n" + lastReply);
642       }
643     }
644     viewerCommandHistory(true);
645     lastCommand = command;
646   }
647
648   /**
649    * colour any structures associated with sequences in the given alignment
650    * using the getFeatureRenderer() and getSequenceRenderer() renderers but only
651    * if colourBySequence is enabled.
652    */
653   public void colourBySequence(boolean showFeatures,
654           jalview.api.AlignmentViewPanel alignmentv)
655   {
656     if (!colourBySequence || !loadingFinished)
657     {
658       return;
659     }
660     if (getSsm() == null)
661     {
662       return;
663     }
664     String[] files = getPdbFile();
665
666     SequenceRenderer sr = getSequenceRenderer(alignmentv);
667
668     FeatureRenderer fr = null;
669     if (showFeatures)
670     {
671       fr = getFeatureRenderer(alignmentv);
672     }
673     AlignmentI alignment = alignmentv.getAlignment();
674
675     for (jalview.structure.StructureMappingcommandSet cpdbbyseq : getColourBySequenceCommands(
676             files, sr, fr, alignment))
677     {
678       for (String command : cpdbbyseq.commands)
679       {
680         executeWhenReady(command);
681       }
682     }
683   }
684
685   /**
686    * @param files
687    * @param sr
688    * @param fr
689    * @param alignment
690    * @return
691    */
692   protected StructureMappingcommandSet[] getColourBySequenceCommands(
693           String[] files, SequenceRenderer sr, FeatureRenderer fr,
694           AlignmentI alignment)
695   {
696     return ChimeraCommands.getColourBySequenceCommand(getSsm(), files,
697             getSequence(), sr, fr, alignment);
698   }
699
700   /**
701    * @param command
702    */
703   protected void executeWhenReady(String command)
704   {
705     waitForChimera();
706     evalStateCommand(command, false);
707     waitForChimera();
708   }
709
710   private void waitForChimera()
711   {
712     while (viewer != null && viewer.isBusy())
713     {
714       try {
715         Thread.sleep(15);
716       } catch (InterruptedException q)
717       {}
718     }
719   }
720
721
722
723   // End StructureListener
724   // //////////////////////////
725
726   public Color getColour(int atomIndex, int pdbResNum, String chain,
727           String pdbfile)
728   {
729     if (getModelNum(pdbfile) < 0)
730     {
731       return null;
732     }
733     log("get model / residue colour attribute unimplemented");
734     return null;
735   }
736
737   /**
738    * returns the current featureRenderer that should be used to colour the
739    * structures
740    * 
741    * @param alignment
742    * 
743    * @return
744    */
745   public abstract FeatureRenderer getFeatureRenderer(
746           AlignmentViewPanel alignment);
747
748   /**
749    * instruct the Jalview binding to update the pdbentries vector if necessary
750    * prior to matching the jmol view's contents to the list of structure files
751    * Jalview knows about.
752    */
753   public abstract void refreshPdbEntries();
754
755   private int getModelNum(String modelFileName)
756   {
757     String[] mfn = getPdbFile();
758     if (mfn == null)
759     {
760       return -1;
761     }
762     for (int i = 0; i < mfn.length; i++)
763     {
764       if (mfn[i].equalsIgnoreCase(modelFileName))
765       {
766         return i;
767       }
768     }
769     return -1;
770   }
771
772   /**
773    * map between index of model filename returned from getPdbFile and the first
774    * index of models from this file in the viewer. Note - this is not trimmed -
775    * use getPdbFile to get number of unique models.
776    */
777   private int _modelFileNameMap[];
778
779   // ////////////////////////////////
780   // /StructureListener
781   public synchronized String[] getPdbFile()
782   {
783     if (viewer == null)
784     {
785       return new String[0];
786     }
787     // if (modelFileNames == null)
788     // {
789     // Collection<ChimeraModel> chimodels = viewer.getChimeraModels();
790     // _modelFileNameMap = new int[chimodels.size()];
791     // int j = 0;
792     // for (ChimeraModel chimodel : chimodels)
793     // {
794     // String mdlName = chimodel.getModelName();
795     // }
796     // modelFileNames = new String[j];
797     // // System.arraycopy(mset, 0, modelFileNames, 0, j);
798     // }
799
800     return chimeraMaps.keySet().toArray(
801             modelFileNames = new String[chimeraMaps.size()]);
802   }
803
804   /**
805    * map from string to applet
806    */
807   public Map getRegistryInfo()
808   {
809     // TODO Auto-generated method stub
810     return null;
811   }
812
813   /**
814    * returns the current sequenceRenderer that should be used to colour the
815    * structures
816    * 
817    * @param alignment
818    * 
819    * @return
820    */
821   public abstract SequenceRenderer getSequenceRenderer(
822           AlignmentViewPanel alignment);
823
824   // jmol/ssm only
825   public void highlightAtom(int atomIndex, int pdbResNum, String chain,
826           String pdbfile)
827   {
828     List<ChimeraModel> cms = chimeraMaps.get(pdbfile);
829     if (cms != null)
830     {
831       int mdlNum = cms.get(0).getModelNumber();
832
833       viewerCommandHistory(false);
834       // viewer.stopListening();
835       if (resetLastRes.length() > 0)
836       {
837         eval.setLength(0);
838         eval.append(resetLastRes.toString() + ";");
839       }
840
841       eval.append("display "); // +modelNum
842
843       resetLastRes.setLength(0);
844       resetLastRes.append("~display ");
845       {
846         eval.append(" #" + (mdlNum));
847         resetLastRes.append(" #" + (mdlNum));
848       }
849       // complete select string
850
851       eval.append(":" + pdbResNum);
852       resetLastRes.append(":" + pdbResNum);
853       if (!chain.equals(" "))
854       {
855         eval.append("." + chain);
856         resetLastRes.append("." + chain);
857       }
858
859       viewer.sendChimeraCommand(eval.toString(), false);
860       viewerCommandHistory(true);
861       // viewer.startListening();
862     }
863   }
864
865   private void log(String message)
866   {
867     System.err.println("## Chimera log: " + message);
868   }
869
870   private void viewerCommandHistory(boolean enable)
871   {
872     // log("(Not yet implemented) History "
873     // + ((debug || enable) ? "on" : "off"));
874   }
875
876   public void loadInline(String string)
877   {
878     loadedInline = true;
879     // TODO: re JAL-623
880     // viewer.loadInline(strModel, isAppend);
881     // could do this:
882     // construct fake fullPathName and fileName so we can identify the file
883     // later.
884     // Then, construct pass a reader for the string to Jmol.
885     // ((org.jmol.Viewer.Viewer) viewer).loadModelFromFile(fullPathName,
886     // fileName, null, reader, false, null, null, 0);
887     // viewer.openStringInline(string);
888     log("cannot load inline in Chimera, yet");
889   }
890
891   public void mouseOverStructure(int atomIndex, String strInfo)
892   {
893     // function to parse a mouseOver event from Chimera
894     //
895     int pdbResNum;
896     int alocsep = strInfo.indexOf("^");
897     int mdlSep = strInfo.indexOf("/");
898     int chainSeparator = strInfo.indexOf(":"), chainSeparator1 = -1;
899
900     if (chainSeparator == -1)
901     {
902       chainSeparator = strInfo.indexOf(".");
903       if (mdlSep > -1 && mdlSep < chainSeparator)
904       {
905         chainSeparator1 = chainSeparator;
906         chainSeparator = mdlSep;
907       }
908     }
909     // handle insertion codes
910     if (alocsep != -1)
911     {
912       pdbResNum = Integer.parseInt(strInfo.substring(
913               strInfo.indexOf("]") + 1, alocsep));
914
915     }
916     else
917     {
918       pdbResNum = Integer.parseInt(strInfo.substring(
919               strInfo.indexOf("]") + 1, chainSeparator));
920     }
921     String chainId;
922
923     if (strInfo.indexOf(":") > -1)
924     {
925       chainId = strInfo.substring(strInfo.indexOf(":") + 1,
926               strInfo.indexOf("."));
927     }
928     else
929     {
930       chainId = " ";
931     }
932
933     String pdbfilename = modelFileNames[frameNo]; // default is first or current
934     // model
935     if (mdlSep > -1)
936     {
937       if (chainSeparator1 == -1)
938       {
939         chainSeparator1 = strInfo.indexOf(".", mdlSep);
940       }
941       String mdlId = (chainSeparator1 > -1) ? strInfo.substring(mdlSep + 1,
942               chainSeparator1) : strInfo.substring(mdlSep + 1);
943       try
944       {
945         // recover PDB filename for the model hovered over.
946         int _mp = _modelFileNameMap.length - 1, mnumber = new Integer(mdlId)
947                 .intValue() - 1;
948         while (mnumber < _modelFileNameMap[_mp])
949         {
950           _mp--;
951         }
952         pdbfilename = modelFileNames[_mp];
953         if (pdbfilename == null)
954         {
955           // pdbfilename = new File(viewer.getModelFileName(mnumber))
956           // .getAbsolutePath();
957         }
958
959       } catch (Exception e)
960       {
961       }
962       ;
963     }
964     if (lastMessage == null || !lastMessage.equals(strInfo))
965     {
966       getSsm().mouseOverStructure(pdbResNum, chainId, pdbfilename);
967     }
968
969     lastMessage = strInfo;
970   }
971
972   public void notifyAtomPicked(int atomIndex, String strInfo, String strData)
973   {
974     /**
975      * this implements the toggle label behaviour copied from the original
976      * structure viewer, MCView
977      */
978     if (strData != null)
979     {
980       System.err.println("Ignoring additional pick data string " + strData);
981     }
982     // rewrite these selections for chimera (DNA, RNA and protein)
983     int chainSeparator = strInfo.indexOf(":");
984     int p = 0;
985     if (chainSeparator == -1)
986     {
987       chainSeparator = strInfo.indexOf(".");
988     }
989
990     String picked = strInfo.substring(strInfo.indexOf("]") + 1,
991             chainSeparator);
992     String mdlString = "";
993     if ((p = strInfo.indexOf(":")) > -1)
994     {
995       picked += strInfo.substring(p + 1, strInfo.indexOf("."));
996     }
997
998     if ((p = strInfo.indexOf("/")) > -1)
999     {
1000       mdlString += strInfo.substring(p, strInfo.indexOf(" #"));
1001     }
1002     picked = "((" + picked + ".CA" + mdlString + ")|(" + picked + ".P"
1003             + mdlString + "))";
1004     viewerCommandHistory(false);
1005
1006     if (!atomsPicked.contains(picked))
1007     {
1008       viewer.select(picked);
1009       atomsPicked.add(picked);
1010     }
1011     else
1012     {
1013       viewer.select("not " + picked);
1014       atomsPicked.remove(picked);
1015     }
1016     viewerCommandHistory(true);
1017     // TODO: in application this happens
1018     //
1019     // if (scriptWindow != null)
1020     // {
1021     // scriptWindow.sendConsoleMessage(strInfo);
1022     // scriptWindow.sendConsoleMessage("\n");
1023     // }
1024
1025   }
1026
1027   // incremented every time a load notification is successfully handled -
1028   // lightweight mechanism for other threads to detect when they can start
1029   // referring to new structures.
1030   private long loadNotifiesHandled = 0;
1031
1032   public long getLoadNotifiesHandled()
1033   {
1034     return loadNotifiesHandled;
1035   }
1036
1037   public void notifyFileLoaded(String fullPathName, String fileName2,
1038           String modelName, String errorMsg, int modelParts)
1039   {
1040     if (errorMsg != null)
1041     {
1042       fileLoadingError = errorMsg;
1043       refreshGUI();
1044       return;
1045     }
1046     // TODO: deal sensibly with models loaded inLine:
1047     // modelName will be null, as will fullPathName.
1048
1049     // the rest of this routine ignores the arguments, and simply interrogates
1050     // the Jmol view to find out what structures it contains, and adds them to
1051     // the structure selection manager.
1052     fileLoadingError = null;
1053     String[] oldmodels = modelFileNames;
1054     modelFileNames = null;
1055     chainNames = new ArrayList<String>();
1056     chainFile = new HashMap<String, String>();
1057     boolean notifyLoaded = false;
1058     String[] modelfilenames = getPdbFile();
1059     // first check if we've lost any structures
1060     if (oldmodels != null && oldmodels.length > 0)
1061     {
1062       int oldm = 0;
1063       for (int i = 0; i < oldmodels.length; i++)
1064       {
1065         for (int n = 0; n < modelfilenames.length; n++)
1066         {
1067           if (modelfilenames[n] == oldmodels[i])
1068           {
1069             oldmodels[i] = null;
1070             break;
1071           }
1072         }
1073         if (oldmodels[i] != null)
1074         {
1075           oldm++;
1076         }
1077       }
1078       if (oldm > 0)
1079       {
1080         String[] oldmfn = new String[oldm];
1081         oldm = 0;
1082         for (int i = 0; i < oldmodels.length; i++)
1083         {
1084           if (oldmodels[i] != null)
1085           {
1086             oldmfn[oldm++] = oldmodels[i];
1087           }
1088         }
1089         // deregister the Jmol instance for these structures - we'll add
1090         // ourselves again at the end for the current structure set.
1091         getSsm().removeStructureViewerListener(this, oldmfn);
1092       }
1093     }
1094
1095     // register ourselves as a listener and notify the gui that it needs to
1096     // update itself.
1097     getSsm().addStructureViewerListener(this);
1098
1099     if (notifyLoaded)
1100     {
1101       FeatureRenderer fr = getFeatureRenderer(null);
1102       if (fr != null)
1103       {
1104         fr.featuresAdded();
1105       }
1106       refreshGUI();
1107       loadNotifiesHandled++;
1108     }
1109     setLoadingFromArchive(false);
1110   }
1111
1112   public void setJalviewColourScheme(ColourSchemeI cs)
1113   {
1114     colourBySequence = false;
1115
1116     if (cs == null)
1117     {
1118       return;
1119     }
1120
1121     int index;
1122     Color col;
1123     // Chimera expects RBG values in the range 0-1
1124     final double normalise = 255D;
1125     viewerCommandHistory(false);
1126     // TODO: Switch between nucleotide or aa selection expressions
1127     StringBuilder command = new StringBuilder(128);
1128     command.append("color white;");
1129     for (String res : ResidueProperties.aa3Hash.keySet())
1130     {
1131       index = ResidueProperties.aa3Hash.get(res).intValue();
1132       if (index > 20)
1133       {
1134         continue;
1135       }
1136
1137       col = cs.findColour(ResidueProperties.aa[index].charAt(0));
1138       command.append("color " + col.getRed() / normalise + ","
1139               + col.getGreen() / normalise + "," + col.getBlue()
1140               / normalise + " ::" + res + ";");
1141     }
1142
1143     evalStateCommand(command.toString(),false);
1144     viewerCommandHistory(true);
1145   }
1146
1147   /**
1148    * called when the binding thinks the UI needs to be refreshed after a Chimera
1149    * state change. this could be because structures were loaded, or because an
1150    * error has occurred.
1151    */
1152   public abstract void refreshGUI();
1153
1154   public void setLoadingFromArchive(boolean loadingFromArchive)
1155   {
1156     this.loadingFromArchive = loadingFromArchive;
1157   }
1158
1159   /**
1160    * 
1161    * @return true if Chimeral is still restoring state or loading is still going
1162    *         on (see setFinsihedLoadingFromArchive)
1163    */
1164   public boolean isLoadingFromArchive()
1165   {
1166     return loadingFromArchive && !loadingFinished;
1167   }
1168
1169   /**
1170    * modify flag which controls if sequence colouring events are honoured by the
1171    * binding. Should be true for normal operation
1172    * 
1173    * @param finishedLoading
1174    */
1175   public void setFinishedLoadingFromArchive(boolean finishedLoading)
1176   {
1177     loadingFinished = finishedLoading;
1178   }
1179
1180   /**
1181    * Send the Chimera 'background solid <color>" command.
1182    * 
1183    * @see https
1184    *      ://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/background
1185    *      .html
1186    * @param col
1187    */
1188   public void setBackgroundColour(Color col)
1189   {
1190     viewerCommandHistory(false);
1191     double normalise = 255D;
1192     final String command = "background solid " + col.getRed() / normalise + ","
1193             + col.getGreen() / normalise + "," + col.getBlue()
1194             / normalise + ";";
1195     viewer.sendChimeraCommand(command, false);
1196     viewerCommandHistory(true);
1197   }
1198
1199   /**
1200    * 
1201    * @param pdbfile
1202    * @return text report of alignment between pdbfile and any associated
1203    *         alignment sequences
1204    */
1205   public String printMapping(String pdbfile)
1206   {
1207     return getSsm().printMapping(pdbfile);
1208   }
1209
1210   /**
1211    * Ask Chimera to save its session to the given file. Returns true if
1212    * successful, else false.
1213    * 
1214    * @param filepath
1215    * @return
1216    */
1217   public boolean saveSession(String filepath)
1218   {
1219     if (isChimeraRunning())
1220     {
1221       List<String> reply = viewer.sendChimeraCommand("save " + filepath,
1222               true);
1223       if (reply.contains("Session written"))
1224       {
1225         return true;
1226       }
1227       else
1228       {
1229         Cache.log
1230                 .error("Error saving Chimera session: " + reply.toString());
1231       }
1232     }
1233     return false;
1234   }
1235
1236   /**
1237    * Ask Chimera to open a session file. Returns true if successful, else false.
1238    * The filename must have a .py extension for this command to work.
1239    * 
1240    * @param filepath
1241    * @return
1242    */
1243   public boolean openSession(String filepath)
1244   {
1245     evalStateCommand("open " + filepath, true);
1246     // todo: test for failure - how?
1247     return true;
1248   }
1249
1250   public boolean isFinishedInit()
1251   {
1252     return finishedInit;
1253   }
1254
1255   public void setFinishedInit(boolean finishedInit)
1256   {
1257     this.finishedInit = finishedInit;
1258   }
1259
1260   public List<String> getChainNames()
1261   {
1262     return chainNames;
1263   }
1264
1265 }