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