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