JAL-3187 hacks to get peptide variant in to Jmol hover tooltip
[jalview.git] / src / jalview / ext / jmol / JalviewJmolBinding.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.ext.jmol;
22
23 import jalview.api.AlignmentViewPanel;
24 import jalview.api.FeatureRenderer;
25 import jalview.api.SequenceRenderer;
26 import jalview.datamodel.AlignmentI;
27 import jalview.datamodel.HiddenColumns;
28 import jalview.datamodel.PDBEntry;
29 import jalview.datamodel.SequenceI;
30 import jalview.gui.IProgressIndicator;
31 import jalview.io.DataSourceType;
32 import jalview.io.StructureFile;
33 import jalview.schemes.ColourSchemeI;
34 import jalview.schemes.ResidueProperties;
35 import jalview.structure.AtomSpec;
36 import jalview.structure.StructureMappingcommandSet;
37 import jalview.structure.StructureSelectionManager;
38 import jalview.structures.models.AAStructureBindingModel;
39 import jalview.util.MessageManager;
40
41 import java.awt.Color;
42 import java.awt.Container;
43 import java.awt.event.ComponentEvent;
44 import java.awt.event.ComponentListener;
45 import java.io.File;
46 import java.net.URL;
47 import java.security.AccessControlException;
48 import java.util.ArrayList;
49 import java.util.BitSet;
50 import java.util.Hashtable;
51 import java.util.List;
52 import java.util.Map;
53 import java.util.StringTokenizer;
54 import java.util.Vector;
55
56 import org.jmol.adapter.smarter.SmarterJmolAdapter;
57 import org.jmol.api.JmolAppConsoleInterface;
58 import org.jmol.api.JmolSelectionListener;
59 import org.jmol.api.JmolStatusListener;
60 import org.jmol.api.JmolViewer;
61 import org.jmol.c.CBK;
62 import org.jmol.script.T;
63 import org.jmol.viewer.Viewer;
64
65 public abstract class JalviewJmolBinding extends AAStructureBindingModel
66         implements JmolStatusListener, JmolSelectionListener,
67         ComponentListener
68 {
69   private String lastMessage;
70
71   boolean allChainsSelected = false;
72
73   /*
74    * when true, try to search the associated datamodel for sequences that are
75    * associated with any unknown structures in the Jmol view.
76    */
77   private boolean associateNewStructs = false;
78
79   Vector<String> atomsPicked = new Vector<>();
80
81   private List<String> chainNames;
82
83   Hashtable<String, String> chainFile;
84
85   /*
86    * the default or current model displayed if the model cannot be identified
87    * from the selection message
88    */
89   int frameNo = 0;
90
91   // protected JmolGenericPopup jmolpopup; // not used - remove?
92
93   String lastCommand;
94
95   boolean loadedInline;
96
97   StringBuffer resetLastRes = new StringBuffer();
98
99   public Viewer viewer;
100
101   public JalviewJmolBinding(StructureSelectionManager ssm,
102           PDBEntry[] pdbentry, SequenceI[][] sequenceIs,
103           DataSourceType protocol)
104   {
105     super(ssm, pdbentry, sequenceIs, protocol);
106     /*
107      * viewer = JmolViewer.allocateViewer(renderPanel, new SmarterJmolAdapter(),
108      * "jalviewJmol", ap.av.applet .getDocumentBase(),
109      * ap.av.applet.getCodeBase(), "", this);
110      * 
111      * jmolpopup = JmolPopup.newJmolPopup(viewer, true, "Jmol", true);
112      */
113   }
114
115   public JalviewJmolBinding(StructureSelectionManager ssm,
116           SequenceI[][] seqs, Viewer theViewer)
117   {
118     super(ssm, seqs);
119
120     viewer = theViewer;
121     viewer.setJmolStatusListener(this);
122     viewer.addSelectionListener(this);
123   }
124
125   /**
126    * construct a title string for the viewer window based on the data jalview
127    * knows about
128    * 
129    * @return
130    */
131   public String getViewerTitle()
132   {
133     return getViewerTitle("Jmol", true);
134   }
135
136   /**
137    * prepare the view for a given set of models/chains. chainList contains
138    * strings of the form 'pdbfilename:Chaincode'
139    * 
140    * @param chainList
141    *          list of chains to make visible
142    */
143   public void centerViewer(Vector<String> chainList)
144   {
145     StringBuilder cmd = new StringBuilder(128);
146     int mlength, p;
147     for (String lbl : chainList)
148     {
149       mlength = 0;
150       do
151       {
152         p = mlength;
153         mlength = lbl.indexOf(":", p);
154       } while (p < mlength && mlength < (lbl.length() - 2));
155       // TODO: lookup each pdb id and recover proper model number for it.
156       cmd.append(":" + lbl.substring(mlength + 1) + " /"
157               + (1 + getModelNum(chainFile.get(lbl))) + " or ");
158     }
159     if (cmd.length() > 0)
160     {
161       cmd.setLength(cmd.length() - 4);
162     }
163     evalStateCommand("select *;restrict " + cmd + ";cartoon;center " + cmd);
164   }
165
166   public void closeViewer()
167   {
168     // remove listeners for all structures in viewer
169     getSsm().removeStructureViewerListener(this, this.getStructureFiles());
170     viewer.dispose();
171     lastCommand = null;
172     viewer = null;
173     releaseUIResources();
174   }
175
176   @Override
177   public void colourByChain()
178   {
179     colourBySequence = false;
180     // TODO: colour by chain should colour each chain distinctly across all
181     // visible models
182     // TODO: http://issues.jalview.org/browse/JAL-628
183     evalStateCommand("select *;color chain");
184   }
185
186   @Override
187   public void colourByCharge()
188   {
189     colourBySequence = false;
190     evalStateCommand("select *;color white;select ASP,GLU;color red;"
191             + "select LYS,ARG;color blue;select CYS;color yellow");
192   }
193
194   /**
195    * superpose the structures associated with sequences in the alignment
196    * according to their corresponding positions.
197    */
198   public void superposeStructures(AlignmentI alignment)
199   {
200     superposeStructures(alignment, -1, null);
201   }
202
203   /**
204    * superpose the structures associated with sequences in the alignment
205    * according to their corresponding positions. ded)
206    * 
207    * @param refStructure
208    *          - select which pdb file to use as reference (default is -1 - the
209    *          first structure in the alignment)
210    */
211   public void superposeStructures(AlignmentI alignment, int refStructure)
212   {
213     superposeStructures(alignment, refStructure, null);
214   }
215
216   /**
217    * superpose the structures associated with sequences in the alignment
218    * according to their corresponding positions. ded)
219    * 
220    * @param refStructure
221    *          - select which pdb file to use as reference (default is -1 - the
222    *          first structure in the alignment)
223    * @param hiddenCols
224    *          TODO
225    */
226   public void superposeStructures(AlignmentI alignment, int refStructure,
227           HiddenColumns hiddenCols)
228   {
229     superposeStructures(new AlignmentI[] { alignment },
230             new int[]
231             { refStructure }, new HiddenColumns[] { hiddenCols });
232   }
233
234   /**
235    * {@inheritDoc}
236    */
237   @Override
238   public String superposeStructures(AlignmentI[] _alignment,
239           int[] _refStructure, HiddenColumns[] _hiddenCols)
240   {
241     while (viewer.isScriptExecuting())
242     {
243       try
244       {
245         Thread.sleep(10);
246       } catch (InterruptedException i)
247       {
248       }
249     }
250
251     /*
252      * get the distinct structure files modelled
253      * (a file with multiple chains may map to multiple sequences)
254      */
255     String[] files = getStructureFiles();
256     if (!waitForFileLoad(files))
257     {
258       return null;
259     }
260
261     StringBuilder selectioncom = new StringBuilder(256);
262     // In principle - nSeconds specifies the speed of animation for each
263     // superposition - but is seems to behave weirdly, so we don't specify it.
264     String nSeconds = " ";
265     if (files.length > 10)
266     {
267       nSeconds = " 0.005 ";
268     }
269     else
270     {
271       nSeconds = " " + (2.0 / files.length) + " ";
272       // if (nSeconds).substring(0,5)+" ";
273     }
274
275     // see JAL-1345 - should really automatically turn off the animation for
276     // large numbers of structures, but Jmol doesn't seem to allow that.
277     // nSeconds = " ";
278     // union of all aligned positions are collected together.
279     for (int a = 0; a < _alignment.length; a++)
280     {
281       int refStructure = _refStructure[a];
282       AlignmentI alignment = _alignment[a];
283       HiddenColumns hiddenCols = _hiddenCols[a];
284       if (a > 0 && selectioncom.length() > 0 && !selectioncom
285               .substring(selectioncom.length() - 1).equals("|"))
286       {
287         selectioncom.append("|");
288       }
289       // process this alignment
290       if (refStructure >= files.length)
291       {
292         System.err.println(
293                 "Invalid reference structure value " + refStructure);
294         refStructure = -1;
295       }
296
297       /*
298        * 'matched' bit j will be set for visible alignment columns j where
299        * all sequences have a residue with a mapping to the PDB structure
300        */
301       BitSet matched = new BitSet();
302       for (int m = 0; m < alignment.getWidth(); m++)
303       {
304         if (hiddenCols == null || hiddenCols.isVisible(m))
305         {
306           matched.set(m);
307         }
308       }
309
310       SuperposeData[] structures = new SuperposeData[files.length];
311       for (int f = 0; f < files.length; f++)
312       {
313         structures[f] = new SuperposeData(alignment.getWidth());
314       }
315
316       /*
317        * Calculate the superposable alignment columns ('matched'), and the
318        * corresponding structure residue positions (structures.pdbResNo)
319        */
320       int candidateRefStructure = findSuperposableResidues(alignment,
321               matched, structures);
322       if (refStructure < 0)
323       {
324         /*
325          * If no reference structure was specified, pick the first one that has
326          * a mapping in the alignment
327          */
328         refStructure = candidateRefStructure;
329       }
330
331       String[] selcom = new String[files.length];
332       int nmatched = matched.cardinality();
333       if (nmatched < 4)
334       {
335         return (MessageManager.formatMessage("label.insufficient_residues",
336                 nmatched));
337       }
338
339       /*
340        * generate select statements to select regions to superimpose structures
341        */
342       {
343         // TODO extract method to construct selection statements
344         for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
345         {
346           String chainCd = ":" + structures[pdbfnum].chain;
347           int lpos = -1;
348           boolean run = false;
349           StringBuilder molsel = new StringBuilder();
350           molsel.append("{");
351
352           int nextColumnMatch = matched.nextSetBit(0);
353           while (nextColumnMatch != -1)
354           {
355             int pdbResNo = structures[pdbfnum].pdbResNo[nextColumnMatch];
356             if (lpos != pdbResNo - 1)
357             {
358               // discontinuity
359               if (lpos != -1)
360               {
361                 molsel.append(lpos);
362                 molsel.append(chainCd);
363                 molsel.append("|");
364               }
365               run = false;
366             }
367             else
368             {
369               // continuous run - and lpos >-1
370               if (!run)
371               {
372                 // at the beginning, so add dash
373                 molsel.append(lpos);
374                 molsel.append("-");
375               }
376               run = true;
377             }
378             lpos = pdbResNo;
379             nextColumnMatch = matched.nextSetBit(nextColumnMatch + 1);
380           }
381           /*
382            * add final selection phrase
383            */
384           if (lpos != -1)
385           {
386             molsel.append(lpos);
387             molsel.append(chainCd);
388             molsel.append("}");
389           }
390           if (molsel.length() > 1)
391           {
392             selcom[pdbfnum] = molsel.toString();
393             selectioncom.append("((");
394             selectioncom.append(selcom[pdbfnum].substring(1,
395                     selcom[pdbfnum].length() - 1));
396             selectioncom.append(" )& ");
397             selectioncom.append(pdbfnum + 1);
398             selectioncom.append(".1)");
399             if (pdbfnum < files.length - 1)
400             {
401               selectioncom.append("|");
402             }
403           }
404           else
405           {
406             selcom[pdbfnum] = null;
407           }
408         }
409       }
410       StringBuilder command = new StringBuilder(256);
411       // command.append("set spinFps 10;\n");
412
413       for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
414       {
415         if (pdbfnum == refStructure || selcom[pdbfnum] == null
416                 || selcom[refStructure] == null)
417         {
418           continue;
419         }
420         command.append("echo ");
421         command.append("\"Superposing (");
422         command.append(structures[pdbfnum].pdbId);
423         command.append(") against reference (");
424         command.append(structures[refStructure].pdbId);
425         command.append(")\";\ncompare " + nSeconds);
426         command.append("{");
427         command.append(Integer.toString(1 + pdbfnum));
428         command.append(".1} {");
429         command.append(Integer.toString(1 + refStructure));
430         // conformation=1 excludes alternate locations for CA (JAL-1757)
431         command.append(
432                 ".1} SUBSET {(*.CA | *.P) and conformation=1} ATOMS ");
433
434         // for (int s = 0; s < 2; s++)
435         // {
436         // command.append(selcom[(s == 0 ? pdbfnum : refStructure)]);
437         // }
438         command.append(selcom[pdbfnum]);
439         command.append(selcom[refStructure]);
440         command.append(" ROTATE TRANSLATE;\n");
441       }
442       if (selectioncom.length() > 0)
443       {
444         // TODO is performing selectioncom redundant here? is done later on
445         // System.out.println("Select regions:\n" + selectioncom.toString());
446         evalStateCommand("select *; cartoons off; backbone; select ("
447                 + selectioncom.toString() + "); cartoons; ");
448         // selcom.append("; ribbons; ");
449         String cmdString = command.toString();
450         // System.out.println("Superimpose command(s):\n" + cmdString);
451
452         evalStateCommand(cmdString);
453       }
454     }
455     if (selectioncom.length() > 0)
456     {// finally, mark all regions that were superposed.
457       if (selectioncom.substring(selectioncom.length() - 1).equals("|"))
458       {
459         selectioncom.setLength(selectioncom.length() - 1);
460       }
461       // System.out.println("Select regions:\n" + selectioncom.toString());
462       evalStateCommand("select *; cartoons off; backbone; select ("
463               + selectioncom.toString() + "); cartoons; ");
464       // evalStateCommand("select *; backbone; select "+selcom.toString()+";
465       // cartoons; center "+selcom.toString());
466     }
467
468     return null;
469   }
470
471   public void evalStateCommand(String command)
472   {
473     jmolHistory(false);
474     if (lastCommand == null || !lastCommand.equals(command))
475     {
476       viewer.evalStringQuiet(command + "\n");
477     }
478     jmolHistory(true);
479     lastCommand = command;
480   }
481
482   Thread colourby = null;
483   /**
484    * Sends a set of colour commands to the structure viewer
485    * 
486    * @param colourBySequenceCommands
487    */
488   @Override
489   protected void colourBySequence(
490           final StructureMappingcommandSet[] colourBySequenceCommands)
491   {
492     if (colourby != null)
493     {
494       colourby.interrupt();
495       colourby = null;
496     }
497     colourby = new Thread(new Runnable()
498     {
499       @Override
500       public void run()
501       {
502         for (StructureMappingcommandSet cpdbbyseq : colourBySequenceCommands)
503         {
504           for (String cbyseq : cpdbbyseq.commands)
505           {
506             executeWhenReady(cbyseq);
507           }
508         }
509       }
510     });
511     colourby.start();
512   }
513
514   /**
515    * @param files
516    * @param sr
517    * @param viewPanel
518    * @return
519    */
520   @Override
521   protected StructureMappingcommandSet[] getColourBySequenceCommands(
522           String[] files, SequenceRenderer sr, AlignmentViewPanel viewPanel)
523   {
524     return JmolCommands.getColourBySequenceCommand(getSsm(), files,
525             getSequence(), sr, viewPanel);
526   }
527
528   /**
529    * @param command
530    */
531   protected void executeWhenReady(String command)
532   {
533     evalStateCommand(command);
534   }
535
536   public void createImage(String file, String type, int quality)
537   {
538     System.out.println("JMOL CREATE IMAGE");
539   }
540
541   @Override
542   public String createImage(String fileName, String type,
543           Object textOrBytes, int quality)
544   {
545     System.out.println("JMOL CREATE IMAGE");
546     return null;
547   }
548
549   @Override
550   public String eval(String strEval)
551   {
552     // System.out.println(strEval);
553     // "# 'eval' is implemented only for the applet.";
554     return null;
555   }
556
557   // End StructureListener
558   // //////////////////////////
559
560   @Override
561   public float[][] functionXY(String functionName, int x, int y)
562   {
563     return null;
564   }
565
566   @Override
567   public float[][][] functionXYZ(String functionName, int nx, int ny,
568           int nz)
569   {
570     // TODO Auto-generated method stub
571     return null;
572   }
573
574   public Color getColour(int atomIndex, int pdbResNum, String chain,
575           String pdbfile)
576   {
577     if (getModelNum(pdbfile) < 0)
578     {
579       return null;
580     }
581     // TODO: verify atomIndex is selecting correct model.
582     // return new Color(viewer.getAtomArgb(atomIndex)); Jmol 12.2.4
583     int colour = viewer.ms.at[atomIndex].atomPropertyInt(T.color);
584     return new Color(colour);
585   }
586
587   /**
588    * instruct the Jalview binding to update the pdbentries vector if necessary
589    * prior to matching the jmol view's contents to the list of structure files
590    * Jalview knows about.
591    */
592   public abstract void refreshPdbEntries();
593
594   private int getModelNum(String modelFileName)
595   {
596     String[] mfn = getStructureFiles();
597     if (mfn == null)
598     {
599       return -1;
600     }
601     for (int i = 0; i < mfn.length; i++)
602     {
603       if (mfn[i].equalsIgnoreCase(modelFileName))
604       {
605         return i;
606       }
607     }
608     return -1;
609   }
610
611   /**
612    * map between index of model filename returned from getPdbFile and the first
613    * index of models from this file in the viewer. Note - this is not trimmed -
614    * use getPdbFile to get number of unique models.
615    */
616   private int _modelFileNameMap[];
617
618   // ////////////////////////////////
619   // /StructureListener
620   // @Override
621   public synchronized String[] getPdbFilex()
622   {
623     if (viewer == null)
624     {
625       return new String[0];
626     }
627     if (modelFileNames == null)
628     {
629       List<String> mset = new ArrayList<>();
630       _modelFileNameMap = new int[viewer.ms.mc];
631       String m = viewer.ms.getModelFileName(0);
632       if (m != null)
633       {
634         String filePath = m;
635         try
636         {
637           filePath = new File(m).getAbsolutePath();
638         } catch (AccessControlException x)
639         {
640           // usually not allowed to do this in applet
641           System.err.println(
642                   "jmolBinding: Using local file string from Jmol: " + m);
643         }
644         if (filePath.indexOf("/file:") != -1)
645         {
646           // applet path with docroot - discard as format won't match pdbfile
647           filePath = m;
648         }
649         mset.add(filePath);
650         _modelFileNameMap[0] = 0; // filename index for first model is always 0.
651       }
652       int j = 1;
653       for (int i = 1; i < viewer.ms.mc; i++)
654       {
655         m = viewer.ms.getModelFileName(i);
656         String filePath = m;
657         if (m != null)
658         {
659           try
660           {
661             filePath = new File(m).getAbsolutePath();
662           } catch (AccessControlException x)
663           {
664             // usually not allowed to do this in applet, so keep raw handle
665             // System.err.println("jmolBinding: Using local file string from
666             // Jmol: "+m);
667           }
668         }
669
670         /*
671          * add this model unless it is read from a structure file we have
672          * already seen (example: 2MJW is an NMR structure with 10 models)
673          */
674         if (!mset.contains(filePath))
675         {
676           mset.add(filePath);
677           _modelFileNameMap[j] = i; // record the model index for the filename
678           j++;
679         }
680       }
681       modelFileNames = mset.toArray(new String[mset.size()]);
682     }
683     return modelFileNames;
684   }
685
686   @Override
687   public synchronized String[] getStructureFiles()
688   {
689     List<String> mset = new ArrayList<>();
690     if (viewer == null)
691     {
692       return new String[0];
693     }
694
695     if (modelFileNames == null)
696     {
697       int modelCount = viewer.ms.mc;
698       String filePath = null;
699       for (int i = 0; i < modelCount; ++i)
700       {
701         filePath = viewer.ms.getModelFileName(i);
702         if (!mset.contains(filePath))
703         {
704           mset.add(filePath);
705         }
706       }
707       modelFileNames = mset.toArray(new String[mset.size()]);
708     }
709
710     return modelFileNames;
711   }
712
713   /**
714    * map from string to applet
715    */
716   @Override
717   public Map<String, Object> getRegistryInfo()
718   {
719     // TODO Auto-generated method stub
720     return null;
721   }
722
723   // ///////////////////////////////
724   // JmolStatusListener
725
726   public void handlePopupMenu(int x, int y)
727   {
728     // jmolpopup.show(x, y);
729     // jmolpopup.jpiShow(x, y);
730   }
731
732   /**
733    * Highlight zero, one or more atoms on the structure
734    */
735   @Override
736   public void highlightAtoms(List<AtomSpec> atoms)
737   {
738     if (atoms != null)
739     {
740       if (resetLastRes.length() > 0)
741       {
742         viewer.evalStringQuiet(resetLastRes.toString());
743         resetLastRes.setLength(0);
744       }
745       for (AtomSpec atom : atoms)
746       {
747         highlightAtom(atom.getAtomIndex(), atom.getPdbResNum(),
748                 atom.getChain(), atom.getPdbFile());
749       }
750     }
751   }
752
753   // jmol/ssm only
754   public void highlightAtom(int atomIndex, int pdbResNum, String chain,
755           String pdbfile)
756   {
757     if (modelFileNames == null)
758     {
759       return;
760     }
761
762     // look up file model number for this pdbfile
763     int mdlNum = 0;
764     // may need to adjust for URLencoding here - we don't worry about that yet.
765     while (mdlNum < modelFileNames.length
766             && !pdbfile.equals(modelFileNames[mdlNum]))
767     {
768       mdlNum++;
769     }
770     if (mdlNum == modelFileNames.length)
771     {
772       return;
773     }
774
775     jmolHistory(false);
776
777     StringBuilder cmd = new StringBuilder(64);
778     cmd.append("select " + pdbResNum); // +modelNum
779
780     resetLastRes.append("select " + pdbResNum); // +modelNum
781
782     cmd.append(":");
783     resetLastRes.append(":");
784     if (!chain.equals(" "))
785     {
786       cmd.append(chain);
787       resetLastRes.append(chain);
788     }
789     {
790       cmd.append(" /" + (mdlNum + 1));
791       resetLastRes.append("/" + (mdlNum + 1));
792     }
793     cmd.append(";wireframe 100;" + cmd.toString() + " and not hetero;");
794
795     resetLastRes.append(";wireframe 0;" + resetLastRes.toString()
796             + " and not hetero; spacefill 0;");
797
798     cmd.append("spacefill 200;select none");
799
800     viewer.evalStringQuiet(cmd.toString());
801     jmolHistory(true);
802
803   }
804
805   boolean debug = true;
806
807   private void jmolHistory(boolean enable)
808   {
809     viewer.evalStringQuiet("History " + ((debug || enable) ? "on" : "off"));
810   }
811
812   public void loadInline(String string)
813   {
814     loadedInline = true;
815     // TODO: re JAL-623
816     // viewer.loadInline(strModel, isAppend);
817     // could do this:
818     // construct fake fullPathName and fileName so we can identify the file
819     // later.
820     // Then, construct pass a reader for the string to Jmol.
821     // ((org.jmol.Viewer.Viewer) viewer).loadModelFromFile(fullPathName,
822     // fileName, null, reader, false, null, null, 0);
823     viewer.openStringInline(string);
824   }
825
826   protected void mouseOverStructure(int atomIndex, final String strInfo)
827   {
828     int pdbResNum;
829     int alocsep = strInfo.indexOf("^");
830     int mdlSep = strInfo.indexOf("/");
831     int chainSeparator = strInfo.indexOf(":"), chainSeparator1 = -1;
832
833     if (chainSeparator == -1)
834     {
835       chainSeparator = strInfo.indexOf(".");
836       if (mdlSep > -1 && mdlSep < chainSeparator)
837       {
838         chainSeparator1 = chainSeparator;
839         chainSeparator = mdlSep;
840       }
841     }
842     // handle insertion codes
843     if (alocsep != -1)
844     {
845       pdbResNum = Integer.parseInt(
846               strInfo.substring(strInfo.indexOf("]") + 1, alocsep));
847
848     }
849     else
850     {
851       pdbResNum = Integer.parseInt(
852               strInfo.substring(strInfo.indexOf("]") + 1, chainSeparator));
853     }
854     String chainId;
855
856     if (strInfo.indexOf(":") > -1)
857     {
858       chainId = strInfo.substring(strInfo.indexOf(":") + 1,
859               strInfo.indexOf("."));
860     }
861     else
862     {
863       chainId = " ";
864     }
865
866     String pdbfilename = modelFileNames[frameNo]; // default is first or current
867     // model
868     if (mdlSep > -1)
869     {
870       if (chainSeparator1 == -1)
871       {
872         chainSeparator1 = strInfo.indexOf(".", mdlSep);
873       }
874       String mdlId = (chainSeparator1 > -1)
875               ? strInfo.substring(mdlSep + 1, chainSeparator1)
876               : strInfo.substring(mdlSep + 1);
877       try
878       {
879         // recover PDB filename for the model hovered over.
880         int mnumber = Integer.valueOf(mdlId).intValue() - 1;
881         if (_modelFileNameMap != null)
882         {
883           int _mp = _modelFileNameMap.length - 1;
884
885           while (mnumber < _modelFileNameMap[_mp])
886           {
887             _mp--;
888           }
889           pdbfilename = modelFileNames[_mp];
890         }
891         else
892         {
893           if (mnumber >= 0 && mnumber < modelFileNames.length)
894           {
895             pdbfilename = modelFileNames[mnumber];
896           }
897
898           if (pdbfilename == null)
899           {
900             pdbfilename = new File(viewer.ms.getModelFileName(mnumber))
901                     .getAbsolutePath();
902           }
903         }
904       } catch (Exception e)
905       {
906       }
907     }
908
909     /*
910      * highlight position on alignment(s); if some text is returned, 
911      * show this as a second line on the structure hover tooltip
912      */
913     String label = getSsm().mouseOverStructure(pdbResNum, chainId,
914             pdbfilename);
915     if (label != null)
916     {
917       StringTokenizer toks = new StringTokenizer(strInfo, " ");
918       StringBuilder sb = new StringBuilder();
919       sb.append("select ").append(String.valueOf(pdbResNum)).append(":")
920               .append(chainId).append("/1");
921       sb.append(";set hoverLabel \"").append(toks.nextToken()).append(" ")
922               .append(toks.nextToken());
923       sb.append("|").append(label).append("\"");
924       evalStateCommand(sb.toString());
925     }
926   }
927
928   public void notifyAtomHovered(int atomIndex, String strInfo, String data)
929   {
930     if (strInfo.equals(lastMessage))
931     {
932       return;
933     }
934     lastMessage = strInfo;
935     if (data != null)
936     {
937       System.err.println("Ignoring additional hover info: " + data
938               + " (other info: '" + strInfo + "' pos " + atomIndex + ")");
939     }
940     mouseOverStructure(atomIndex, strInfo);
941   }
942
943   /*
944    * { if (history != null && strStatus != null &&
945    * !strStatus.equals("Script completed")) { history.append("\n" + strStatus);
946    * } }
947    */
948
949   public void notifyAtomPicked(int atomIndex, String strInfo,
950           String strData)
951   {
952     /**
953      * this implements the toggle label behaviour copied from the original
954      * structure viewer, MCView
955      */
956     if (strData != null)
957     {
958       System.err.println("Ignoring additional pick data string " + strData);
959     }
960     int chainSeparator = strInfo.indexOf(":");
961     int p = 0;
962     if (chainSeparator == -1)
963     {
964       chainSeparator = strInfo.indexOf(".");
965     }
966
967     String picked = strInfo.substring(strInfo.indexOf("]") + 1,
968             chainSeparator);
969     String mdlString = "";
970     if ((p = strInfo.indexOf(":")) > -1)
971     {
972       picked += strInfo.substring(p, strInfo.indexOf("."));
973     }
974
975     if ((p = strInfo.indexOf("/")) > -1)
976     {
977       mdlString += strInfo.substring(p, strInfo.indexOf(" #"));
978     }
979     picked = "((" + picked + ".CA" + mdlString + ")|(" + picked + ".P"
980             + mdlString + "))";
981     jmolHistory(false);
982
983     if (!atomsPicked.contains(picked))
984     {
985       viewer.evalStringQuiet("select " + picked + ";label %n %r:%c");
986       atomsPicked.addElement(picked);
987     }
988     else
989     {
990       viewer.evalString("select " + picked + ";label off");
991       atomsPicked.removeElement(picked);
992     }
993     jmolHistory(true);
994     // TODO: in application this happens
995     //
996     // if (scriptWindow != null)
997     // {
998     // scriptWindow.sendConsoleMessage(strInfo);
999     // scriptWindow.sendConsoleMessage("\n");
1000     // }
1001
1002   }
1003
1004   @Override
1005   public void notifyCallback(CBK type, Object[] data)
1006   {
1007     try
1008     {
1009       switch (type)
1010       {
1011       case LOADSTRUCT:
1012         notifyFileLoaded((String) data[1], (String) data[2],
1013                 (String) data[3], (String) data[4],
1014                 ((Integer) data[5]).intValue());
1015
1016         break;
1017       case PICK:
1018         notifyAtomPicked(((Integer) data[2]).intValue(), (String) data[1],
1019                 (String) data[0]);
1020         // also highlight in alignment
1021         // deliberate fall through
1022       case HOVER:
1023         notifyAtomHovered(((Integer) data[2]).intValue(), (String) data[1],
1024                 (String) data[0]);
1025         break;
1026       case SCRIPT:
1027         notifyScriptTermination((String) data[2],
1028                 ((Integer) data[3]).intValue());
1029         break;
1030       case ECHO:
1031         sendConsoleEcho((String) data[1]);
1032         break;
1033       case MESSAGE:
1034         sendConsoleMessage(
1035                 (data == null) ? ((String) null) : (String) data[1]);
1036         break;
1037       case ERROR:
1038         // System.err.println("Ignoring error callback.");
1039         break;
1040       case SYNC:
1041       case RESIZE:
1042         refreshGUI();
1043         break;
1044       case MEASURE:
1045
1046       case CLICK:
1047       default:
1048         System.err.println(
1049                 "Unhandled callback " + type + " " + data[1].toString());
1050         break;
1051       }
1052     } catch (Exception e)
1053     {
1054       System.err.println("Squashed Jmol callback handler error:");
1055       e.printStackTrace();
1056     }
1057   }
1058
1059   @Override
1060   public boolean notifyEnabled(CBK callbackPick)
1061   {
1062     switch (callbackPick)
1063     {
1064     case ECHO:
1065     case LOADSTRUCT:
1066     case MEASURE:
1067     case MESSAGE:
1068     case PICK:
1069     case SCRIPT:
1070     case HOVER:
1071     case ERROR:
1072       return true;
1073     default:
1074       return false;
1075     }
1076   }
1077
1078   // incremented every time a load notification is successfully handled -
1079   // lightweight mechanism for other threads to detect when they can start
1080   // referrring to new structures.
1081   private long loadNotifiesHandled = 0;
1082
1083   public long getLoadNotifiesHandled()
1084   {
1085     return loadNotifiesHandled;
1086   }
1087
1088   public void notifyFileLoaded(String fullPathName, String fileName2,
1089           String modelName, String errorMsg, int modelParts)
1090   {
1091     if (errorMsg != null)
1092     {
1093       fileLoadingError = errorMsg;
1094       refreshGUI();
1095       return;
1096     }
1097     // TODO: deal sensibly with models loaded inLine:
1098     // modelName will be null, as will fullPathName.
1099
1100     // the rest of this routine ignores the arguments, and simply interrogates
1101     // the Jmol view to find out what structures it contains, and adds them to
1102     // the structure selection manager.
1103     fileLoadingError = null;
1104     String[] oldmodels = modelFileNames;
1105     modelFileNames = null;
1106     chainNames = new ArrayList<>();
1107     chainFile = new Hashtable<>();
1108     boolean notifyLoaded = false;
1109     String[] modelfilenames = getStructureFiles();
1110     // first check if we've lost any structures
1111     if (oldmodels != null && oldmodels.length > 0)
1112     {
1113       int oldm = 0;
1114       for (int i = 0; i < oldmodels.length; i++)
1115       {
1116         for (int n = 0; n < modelfilenames.length; n++)
1117         {
1118           if (modelfilenames[n] == oldmodels[i])
1119           {
1120             oldmodels[i] = null;
1121             break;
1122           }
1123         }
1124         if (oldmodels[i] != null)
1125         {
1126           oldm++;
1127         }
1128       }
1129       if (oldm > 0)
1130       {
1131         String[] oldmfn = new String[oldm];
1132         oldm = 0;
1133         for (int i = 0; i < oldmodels.length; i++)
1134         {
1135           if (oldmodels[i] != null)
1136           {
1137             oldmfn[oldm++] = oldmodels[i];
1138           }
1139         }
1140         // deregister the Jmol instance for these structures - we'll add
1141         // ourselves again at the end for the current structure set.
1142         getSsm().removeStructureViewerListener(this, oldmfn);
1143       }
1144     }
1145     refreshPdbEntries();
1146     for (int modelnum = 0; modelnum < modelfilenames.length; modelnum++)
1147     {
1148       String fileName = modelfilenames[modelnum];
1149       boolean foundEntry = false;
1150       StructureFile pdb = null;
1151       String pdbfile = null;
1152       // model was probably loaded inline - so check the pdb file hashcode
1153       if (loadedInline)
1154       {
1155         // calculate essential attributes for the pdb data imported inline.
1156         // prolly need to resolve modelnumber properly - for now just use our
1157         // 'best guess'
1158         pdbfile = viewer.getData(
1159                 "" + (1 + _modelFileNameMap[modelnum]) + ".0", "PDB");
1160       }
1161       // search pdbentries and sequences to find correct pdbentry for this
1162       // model
1163       for (int pe = 0; pe < getPdbCount(); pe++)
1164       {
1165         boolean matches = false;
1166         addSequence(pe, getSequence()[pe]);
1167         if (fileName == null)
1168         {
1169           if (false)
1170           // see JAL-623 - need method of matching pasted data up
1171           {
1172             pdb = getSsm().setMapping(getSequence()[pe], getChains()[pe],
1173                     pdbfile, DataSourceType.PASTE,
1174                     getIProgressIndicator());
1175             getPdbEntry(modelnum).setFile("INLINE" + pdb.getId());
1176             matches = true;
1177             foundEntry = true;
1178           }
1179         }
1180         else
1181         {
1182           File fl = new File(getPdbEntry(pe).getFile());
1183           matches = fl.equals(new File(fileName));
1184           if (matches)
1185           {
1186             foundEntry = true;
1187             // TODO: Jmol can in principle retrieve from CLASSLOADER but
1188             // this
1189             // needs
1190             // to be tested. See mantis bug
1191             // https://mantis.lifesci.dundee.ac.uk/view.php?id=36605
1192             DataSourceType protocol = DataSourceType.URL;
1193             try
1194             {
1195               if (fl.exists())
1196               {
1197                 protocol = DataSourceType.FILE;
1198               }
1199             } catch (Exception e)
1200             {
1201             } catch (Error e)
1202             {
1203             }
1204             // Explicitly map to the filename used by Jmol ;
1205             pdb = getSsm().setMapping(getSequence()[pe], getChains()[pe],
1206                     fileName, protocol, getIProgressIndicator());
1207             // pdbentry[pe].getFile(), protocol);
1208
1209           }
1210         }
1211         if (matches)
1212         {
1213           // add an entry for every chain in the model
1214           for (int i = 0; i < pdb.getChains().size(); i++)
1215           {
1216             String chid = new String(
1217                     pdb.getId() + ":" + pdb.getChains().elementAt(i).id);
1218             chainFile.put(chid, fileName);
1219             chainNames.add(chid);
1220           }
1221           notifyLoaded = true;
1222         }
1223       }
1224
1225       if (!foundEntry && associateNewStructs)
1226       {
1227         // this is a foreign pdb file that jalview doesn't know about - add
1228         // it to the dataset and try to find a home - either on a matching
1229         // sequence or as a new sequence.
1230         String pdbcontent = viewer.getData("/" + (modelnum + 1) + ".1",
1231                 "PDB");
1232         // parse pdb file into a chain, etc.
1233         // locate best match for pdb in associated views and add mapping to
1234         // ssm
1235         // if properly registered then
1236         notifyLoaded = true;
1237
1238       }
1239     }
1240     // FILE LOADED OK
1241     // so finally, update the jmol bits and pieces
1242     // if (jmolpopup != null)
1243     // {
1244     // // potential for deadlock here:
1245     // // jmolpopup.updateComputedMenus();
1246     // }
1247     if (!isLoadingFromArchive())
1248     {
1249       viewer.evalStringQuiet(
1250               "model *; select backbone;restrict;cartoon;wireframe off;spacefill off");
1251     }
1252     // register ourselves as a listener and notify the gui that it needs to
1253     // update itself.
1254     getSsm().addStructureViewerListener(this);
1255     if (notifyLoaded)
1256     {
1257       FeatureRenderer fr = getFeatureRenderer(null);
1258       if (fr != null)
1259       {
1260         fr.featuresAdded();
1261       }
1262       refreshGUI();
1263       loadNotifiesHandled++;
1264     }
1265     setLoadingFromArchive(false);
1266   }
1267
1268   @Override
1269   public List<String> getChainNames()
1270   {
1271     return chainNames;
1272   }
1273
1274   protected IProgressIndicator getIProgressIndicator()
1275   {
1276     return null;
1277   }
1278
1279   public void notifyNewPickingModeMeasurement(int iatom, String strMeasure)
1280   {
1281     notifyAtomPicked(iatom, strMeasure, null);
1282   }
1283
1284   public abstract void notifyScriptTermination(String strStatus,
1285           int msWalltime);
1286
1287   /**
1288    * display a message echoed from the jmol viewer
1289    * 
1290    * @param strEcho
1291    */
1292   public abstract void sendConsoleEcho(String strEcho); /*
1293                                                          * { showConsole(true);
1294                                                          * 
1295                                                          * history.append("\n" +
1296                                                          * strEcho); }
1297                                                          */
1298
1299   // /End JmolStatusListener
1300   // /////////////////////////////
1301
1302   /**
1303    * @param strStatus
1304    *          status message - usually the response received after a script
1305    *          executed
1306    */
1307   public abstract void sendConsoleMessage(String strStatus);
1308
1309   @Override
1310   public void setCallbackFunction(String callbackType,
1311           String callbackFunction)
1312   {
1313     System.err.println("Ignoring set-callback request to associate "
1314             + callbackType + " with function " + callbackFunction);
1315
1316   }
1317
1318   @Override
1319   public void setJalviewColourScheme(ColourSchemeI cs)
1320   {
1321     colourBySequence = false;
1322
1323     if (cs == null)
1324     {
1325       return;
1326     }
1327
1328     jmolHistory(false);
1329     StringBuilder command = new StringBuilder(128);
1330     command.append("select *;color white;");
1331     List<String> residueSet = ResidueProperties.getResidues(isNucleotide(),
1332             false);
1333     for (String resName : residueSet)
1334     {
1335       char res = resName.length() == 3
1336               ? ResidueProperties.getSingleCharacterCode(resName)
1337               : resName.charAt(0);
1338       Color col = cs.findColour(res, 0, null, null, 0f);
1339       command.append("select " + resName + ";color[" + col.getRed() + ","
1340               + col.getGreen() + "," + col.getBlue() + "];");
1341     }
1342
1343     evalStateCommand(command.toString());
1344     jmolHistory(true);
1345   }
1346
1347   public void showHelp()
1348   {
1349     showUrl("http://jmol.sourceforge.net/docs/JmolUserGuide/", "jmolHelp");
1350   }
1351
1352   /**
1353    * open the URL somehow
1354    * 
1355    * @param target
1356    */
1357   public abstract void showUrl(String url, String target);
1358
1359   /**
1360    * called when the binding thinks the UI needs to be refreshed after a Jmol
1361    * state change. this could be because structures were loaded, or because an
1362    * error has occured.
1363    */
1364   public abstract void refreshGUI();
1365
1366   /**
1367    * called to show or hide the associated console window container.
1368    * 
1369    * @param show
1370    */
1371   public abstract void showConsole(boolean show);
1372
1373   /**
1374    * @param renderPanel
1375    * @param jmolfileio
1376    *          - when true will initialise jmol's file IO system (should be false
1377    *          in applet context)
1378    * @param htmlName
1379    * @param documentBase
1380    * @param codeBase
1381    * @param commandOptions
1382    */
1383   public void allocateViewer(Container renderPanel, boolean jmolfileio,
1384           String htmlName, URL documentBase, URL codeBase,
1385           String commandOptions)
1386   {
1387     allocateViewer(renderPanel, jmolfileio, htmlName, documentBase,
1388             codeBase, commandOptions, null, null);
1389   }
1390
1391   /**
1392    * 
1393    * @param renderPanel
1394    * @param jmolfileio
1395    *          - when true will initialise jmol's file IO system (should be false
1396    *          in applet context)
1397    * @param htmlName
1398    * @param documentBase
1399    * @param codeBase
1400    * @param commandOptions
1401    * @param consolePanel
1402    *          - panel to contain Jmol console
1403    * @param buttonsToShow
1404    *          - buttons to show on the console, in ordr
1405    */
1406   public void allocateViewer(Container renderPanel, boolean jmolfileio,
1407           String htmlName, URL documentBase, URL codeBase,
1408           String commandOptions, final Container consolePanel,
1409           String buttonsToShow)
1410   {
1411     if (commandOptions == null)
1412     {
1413       commandOptions = "";
1414     }
1415     viewer = (Viewer) JmolViewer.allocateViewer(renderPanel,
1416             (jmolfileio ? new SmarterJmolAdapter() : null),
1417             htmlName + ((Object) this).toString(), documentBase, codeBase,
1418             commandOptions, this);
1419
1420     viewer.setJmolStatusListener(this); // extends JmolCallbackListener
1421
1422     console = createJmolConsole(consolePanel, buttonsToShow);
1423     if (consolePanel != null)
1424     {
1425       consolePanel.addComponentListener(this);
1426
1427     }
1428
1429   }
1430
1431   protected abstract JmolAppConsoleInterface createJmolConsole(
1432           Container consolePanel, String buttonsToShow);
1433
1434   protected org.jmol.api.JmolAppConsoleInterface console = null;
1435
1436   @Override
1437   public void setBackgroundColour(java.awt.Color col)
1438   {
1439     jmolHistory(false);
1440     viewer.evalStringQuiet("background [" + col.getRed() + ","
1441             + col.getGreen() + "," + col.getBlue() + "];");
1442     jmolHistory(true);
1443   }
1444
1445   @Override
1446   public int[] resizeInnerPanel(String data)
1447   {
1448     // Jalview doesn't honour resize panel requests
1449     return null;
1450   }
1451
1452   /**
1453    * 
1454    */
1455   protected void closeConsole()
1456   {
1457     if (console != null)
1458     {
1459       try
1460       {
1461         console.setVisible(false);
1462       } catch (Error e)
1463       {
1464       } catch (Exception x)
1465       {
1466       }
1467       ;
1468       console = null;
1469     }
1470   }
1471
1472   /**
1473    * ComponentListener method
1474    */
1475   @Override
1476   public void componentMoved(ComponentEvent e)
1477   {
1478   }
1479
1480   /**
1481    * ComponentListener method
1482    */
1483   @Override
1484   public void componentResized(ComponentEvent e)
1485   {
1486   }
1487
1488   /**
1489    * ComponentListener method
1490    */
1491   @Override
1492   public void componentShown(ComponentEvent e)
1493   {
1494     showConsole(true);
1495   }
1496
1497   /**
1498    * ComponentListener method
1499    */
1500   @Override
1501   public void componentHidden(ComponentEvent e)
1502   {
1503     showConsole(false);
1504   }
1505 }