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