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