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