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