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