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