0907695a24e7144b63a3f924e0a3f2b61f3121b9
[jalview.git] / src / jalview / ext / rbvi / chimera / JalviewChimeraBinding.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.rbvi.chimera;
22
23 import java.io.File;
24 import java.io.FileOutputStream;
25 import java.io.IOException;
26 import java.io.PrintWriter;
27 import java.net.BindException;
28 import java.util.ArrayList;
29 import java.util.Collections;
30 import java.util.Iterator;
31 import java.util.LinkedHashMap;
32 import java.util.List;
33 import java.util.Map;
34
35 import ext.edu.ucsf.rbvi.strucviz2.ChimeraManager;
36 import ext.edu.ucsf.rbvi.strucviz2.ChimeraModel;
37 import ext.edu.ucsf.rbvi.strucviz2.StructureManager;
38 import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType;
39 import jalview.api.AlignViewportI;
40 import jalview.api.AlignmentViewPanel;
41 import jalview.api.structures.JalviewStructureDisplayI;
42 import jalview.bin.Cache;
43 import jalview.datamodel.AlignmentI;
44 import jalview.datamodel.PDBEntry;
45 import jalview.datamodel.SearchResultMatchI;
46 import jalview.datamodel.SearchResultsI;
47 import jalview.datamodel.SequenceFeature;
48 import jalview.datamodel.SequenceI;
49 import jalview.gui.StructureViewer.ViewerType;
50 import jalview.httpserver.AbstractRequestHandler;
51 import jalview.io.DataSourceType;
52 import jalview.structure.AtomSpec;
53 import jalview.structure.AtomSpecModel;
54 import jalview.structure.StructureCommand;
55 import jalview.structure.StructureCommandI;
56 import jalview.structure.StructureSelectionManager;
57 import jalview.structures.models.AAStructureBindingModel;
58
59 public abstract class JalviewChimeraBinding extends AAStructureBindingModel
60 {
61   public static final String CHIMERA_SESSION_EXTENSION = ".py";
62
63   public static final String CHIMERA_FEATURE_GROUP = "Chimera";
64
65   /*
66    * Object through which we talk to Chimera
67    */
68   private ChimeraManager chimeraManager;
69
70   /*
71    * Object which listens to Chimera notifications
72    */
73   private AbstractRequestHandler chimeraListener;
74
75   /*
76    * Map of ChimeraModel objects keyed by PDB full local file name
77    */
78   protected Map<String, List<ChimeraModel>> chimeraMaps = new LinkedHashMap<>();
79
80   String lastHighlightCommand;
81
82   private Thread chimeraMonitor;
83
84   /**
85    * Open a PDB structure file in Chimera and set up mappings from Jalview.
86    * 
87    * We check if the PDB model id is already loaded in Chimera, if so don't reopen
88    * it. This is the case if Chimera has opened a saved session file.
89    * 
90    * @param pe
91    * @return
92    */
93   public boolean openFile(PDBEntry pe)
94   {
95     String file = pe.getFile();
96     try
97     {
98       List<ChimeraModel> modelsToMap = new ArrayList<>();
99       List<ChimeraModel> oldList = chimeraManager.getModelList();
100       boolean alreadyOpen = false;
101
102       /*
103        * If Chimera already has this model, don't reopen it, but do remap it.
104        */
105       for (ChimeraModel open : oldList)
106       {
107         if (open.getModelName().equals(pe.getId()))
108         {
109           alreadyOpen = true;
110           modelsToMap.add(open);
111         }
112       }
113
114       /*
115        * If Chimera doesn't yet have this model, ask it to open it, and retrieve
116        * the model name(s) added by Chimera.
117        */
118       if (!alreadyOpen)
119       {
120         chimeraManager.openModel(file, pe.getId(), ModelType.PDB_MODEL);
121         addChimeraModel(pe, modelsToMap);
122       }
123
124       chimeraMaps.put(file, modelsToMap);
125
126       if (getSsm() != null)
127       {
128         getSsm().addStructureViewerListener(this);
129       }
130       return true;
131     } catch (Exception q)
132     {
133       log("Exception when trying to open model " + file + "\n"
134               + q.toString());
135       q.printStackTrace();
136     }
137     return false;
138   }
139
140   /**
141    * Adds the ChimeraModel corresponding to the given PDBEntry, based on model
142    * name matching PDB id
143    * 
144    * @param pe
145    * @param modelsToMap
146    */
147   protected void addChimeraModel(PDBEntry pe,
148           List<ChimeraModel> modelsToMap)
149   {
150     /*
151      * Chimera: query for actual models and find the one with
152      * matching model name - already set in viewer.openModel()
153      */
154     List<ChimeraModel> newList = chimeraManager.getModelList();
155     // JAL-1728 newList.removeAll(oldList) does not work
156     for (ChimeraModel cm : newList)
157     {
158       if (cm.getModelName().equals(pe.getId()))
159       {
160         modelsToMap.add(cm);
161       }
162     }
163   }
164
165   /**
166    * Constructor
167    * 
168    * @param ssm
169    * @param pdbentry
170    * @param sequenceIs
171    * @param protocol
172    */
173   public JalviewChimeraBinding(StructureSelectionManager ssm,
174           PDBEntry[] pdbentry, SequenceI[][] sequenceIs,
175           DataSourceType protocol)
176   {
177     super(ssm, pdbentry, sequenceIs, protocol);
178     chimeraManager = new ChimeraManager(new StructureManager(true));
179     chimeraManager.setChimeraX(ViewerType.CHIMERAX.equals(getViewerType()));
180     setStructureCommands(new ChimeraCommands());
181   }
182
183   @Override
184   protected ViewerType getViewerType()
185   {
186     return ViewerType.CHIMERA;
187   }
188
189   /**
190    * Starts a thread that waits for the Chimera process to finish, so that we can
191    * then close the associated resources. This avoids leaving orphaned Chimera
192    * viewer panels in Jalview if the user closes Chimera.
193    */
194   protected void startChimeraProcessMonitor()
195   {
196     final Process p = chimeraManager.getChimeraProcess();
197     chimeraMonitor = new Thread(new Runnable()
198     {
199
200       @Override
201       public void run()
202       {
203         try
204         {
205           p.waitFor();
206           JalviewStructureDisplayI display = getViewer();
207           if (display != null)
208           {
209             display.closeViewer(false);
210           }
211         } catch (InterruptedException e)
212         {
213           // exit thread if Chimera Viewer is closed in Jalview
214         }
215       }
216     });
217     chimeraMonitor.start();
218   }
219
220   /**
221    * Start a dedicated HttpServer to listen for Chimera notifications, and tell it
222    * to start listening
223    */
224   public void startChimeraListener()
225   {
226     try
227     {
228       chimeraListener = new ChimeraListener(this);
229       chimeraManager.startListening(chimeraListener.getUri());
230     } catch (BindException e)
231     {
232       System.err.println(
233               "Failed to start Chimera listener: " + e.getMessage());
234     }
235   }
236
237   /**
238    * Close down the Jalview viewer and listener, and (optionally) the associated
239    * Chimera window.
240    */
241   @Override
242   public void closeViewer(boolean closeChimera)
243   {
244     super.closeViewer(closeChimera);
245     if (closeChimera)
246     {
247       chimeraManager.exitChimera();
248     }
249     if (this.chimeraListener != null)
250     {
251       chimeraListener.shutdown();
252       chimeraListener = null;
253     }
254     chimeraManager = null;
255
256     if (chimeraMonitor != null)
257     {
258       chimeraMonitor.interrupt();
259     }
260   }
261
262   /**
263    * Helper method to construct model spec in Chimera format:
264    * <ul>
265    * <li>#0 (#1 etc) for a PDB file with no sub-models</li>
266    * <li>#0.1 (#1.1 etc) for a PDB file with sub-models</li>
267    * <ul>
268    * Note for now we only ever choose the first of multiple models. This
269    * corresponds to the hard-coded Jmol equivalent (compare {1.1}). Refactor in
270    * future if there is a need to select specific sub-models.
271    * 
272    * @param pdbfnum
273    * @return
274    */
275   @Override
276   public String getModelSpec(int pdbfnum)
277   {
278     if (pdbfnum < 0 || pdbfnum >= getPdbCount())
279     {
280       return "#" + pdbfnum; // temp hack for ChimeraX
281     }
282
283     /*
284      * For now, the test for having sub-models is whether multiple Chimera
285      * models are mapped for the PDB file; the models are returned as a response
286      * to the Chimera command 'list models type molecule', see
287      * ChimeraManager.getModelList().
288      */
289     String[] structureFiles = getStructureFiles();
290     if (pdbfnum < 0 || pdbfnum >= structureFiles.length)
291     {
292       return "";
293     }
294
295     List<ChimeraModel> maps = chimeraMaps.get(structureFiles[pdbfnum]);
296     boolean hasSubModels = maps != null && maps.size() > 1;
297     String spec = "#" + String.valueOf(pdbfnum);
298     return hasSubModels ? spec + ".1" : spec;
299   }
300
301   /**
302    * Launch Chimera, unless an instance linked to this object is already
303    * running. Returns true if Chimera is successfully launched, or already
304    * running, else false.
305    * 
306    * @return
307    */
308   public boolean launchChimera()
309   {
310     if (chimeraManager.isChimeraLaunched())
311     {
312       return true;
313     }
314
315     boolean launched = chimeraManager.launchChimera(getChimeraPaths());
316     if (launched)
317     {
318       startChimeraProcessMonitor();
319     }
320     else
321     {
322       log("Failed to launch Chimera!");
323     }
324     return launched;
325   }
326
327   /**
328    * Returns a list of candidate paths to the Chimera program executable
329    * 
330    * @return
331    */
332   protected List<String> getChimeraPaths()
333   {
334     return StructureManager.getChimeraPaths(false);
335   }
336
337   /**
338    * Answers true if the Chimera process is still running, false if ended or not
339    * started.
340    * 
341    * @return
342    */
343   @Override
344   public boolean isViewerRunning()
345   {
346     return chimeraManager.isChimeraLaunched();
347   }
348
349   /**
350    * Send a command to Chimera, and optionally log and return any responses.
351    * 
352    * @param command
353    * @param getResponse
354    */
355   @Override
356   public List<String> executeCommand(final StructureCommandI command,
357           boolean getResponse)
358   {
359     if (chimeraManager == null || command == null)
360     {
361       // ? thread running after viewer shut down
362       return null;
363     }
364     List<String> reply = null;
365     // trim command or it may never find a match in the replyLog!!
366     String cmd = command.getCommand().trim();
367     List<String> lastReply = chimeraManager
368             .sendChimeraCommand(cmd, getResponse);
369     if (getResponse)
370     {
371       reply = lastReply;
372       Cache.log.debug(
373               "Response from command ('" + cmd + "') was:\n" + lastReply);
374     }
375
376     return reply;
377   }
378
379   @Override
380   public synchronized String[] getStructureFiles()
381   {
382     if (chimeraManager == null)
383     {
384       return new String[0];
385     }
386
387     return chimeraMaps.keySet()
388             .toArray(modelFileNames = new String[chimeraMaps.size()]);
389   }
390
391   /**
392    * Construct and send a command to highlight zero, one or more atoms. We do this
393    * by sending an "rlabel" command to show the residue label at that position.
394    */
395   @Override
396   public void highlightAtoms(List<AtomSpec> atoms)
397   {
398     if (atoms == null || atoms.size() == 0)
399     {
400       return;
401     }
402
403     boolean forChimeraX = chimeraManager.isChimeraX();
404     StringBuilder cmd = new StringBuilder(128);
405     boolean first = true;
406     boolean found = false;
407
408     for (AtomSpec atom : atoms)
409     {
410       int pdbResNum = atom.getPdbResNum();
411       String chain = atom.getChain();
412       String pdbfile = atom.getPdbFile();
413       List<ChimeraModel> cms = chimeraMaps.get(pdbfile);
414       if (cms != null && !cms.isEmpty())
415       {
416         if (first)
417         {
418           cmd.append(forChimeraX ? "label #" : "rlabel #");
419         }
420         else
421         {
422           cmd.append(",");
423         }
424         first = false;
425         if (forChimeraX)
426         {
427           cmd.append(cms.get(0).getModelNumber())
428                   .append("/").append(chain).append(":").append(pdbResNum);
429         }
430         else
431         {
432           cmd.append(cms.get(0).getModelNumber())
433                   .append(":").append(pdbResNum);
434           if (!chain.equals(" ") && !forChimeraX)
435           {
436             cmd.append(".").append(chain);
437           }
438         }
439         found = true;
440       }
441     }
442     String command = cmd.toString();
443
444     /*
445      * avoid repeated commands for the same residue
446      */
447     if (command.equals(lastHighlightCommand))
448     {
449       return;
450     }
451
452     /*
453      * unshow the label for the previous residue
454      */
455     if (lastHighlightCommand != null)
456     {
457       chimeraManager.sendChimeraCommand("~" + lastHighlightCommand, false);
458     }
459     if (found)
460     {
461       chimeraManager.sendChimeraCommand(command, false);
462     }
463     this.lastHighlightCommand = command;
464   }
465
466   /**
467    * Query Chimera for its current selection, and highlight it on the alignment
468    */
469   public void highlightChimeraSelection()
470   {
471     /*
472      * Ask Chimera for its current selection
473      */
474     List<String> selection = chimeraManager.getSelectedResidueSpecs();
475
476     /*
477      * Parse model number, residue and chain for each selected position,
478      * formatted as #0:123.A or #1.2:87.B (#model.submodel:residue.chain)
479      */
480     List<AtomSpec> atomSpecs = convertStructureResiduesToAlignment(
481             selection);
482
483     /*
484      * Broadcast the selection (which may be empty, if the user just cleared all
485      * selections)
486      */
487     getSsm().mouseOverStructure(atomSpecs);
488   }
489
490   /**
491    * Converts a list of Chimera atomspecs to a list of AtomSpec representing the
492    * corresponding residues (if any) in Jalview
493    * 
494    * @param structureSelection
495    * @return
496    */
497   protected List<AtomSpec> convertStructureResiduesToAlignment(
498           List<String> structureSelection)
499   {
500     boolean chimeraX = chimeraManager.isChimeraX();
501     List<AtomSpec> atomSpecs = new ArrayList<>();
502     for (String atomSpec : structureSelection)
503     {
504       try
505       {
506         AtomSpec spec = AtomSpec.fromChimeraAtomspec(atomSpec, chimeraX);
507         String pdbfilename = getPdbFileForModel(spec.getModelNumber());
508         spec.setPdbFile(pdbfilename);
509         atomSpecs.add(spec);
510       } catch (IllegalArgumentException e)
511       {
512         System.err.println("Failed to parse atomspec: " + atomSpec);
513       }
514     }
515     return atomSpecs;
516   }
517
518   /**
519    * @param modelId
520    * @return
521    */
522   protected String getPdbFileForModel(int modelId)
523   {
524     /*
525      * Work out the pdbfilename from the model number
526      */
527     String pdbfilename = modelFileNames[0];
528     findfileloop: for (String pdbfile : this.chimeraMaps.keySet())
529     {
530       for (ChimeraModel cm : chimeraMaps.get(pdbfile))
531       {
532         if (cm.getModelNumber() == modelId)
533         {
534           pdbfilename = pdbfile;
535           break findfileloop;
536         }
537       }
538     }
539     return pdbfilename;
540   }
541
542   private void log(String message)
543   {
544     System.err.println("## Chimera log: " + message);
545   }
546
547   /**
548    * Send a 'show' command for all atoms in the currently selected columns
549    * 
550    * TODO: pull up to abstract structure viewer interface
551    * 
552    * @param vp
553    */
554   public void highlightSelection(AlignmentViewPanel vp)
555   {
556     List<Integer> cols = vp.getAlignViewport().getColumnSelection()
557             .getSelected();
558     AlignmentI alignment = vp.getAlignment();
559     StructureSelectionManager sm = getSsm();
560     for (SequenceI seq : alignment.getSequences())
561     {
562       /*
563        * convert selected columns into sequence positions
564        */
565       int[] positions = new int[cols.size()];
566       int i = 0;
567       for (Integer col : cols)
568       {
569         positions[i++] = seq.findPosition(col);
570       }
571       sm.highlightStructure(this, seq, positions);
572     }
573   }
574
575   /**
576    * Constructs and send commands to Chimera to set attributes on residues for
577    * features visible in Jalview.
578    * <p>
579    * The syntax is: setattr r &lt;attName&gt; &lt;attValue&gt; &lt;atomSpec&gt;
580    * <p>
581    * For example: setattr r jv_chain "Ferredoxin-1, Chloroplastic" #0:94.A
582    * 
583    * @param avp
584    * @return
585    */
586   public int sendFeaturesToViewer(AlignmentViewPanel avp)
587   {
588     // TODO refactor as required to pull up to an interface
589     Map<String, Map<Object, AtomSpecModel>> featureValues = buildFeaturesMap(
590             avp);
591     List<StructureCommandI> commands = getCommandGenerator()
592             .setAttributes(featureValues);
593     if (commands.size() > 10)
594     {
595       sendCommandsByFile(commands);
596     }
597     else
598     {
599       for (StructureCommandI command : commands)
600       {
601         sendAsynchronousCommand(command, null);
602       }
603     }
604     return commands.size();
605   }
606
607   /**
608    * Write commands to a temporary file, and send a command to Chimera to open the
609    * file as a commands script. For use when sending a large number of separate
610    * commands would overload the REST interface mechanism.
611    * 
612    * @param commands
613    */
614   protected void sendCommandsByFile(List<StructureCommandI> commands)
615   {
616     try
617     {
618       File tmp = File.createTempFile("chim", getCommandFileExtension());
619       tmp.deleteOnExit();
620       PrintWriter out = new PrintWriter(new FileOutputStream(tmp));
621       for (StructureCommandI command : commands)
622       {
623         out.println(command.getCommand());
624       }
625       out.flush();
626       out.close();
627       String path = tmp.getAbsolutePath();
628       StructureCommandI command = getCommandGenerator()
629               .openCommandFile(path);
630       sendAsynchronousCommand(command, null);
631     } catch (IOException e)
632     {
633       System.err.println("Sending commands to Chimera via file failed with "
634               + e.getMessage());
635     }
636   }
637
638   /**
639    * Returns the file extension required for a file of commands to be read by
640    * the structure viewer
641    * @return
642    */
643   protected String getCommandFileExtension()
644   {
645     return ".com";
646   }
647
648   /**
649    * Get Chimera residues which have the named attribute, find the mapped
650    * positions in the Jalview sequence(s), and set as sequence features
651    * 
652    * @param attName
653    * @param alignmentPanel
654    */
655   public void copyStructureAttributesToFeatures(String attName,
656           AlignmentViewPanel alignmentPanel)
657   {
658     // todo pull up to AAStructureBindingModel (and interface?)
659
660     /*
661      * ask Chimera to list residues with the attribute, reporting its value
662      */
663     // this alternative command
664     // list residues spec ':*/attName' attr attName
665     // doesn't report 'None' values (which is good), but
666     // fails for 'average.bfactor' (which is bad):
667
668     String cmd = "list residues attr '" + attName + "'";
669     List<String> residues = executeCommand(new StructureCommand(cmd), true);
670
671     boolean featureAdded = createFeaturesForAttributes(attName, residues);
672     if (featureAdded)
673     {
674       alignmentPanel.getFeatureRenderer().featuresAdded();
675     }
676   }
677
678   /**
679    * Create features in Jalview for the given attribute name and structure
680    * residues.
681    * 
682    * <pre>
683    * The residue list should be 0, 1 or more reply lines of the format: 
684    *     residue id #0:5.A isHelix -155.000836316 index 5 
685    * or 
686    *     residue id #0:6.A isHelix None
687    * </pre>
688    * 
689    * @param attName
690    * @param residues
691    * @return
692    */
693   protected boolean createFeaturesForAttributes(String attName,
694           List<String> residues)
695   {
696     boolean featureAdded = false;
697     String featureGroup = getViewerFeatureGroup();
698     boolean chimeraX = chimeraManager.isChimeraX();
699
700     for (String residue : residues)
701     {
702       AtomSpec spec = null;
703       String[] tokens = residue.split(" ");
704       if (tokens.length < 5)
705       {
706         continue;
707       }
708       String atomSpec = tokens[2];
709       String attValue = tokens[4];
710
711       /*
712        * ignore 'None' (e.g. for phi) or 'False' (e.g. for isHelix)
713        */
714       if ("None".equalsIgnoreCase(attValue)
715               || "False".equalsIgnoreCase(attValue))
716       {
717         continue;
718       }
719
720       try
721       {
722         spec = AtomSpec.fromChimeraAtomspec(atomSpec, chimeraX);
723       } catch (IllegalArgumentException e)
724       {
725         System.err.println("Problem parsing atomspec " + atomSpec);
726         continue;
727       }
728
729       String chainId = spec.getChain();
730       String description = attValue;
731       float score = Float.NaN;
732       try
733       {
734         score = Float.valueOf(attValue);
735         description = chainId;
736       } catch (NumberFormatException e)
737       {
738         // was not a float value
739       }
740
741       String pdbFile = getPdbFileForModel(spec.getModelNumber());
742       spec.setPdbFile(pdbFile);
743
744       List<AtomSpec> atoms = Collections.singletonList(spec);
745
746       /*
747        * locate the mapped position in the alignment (if any)
748        */
749       SearchResultsI sr = getSsm()
750               .findAlignmentPositionsForStructurePositions(atoms);
751
752       /*
753        * expect one matched alignment position, or none 
754        * (if the structure position is not mapped)
755        */
756       for (SearchResultMatchI m : sr.getResults())
757       {
758         SequenceI seq = m.getSequence();
759         int start = m.getStart();
760         int end = m.getEnd();
761         SequenceFeature sf = new SequenceFeature(attName, description,
762                 start, end, score, featureGroup);
763         // todo: should SequenceFeature have an explicit property for chain?
764         // note: repeating the action shouldn't duplicate features
765         featureAdded |= seq.addSequenceFeature(sf);
766       }
767     }
768     return featureAdded;
769   }
770
771   /**
772    * Answers the feature group name to apply to features created in Jalview from
773    * Chimera attributes
774    * 
775    * @return
776    */
777   protected String getViewerFeatureGroup()
778   {
779     // todo pull up to interface
780     return CHIMERA_FEATURE_GROUP;
781   }
782
783   @Override
784   public String getModelIdForFile(String pdbFile)
785   {
786     List<ChimeraModel> foundModels = chimeraMaps.get(pdbFile);
787     if (foundModels != null && !foundModels.isEmpty())
788     {
789       return String.valueOf(foundModels.get(0).getModelNumber());
790     }
791     return "";
792   }
793
794   /**
795    * Answers a (possibly empty) list of attribute names in Chimera[X], excluding
796    * any which were added from Jalview
797    * 
798    * @return
799    */
800   public List<String> getChimeraAttributes()
801   {
802     List<String> atts = chimeraManager.getAttrList();
803     Iterator<String> it = atts.iterator();
804     while (it.hasNext())
805     {
806       if (it.next().startsWith(ChimeraCommands.NAMESPACE_PREFIX))
807       {
808         /*
809          * attribute added from Jalview - exclude it
810          */
811         it.remove();
812       }
813     }
814     return atts;
815   }
816
817   /**
818    * Returns the file extension to use for a saved viewer session file (.py)
819    * 
820    * @return
821    */
822   @Override
823   public String getSessionFileExtension()
824   {
825     return CHIMERA_SESSION_EXTENSION;
826   }
827
828   @Override
829   public String getHelpURL()
830   {
831     return "https://www.cgl.ucsf.edu/chimera/docs/UsersGuide";
832   }
833
834   @Override
835   public void showStructures(AlignViewportI av, boolean refocus)
836   {
837     StructureCommandI cmd = buildShowStructuresCommand(av, refocus);
838     executeCommand(cmd, false);
839   }
840
841   /**
842    * Builds a command to show parts of the structure, depending on whether
843    * <ul>
844    * <li>all structures or regions mapped to alignment only are shown</li>
845    * <li>all chains or only selected chains are shown</li>
846    * </ul>
847    * 
848    * @param av
849    * @param refocus
850    * @return
851    */
852   protected StructureCommandI buildShowStructuresCommand(
853           AlignViewportI av,
854           boolean refocus)
855   {
856     // TODO refactor using command generator
857     // pull up this method and Jmol variant to base class
858     StringBuilder cmd = new StringBuilder(128);
859     cmd.append("~display");
860
861     if (isShowAlignmentOnly())
862     {
863       AtomSpecModel model = getShownResidues(av);
864       String atomSpec = getCommandGenerator().getAtomSpec(model, false);
865       if (!atomSpec.isEmpty())
866       {
867         cmd.append("; ~ribbon; ribbon ").append(atomSpec);
868       }
869     }
870     else
871     {
872       cmd.append("; ribbon");
873     }
874
875     /*
876      * hide any chains selected not to be shown (whether mapped to
877      * sequence in the alignment or not)
878      */
879     for (String pdbChain : chainsToHide)
880     {
881       String chainId = pdbChain.split(":")[1];
882       String modelNo = getModelIdForFile(getFileForChain(pdbChain));
883       if (!"".equals(modelNo))
884       {
885         cmd.append("; ~ribbon #").append(modelNo).append(":.")
886                 .append(chainId);
887       }
888     }
889     if (refocus)
890     {
891       cmd.append("; focus");
892     }
893     return new StructureCommand(cmd.toString());
894   }
895
896   @Override
897   public int getModelForPdbFile(String fileName, int fileIndex)
898   {
899     if (chimeraMaps.containsKey(fileName))
900     {
901       List<ChimeraModel> models = chimeraMaps.get(fileName);
902       if (!models.isEmpty())
903       {
904         return models.get(0).getModelNumber();
905       }
906     }
907     return -1;
908   }
909 }