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