95ece8a47e2a28fa147a3ef1a46555c53d8c3c4a
[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, false);
295   }
296
297   /**
298    * Close down the Jalview viewer and listener, and (optionally) the associated
299    * Chimera window.
300    */
301   public void closeViewer(boolean closeChimera)
302   {
303     getSsm().removeStructureViewerListener(this, this.getPdbFile());
304     if (closeChimera)
305     {
306       viewer.exitChimera();
307     }
308     if (this.chimeraListener != null)
309     {
310       chimeraListener.shutdown();
311       chimeraListener = null;
312     }
313     lastCommand = null;
314     viewer = null;
315
316     releaseUIResources();
317   }
318
319   public void colourByChain()
320   {
321     colourBySequence = false;
322     evalStateCommand("rainbow chain", false);
323   }
324
325   public void colourByCharge()
326   {
327     colourBySequence = false;
328     evalStateCommand(
329             "color white;color red ::ASP;color red ::GLU;color blue ::LYS;color blue ::ARG;color yellow ::CYS",
330             false);
331   }
332
333   /**
334    * superpose the structures associated with sequences in the alignment
335    * according to their corresponding positions.
336    */
337   public void superposeStructures(AlignmentI alignment)
338   {
339     superposeStructures(alignment, -1, null);
340   }
341
342   /**
343    * superpose the structures associated with sequences in the alignment
344    * according to their corresponding positions. ded)
345    * 
346    * @param refStructure
347    *          - select which pdb file to use as reference (default is -1 - the
348    *          first structure in the alignment)
349    */
350   public void superposeStructures(AlignmentI alignment, int refStructure)
351   {
352     superposeStructures(alignment, refStructure, null);
353   }
354
355   /**
356    * superpose the structures associated with sequences in the alignment
357    * according to their corresponding positions. ded)
358    * 
359    * @param refStructure
360    *          - select which pdb file to use as reference (default is -1 - the
361    *          first structure in the alignment)
362    * @param hiddenCols
363    *          TODO
364    */
365   public void superposeStructures(AlignmentI alignment, int refStructure,
366           ColumnSelection hiddenCols)
367   {
368     superposeStructures(new AlignmentI[]
369     { alignment }, new int[]
370     { refStructure }, new ColumnSelection[]
371     { hiddenCols });
372   }
373
374   public void superposeStructures(AlignmentI[] _alignment,
375           int[] _refStructure, ColumnSelection[] _hiddenCols)
376   {
377     assert (_alignment.length == _refStructure.length && _alignment.length != _hiddenCols.length);
378     StringBuilder allComs = new StringBuilder(128); // Chimera superposition cmd
379     String[] files = getPdbFile();
380     // check to see if we are still waiting for Chimera files
381     long starttime = System.currentTimeMillis();
382     boolean waiting = true;
383     do
384     {
385       waiting = false;
386       for (String file : files)
387       {
388         try
389         {
390           // HACK - in Jalview 2.8 this call may not be threadsafe so we catch
391           // every possible exception
392           StructureMapping[] sm = getSsm().getMapping(file);
393           if (sm == null || sm.length == 0)
394           {
395             waiting = true;
396           }
397         } catch (Exception x)
398         {
399           waiting = true;
400         } catch (Error q)
401         {
402           waiting = true;
403         }
404       }
405       // we wait around for a reasonable time before we give up
406     } while (waiting
407             && System.currentTimeMillis() < (10000 + 1000 * files.length + starttime));
408     if (waiting)
409     {
410       System.err
411               .println("RUNTIME PROBLEM: Chimera seems to be taking a long time to process all the structures.");
412       return;
413     }
414     refreshPdbEntries();
415     StringBuffer selectioncom = new StringBuffer();
416     for (int a = 0; a < _alignment.length; a++)
417     {
418       int refStructure = _refStructure[a];
419       AlignmentI alignment = _alignment[a];
420       ColumnSelection hiddenCols = _hiddenCols[a];
421       if (a > 0
422               && selectioncom.length() > 0
423               && !selectioncom.substring(selectioncom.length() - 1).equals(
424                       " "))
425       {
426         selectioncom.append(" ");
427       }
428       // process this alignment
429       if (refStructure >= files.length)
430       {
431         System.err.println("Invalid reference structure value "
432                 + refStructure);
433         refStructure = -1;
434       }
435       if (refStructure < -1)
436       {
437         refStructure = -1;
438       }
439
440       boolean matched[] = new boolean[alignment.getWidth()];
441       for (int m = 0; m < matched.length; m++)
442       {
443
444         matched[m] = (hiddenCols != null) ? hiddenCols.isVisible(m) : true;
445       }
446
447       int commonrpositions[][] = new int[files.length][alignment.getWidth()];
448       String isel[] = new String[files.length];
449       String[] targetC = new String[files.length];
450       String[] chainNames = new String[files.length];
451       String[] atomSpec = new String[files.length];
452       for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
453       {
454         StructureMapping[] mapping = getSsm().getMapping(files[pdbfnum]);
455         // RACE CONDITION - getMapping only returns Jmol loaded filenames once
456         // Jmol callback has completed.
457         if (mapping == null || mapping.length < 1)
458         {
459           throw new Error(MessageManager.getString("error.implementation_error_chimera_getting_data"));
460         }
461         int lastPos = -1;
462         final int seqCountForPdbFile = getSequence()[pdbfnum].length;
463         for (int s = 0; s < seqCountForPdbFile; s++)
464         {
465           for (int sp, m = 0; m < mapping.length; m++)
466           {
467             final SequenceI theSequence = getSequence()[pdbfnum][s];
468             if (mapping[m].getSequence() == theSequence
469                     && (sp = alignment.findIndex(theSequence)) > -1)
470             {
471               if (refStructure == -1)
472               {
473                 refStructure = pdbfnum;
474               }
475               SequenceI asp = alignment.getSequenceAt(sp);
476               for (int r = 0; r < matched.length; r++)
477               {
478                 if (!matched[r])
479                 {
480                   continue;
481                 }
482                 matched[r] = false; // assume this is not a good site
483                 if (r >= asp.getLength())
484                 {
485                   continue;
486                 }
487
488                 if (Comparison.isGap(asp.getCharAt(r)))
489                 {
490                   // no mapping to gaps in sequence
491                   continue;
492                 }
493                 int t = asp.findPosition(r); // sequence position
494                 int apos = mapping[m].getAtomNum(t);
495                 int pos = mapping[m].getPDBResNum(t);
496
497                 if (pos < 1 || pos == lastPos)
498                 {
499                   // can't align unmapped sequence
500                   continue;
501                 }
502                 matched[r] = true; // this is a good ite
503                 lastPos = pos;
504                 // just record this residue position
505                 commonrpositions[pdbfnum][r] = pos;
506               }
507               // create model selection suffix
508               isel[pdbfnum] = "#" + pdbfnum;
509               if (mapping[m].getChain() == null
510                       || mapping[m].getChain().trim().length() == 0)
511               {
512                 targetC[pdbfnum] = "";
513               }
514               else
515               {
516                 targetC[pdbfnum] = "." + mapping[m].getChain();
517               }
518               chainNames[pdbfnum] = mapping[m].getPdbId()
519                       + targetC[pdbfnum];
520               atomSpec[pdbfnum] = asp.getRNA() != null ? PHOSPHORUS : ALPHACARBON;
521               // move on to next pdb file
522               s = seqCountForPdbFile;
523               break;
524             }
525           }
526         }
527       }
528
529       // TODO: consider bailing if nmatched less than 4 because superposition
530       // not
531       // well defined.
532       // TODO: refactor superposable position search (above) from jmol selection
533       // construction (below)
534
535       String[] selcom = new String[files.length];
536       int nmatched = 0;
537       String sep = "";
538       // generate select statements to select regions to superimpose structures
539       {
540         for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
541         {
542           String chainCd = targetC[pdbfnum];
543           int lpos = -1;
544           boolean run = false;
545           StringBuffer molsel = new StringBuffer();
546           for (int r = 0; r < matched.length; r++)
547           {
548             if (matched[r])
549             {
550               if (pdbfnum == 0)
551               {
552                 nmatched++;
553               }
554               if (lpos != commonrpositions[pdbfnum][r] - 1)
555               {
556                 // discontinuity
557                 if (lpos != -1)
558                 {
559                   molsel.append((run ? "" : ":") + lpos);
560                   molsel.append(chainCd);
561                   molsel.append(",");
562                 }
563               }
564               else
565               {
566                 // continuous run - and lpos >-1
567                 if (!run)
568                 {
569                   // at the beginning, so add dash
570                   molsel.append(":" + lpos);
571                   molsel.append("-");
572                 }
573                 run = true;
574               }
575               lpos = commonrpositions[pdbfnum][r];
576               // molsel.append(lpos);
577             }
578           }
579           // add final selection phrase
580           if (lpos != -1)
581           {
582             molsel.append((run ? "" : ":") + lpos);
583             molsel.append(chainCd);
584             // molsel.append("");
585           }
586           if (molsel.length() > 1)
587           {
588             selcom[pdbfnum] = molsel.toString();
589             selectioncom.append("#" + pdbfnum);
590             selectioncom.append(selcom[pdbfnum]);
591             selectioncom.append(" ");
592             if (pdbfnum < files.length - 1)
593             {
594               selectioncom.append("| ");
595             }
596           }
597           else
598           {
599             selcom[pdbfnum] = null;
600           }
601         }
602       }
603       StringBuilder command = new StringBuilder(256);
604       for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
605       {
606         if (pdbfnum == refStructure || selcom[pdbfnum] == null
607                 || selcom[refStructure] == null)
608         {
609           continue;
610         }
611         if (command.length() > 0)
612         {
613           command.append(";");
614         }
615
616         /*
617          * Form Chimera match command, from the 'new' structure to the
618          * 'reference' structure e.g. (residues 1-91, chain B/A, alphacarbons):
619          * 
620          * match #1:1-91.B@CA #0:1-91.A@CA
621          * 
622          * @see
623          * https://www.cgl.ucsf.edu/chimera/docs/UsersGuide/midas/match.html
624          */
625         command.append("match #" + pdbfnum /* +".1" */);
626         // TODO: handle sub-models
627         command.append(selcom[pdbfnum]);
628         command.append("@" + atomSpec[pdbfnum]);
629         // JAL-1757 exclude alternative CA locations
630         command.append(NO_ALTLOCS);
631         command.append(" #" + refStructure /* +".1" */);
632         command.append(selcom[refStructure]);
633         command.append("@" + atomSpec[refStructure]);
634         command.append(NO_ALTLOCS);
635       }
636       if (selectioncom.length() > 0)
637       {
638         if (debug)
639         {
640           System.out.println("Select regions:\n" + selectioncom.toString());
641           System.out.println("Superimpose command(s):\n"
642                   + command.toString());
643         }
644         allComs.append("~display all; chain @CA|P; ribbon "
645                 + selectioncom.toString() + ";"+command.toString());
646         // selcom.append("; ribbons; ");
647       }
648     }
649     if (selectioncom.length() > 0)
650     {// finally, mark all regions that were superposed.
651       if (selectioncom.substring(selectioncom.length() - 1).equals("|"))
652       {
653         selectioncom.setLength(selectioncom.length() - 1);
654       }
655       if (debug)
656       {
657         System.out.println("Select regions:\n" + selectioncom.toString());
658       }
659       allComs.append("; ~display all; chain @CA|P; ribbon "
660               + selectioncom.toString() + "; focus");
661       // evalStateCommand("select *; backbone; select "+selcom.toString()+"; cartoons; center "+selcom.toString());
662       evalStateCommand(allComs.toString(), true /* false */);
663     }
664     
665   }
666
667   private void checkLaunched()
668   {
669     if (!viewer.isChimeraLaunched())
670     {
671       viewer.launchChimera(StructureManager.getChimeraPaths());
672     }
673     if (!viewer.isChimeraLaunched())
674     {
675       log("Failed to launch Chimera!");
676     }
677   }
678
679   /**
680    * Answers true if the Chimera process is still running, false if ended or not
681    * started.
682    * 
683    * @return
684    */
685   public boolean isChimeraRunning()
686   {
687     return viewer.isChimeraLaunched();
688   }
689
690   /**
691    * Send a command to Chimera, launching it first if necessary, and optionally
692    * log any responses.
693    * 
694    * @param command
695    * @param logResponse
696    */
697   public void evalStateCommand(final String command, boolean logResponse)
698   {
699     viewerCommandHistory(false);
700     checkLaunched();
701     if (lastCommand == null || !lastCommand.equals(command))
702     {
703       // trim command or it may never find a match in the replyLog!!
704       lastReply = viewer.sendChimeraCommand(command.trim(), logResponse);
705       if (debug && logResponse)
706       {
707         log("Response from command ('" + command + "') was:\n" + lastReply);
708       }
709     }
710     viewerCommandHistory(true);
711     lastCommand = command;
712   }
713
714   /**
715    * colour any structures associated with sequences in the given alignment
716    * using the getFeatureRenderer() and getSequenceRenderer() renderers but only
717    * if colourBySequence is enabled.
718    */
719   public void colourBySequence(boolean showFeatures,
720           jalview.api.AlignmentViewPanel alignmentv)
721   {
722     if (!colourBySequence || !loadingFinished)
723     {
724       return;
725     }
726     if (getSsm() == null)
727     {
728       return;
729     }
730     String[] files = getPdbFile();
731
732     SequenceRenderer sr = getSequenceRenderer(alignmentv);
733
734     FeatureRenderer fr = null;
735     if (showFeatures)
736     {
737       fr = getFeatureRenderer(alignmentv);
738     }
739     AlignmentI alignment = alignmentv.getAlignment();
740
741     for (jalview.structure.StructureMappingcommandSet cpdbbyseq : getColourBySequenceCommands(
742             files, sr, fr, alignment))
743     {
744       for (String command : cpdbbyseq.commands)
745       {
746         executeWhenReady(command);
747       }
748     }
749   }
750
751   /**
752    * @param files
753    * @param sr
754    * @param fr
755    * @param alignment
756    * @return
757    */
758   protected StructureMappingcommandSet[] getColourBySequenceCommands(
759           String[] files, SequenceRenderer sr, FeatureRenderer fr,
760           AlignmentI alignment)
761   {
762     return ChimeraCommands.getColourBySequenceCommand(getSsm(), files,
763             getSequence(), sr, fr, alignment);
764   }
765
766   /**
767    * @param command
768    */
769   protected void executeWhenReady(String command)
770   {
771     waitForChimera();
772     evalStateCommand(command, false);
773     waitForChimera();
774   }
775
776   private void waitForChimera()
777   {
778     while (viewer != null && viewer.isBusy())
779     {
780       try {
781         Thread.sleep(15);
782       } catch (InterruptedException q)
783       {}
784     }
785   }
786
787
788
789   // End StructureListener
790   // //////////////////////////
791
792   public Color getColour(int atomIndex, int pdbResNum, String chain,
793           String pdbfile)
794   {
795     if (getModelNum(pdbfile) < 0)
796     {
797       return null;
798     }
799     log("get model / residue colour attribute unimplemented");
800     return null;
801   }
802
803   /**
804    * returns the current featureRenderer that should be used to colour the
805    * structures
806    * 
807    * @param alignment
808    * 
809    * @return
810    */
811   public abstract FeatureRenderer getFeatureRenderer(
812           AlignmentViewPanel alignment);
813
814   /**
815    * instruct the Jalview binding to update the pdbentries vector if necessary
816    * prior to matching the jmol view's contents to the list of structure files
817    * Jalview knows about.
818    */
819   public abstract void refreshPdbEntries();
820
821   private int getModelNum(String modelFileName)
822   {
823     String[] mfn = getPdbFile();
824     if (mfn == null)
825     {
826       return -1;
827     }
828     for (int i = 0; i < mfn.length; i++)
829     {
830       if (mfn[i].equalsIgnoreCase(modelFileName))
831       {
832         return i;
833       }
834     }
835     return -1;
836   }
837
838   /**
839    * map between index of model filename returned from getPdbFile and the first
840    * index of models from this file in the viewer. Note - this is not trimmed -
841    * use getPdbFile to get number of unique models.
842    */
843   private int _modelFileNameMap[];
844
845   // ////////////////////////////////
846   // /StructureListener
847   public synchronized String[] getPdbFile()
848   {
849     if (viewer == null)
850     {
851       return new String[0];
852     }
853     // if (modelFileNames == null)
854     // {
855     // Collection<ChimeraModel> chimodels = viewer.getChimeraModels();
856     // _modelFileNameMap = new int[chimodels.size()];
857     // int j = 0;
858     // for (ChimeraModel chimodel : chimodels)
859     // {
860     // String mdlName = chimodel.getModelName();
861     // }
862     // modelFileNames = new String[j];
863     // // System.arraycopy(mset, 0, modelFileNames, 0, j);
864     // }
865
866     return chimeraMaps.keySet().toArray(
867             modelFileNames = new String[chimeraMaps.size()]);
868   }
869
870   /**
871    * map from string to applet
872    */
873   public Map getRegistryInfo()
874   {
875     // TODO Auto-generated method stub
876     return null;
877   }
878
879   /**
880    * returns the current sequenceRenderer that should be used to colour the
881    * structures
882    * 
883    * @param alignment
884    * 
885    * @return
886    */
887   public abstract SequenceRenderer getSequenceRenderer(
888           AlignmentViewPanel alignment);
889
890   /**
891    * Construct and send a command to highlight an atom.
892    * 
893    * <pre>
894    * Done by generating a command like (to 'highlight' position 44)
895    *   ~show #0:43.C;show #0:44.C
896    * Note this removes the highlight from the previous position.
897    * </pre>
898    */
899   public void highlightAtom(int atomIndex, int pdbResNum, String chain,
900           String pdbfile)
901   {
902     List<ChimeraModel> cms = chimeraMaps.get(pdbfile);
903     if (cms != null)
904     {
905       StringBuilder sb = new StringBuilder();
906       sb.append(" #" + cms.get(0).getModelNumber());
907       sb.append(":" + pdbResNum);
908       if (!chain.equals(" "))
909       {
910         sb.append("." + chain);
911       }
912       String atomSpec = sb.toString();
913
914       StringBuilder command = new StringBuilder(32);
915       if (lastMousedOverAtomSpec != null)
916       {
917         command.append("~show " + lastMousedOverAtomSpec + ";");
918       }
919       viewerCommandHistory(false);
920       command.append("show ").append(atomSpec);
921       String cmd = command.toString();
922       if (cmd.length() > 0)
923       {
924         viewer.stopListening(chimeraListener.getUri());
925         viewer.sendChimeraCommand(cmd, false);
926         viewer.startListening(chimeraListener.getUri());
927       }
928       viewerCommandHistory(true);
929       this.lastMousedOverAtomSpec = atomSpec;
930     }
931   }
932
933   /**
934    * Query Chimera for its current selection, and highlight it on the alignment
935    */
936   public void highlightChimeraSelection()
937   {
938     /*
939      * Ask Chimera for its current selection
940      */
941     List<String> selection = viewer.getSelectedResidueSpecs();
942
943     /*
944      * Parse model number, residue and chain for each selected position,
945      * formatted as #0:123.A or #1.2:87.B (#model.submodel:residue.chain)
946      */
947     List<AtomSpec> atomSpecs = new ArrayList<AtomSpec>();
948     for (String atomSpec : selection)
949     {
950       int colonPos = atomSpec.indexOf(":");
951       if (colonPos == -1)
952       {
953         continue; // malformed
954       }
955     
956       int hashPos = atomSpec.indexOf("#");
957       String modelSubmodel = atomSpec.substring(hashPos + 1, colonPos);
958       int dotPos = modelSubmodel.indexOf(".");
959       int modelId = 0;
960       try {
961         modelId = Integer.valueOf(dotPos == -1 ? modelSubmodel
962                 : modelSubmodel.substring(0, dotPos));
963       } catch (NumberFormatException e) {
964         // ignore, default to model 0
965       }
966     
967       String residueChain = atomSpec.substring(colonPos + 1);
968       dotPos = residueChain.indexOf(".");
969       int pdbResNum = Integer.parseInt(dotPos == -1 ? residueChain
970               : residueChain.substring(0, dotPos));
971     
972       String chainId = dotPos == -1 ? "" : residueChain
973               .substring(dotPos + 1);
974     
975       /*
976        * Work out the pdbfilename from the model number
977        */
978       String pdbfilename = modelFileNames[frameNo];
979       findfileloop: for (String pdbfile : this.chimeraMaps.keySet())
980       {
981         for (ChimeraModel cm : chimeraMaps.get(pdbfile))
982         {
983           if (cm.getModelNumber() == modelId)
984           {
985             pdbfilename = pdbfile;
986             break findfileloop;
987           }
988         }
989       }
990       atomSpecs.add(new AtomSpec(pdbfilename, chainId, pdbResNum, 0));
991     }
992
993     /*
994      * Broadcast the selection (which may be empty, if the user just cleared all
995      * selections)
996      */
997     getSsm().mouseOverStructure(atomSpecs);
998   }
999
1000   private void log(String message)
1001   {
1002     System.err.println("## Chimera log: " + message);
1003   }
1004
1005   private void viewerCommandHistory(boolean enable)
1006   {
1007     // log("(Not yet implemented) History "
1008     // + ((debug || enable) ? "on" : "off"));
1009   }
1010
1011   public long getLoadNotifiesHandled()
1012   {
1013     return loadNotifiesHandled;
1014   }
1015
1016   public void setJalviewColourScheme(ColourSchemeI cs)
1017   {
1018     colourBySequence = false;
1019
1020     if (cs == null)
1021     {
1022       return;
1023     }
1024
1025     // Chimera expects RBG values in the range 0-1
1026     final double normalise = 255D;
1027     viewerCommandHistory(false);
1028     StringBuilder command = new StringBuilder(128);
1029
1030     List<String> residueSet = ResidueProperties.getResidues(isNucleotide(),
1031             false);
1032     for (String res : residueSet)
1033     {
1034       Color col = cs.findColour(res.charAt(0));
1035       command.append("color " + col.getRed() / normalise + ","
1036               + col.getGreen() / normalise + "," + col.getBlue()
1037               / normalise + " ::" + res + ";");
1038     }
1039
1040     evalStateCommand(command.toString(),false);
1041     viewerCommandHistory(true);
1042   }
1043
1044   /**
1045    * called when the binding thinks the UI needs to be refreshed after a Chimera
1046    * state change. this could be because structures were loaded, or because an
1047    * error has occurred.
1048    */
1049   public abstract void refreshGUI();
1050
1051   public void setLoadingFromArchive(boolean loadingFromArchive)
1052   {
1053     this.loadingFromArchive = loadingFromArchive;
1054   }
1055
1056   /**
1057    * 
1058    * @return true if Chimeral is still restoring state or loading is still going
1059    *         on (see setFinsihedLoadingFromArchive)
1060    */
1061   public boolean isLoadingFromArchive()
1062   {
1063     return loadingFromArchive && !loadingFinished;
1064   }
1065
1066   /**
1067    * modify flag which controls if sequence colouring events are honoured by the
1068    * binding. Should be true for normal operation
1069    * 
1070    * @param finishedLoading
1071    */
1072   public void setFinishedLoadingFromArchive(boolean finishedLoading)
1073   {
1074     loadingFinished = finishedLoading;
1075   }
1076
1077   /**
1078    * Send the Chimera 'background solid <color>" command.
1079    * 
1080    * @see https
1081    *      ://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/background
1082    *      .html
1083    * @param col
1084    */
1085   public void setBackgroundColour(Color col)
1086   {
1087     viewerCommandHistory(false);
1088     double normalise = 255D;
1089     final String command = "background solid " + col.getRed() / normalise + ","
1090             + col.getGreen() / normalise + "," + col.getBlue()
1091             / normalise + ";";
1092     viewer.sendChimeraCommand(command, false);
1093     viewerCommandHistory(true);
1094   }
1095
1096   /**
1097    * 
1098    * @param pdbfile
1099    * @return text report of alignment between pdbfile and any associated
1100    *         alignment sequences
1101    */
1102   public String printMapping(String pdbfile)
1103   {
1104     return getSsm().printMapping(pdbfile);
1105   }
1106
1107   /**
1108    * Ask Chimera to save its session to the given file. Returns true if
1109    * successful, else false.
1110    * 
1111    * @param filepath
1112    * @return
1113    */
1114   public boolean saveSession(String filepath)
1115   {
1116     if (isChimeraRunning())
1117     {
1118       List<String> reply = viewer.sendChimeraCommand("save " + filepath,
1119               true);
1120       if (reply.contains("Session written"))
1121       {
1122         return true;
1123       }
1124       else
1125       {
1126         Cache.log
1127                 .error("Error saving Chimera session: " + reply.toString());
1128       }
1129     }
1130     return false;
1131   }
1132
1133   /**
1134    * Ask Chimera to open a session file. Returns true if successful, else false.
1135    * The filename must have a .py extension for this command to work.
1136    * 
1137    * @param filepath
1138    * @return
1139    */
1140   public boolean openSession(String filepath)
1141   {
1142     evalStateCommand("open " + filepath, true);
1143     // todo: test for failure - how?
1144     return true;
1145   }
1146
1147   public boolean isFinishedInit()
1148   {
1149     return finishedInit;
1150   }
1151
1152   public void setFinishedInit(boolean finishedInit)
1153   {
1154     this.finishedInit = finishedInit;
1155   }
1156
1157   public List<String> getChainNames()
1158   {
1159     return chainNames;
1160   }
1161
1162 }