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