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