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