Merge branch 'releases/Release_2_11_3_Branch'
[jalview.git] / src / jalview / structures / models / AAStructureBindingModel.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.structures.models;
22
23 import java.awt.Color;
24 import java.io.File;
25 import java.io.IOException;
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.BitSet;
29 import java.util.Collections;
30 import java.util.HashMap;
31 import java.util.LinkedHashMap;
32 import java.util.List;
33 import java.util.Locale;
34 import java.util.Map;
35
36 import javax.swing.SwingUtilities;
37
38 import jalview.api.AlignViewportI;
39 import jalview.api.AlignmentViewPanel;
40 import jalview.api.FeatureRenderer;
41 import jalview.api.SequenceRenderer;
42 import jalview.api.StructureSelectionManagerProvider;
43 import jalview.api.structures.JalviewStructureDisplayI;
44 import jalview.bin.Console;
45 import jalview.datamodel.AlignmentI;
46 import jalview.datamodel.ColumnSelection;
47 import jalview.datamodel.HiddenColumns;
48 import jalview.datamodel.MappedFeatures;
49 import jalview.datamodel.PDBEntry;
50 import jalview.datamodel.SequenceFeature;
51 import jalview.datamodel.SequenceI;
52 import jalview.ext.rbvi.chimera.JalviewChimeraBinding;
53 import jalview.gui.AlignmentPanel;
54 import jalview.gui.Desktop;
55 import jalview.gui.StructureViewer.ViewerType;
56 import jalview.io.DataSourceType;
57 import jalview.io.StructureFile;
58 import jalview.renderer.seqfeatures.FeatureColourFinder;
59 import jalview.schemes.ColourSchemeI;
60 import jalview.schemes.ResidueProperties;
61 import jalview.structure.AtomSpec;
62 import jalview.structure.AtomSpecModel;
63 import jalview.structure.StructureCommandI;
64 import jalview.structure.StructureCommandsI;
65 import jalview.structure.StructureCommandsI.AtomSpecType;
66 import jalview.structure.StructureListener;
67 import jalview.structure.StructureMapping;
68 import jalview.structure.StructureSelectionManager;
69 import jalview.util.Comparison;
70 import jalview.util.MessageManager;
71
72 /**
73  * 
74  * A base class to hold common function for 3D structure model binding. Initial
75  * version created by refactoring JMol and Chimera binding models, but other
76  * structure viewers could in principle be accommodated in future.
77  * 
78  * @author gmcarstairs
79  *
80  */
81 public abstract class AAStructureBindingModel
82         extends SequenceStructureBindingModel
83         implements StructureListener, StructureSelectionManagerProvider
84 {
85   /**
86    * Data bean class to simplify parameterisation in superposeStructures
87    */
88   public static class SuperposeData
89   {
90     public String filename;
91
92     public String pdbId;
93
94     public String chain = "";
95
96     /**
97      * is the mapped sequence not protein ?
98      */
99     public boolean isRna;
100
101     /*
102      * The pdb residue number (if any) mapped to columns of the alignment
103      */
104     public int[] pdbResNo; // or use SparseIntArray?
105
106     public String modelId;
107
108     /**
109      * Constructor
110      * 
111      * @param width
112      *          width of alignment (number of columns that may potentially
113      *          participate in superposition)
114      * @param model
115      *          structure viewer model number
116      */
117     public SuperposeData(int width, String model)
118     {
119       pdbResNo = new int[width];
120       modelId = model;
121     }
122   }
123
124   private static final int MIN_POS_TO_SUPERPOSE = 4;
125
126   private static final String COLOURING_STRUCTURES = MessageManager
127           .getString("status.colouring_structures");
128
129   /*
130    * the Jalview panel through which the user interacts
131    * with the structure viewer
132    */
133   private JalviewStructureDisplayI viewer;
134
135   /*
136    * helper that generates command syntax
137    */
138   private StructureCommandsI commandGenerator;
139
140   private StructureSelectionManager ssm;
141
142   /*
143    * modelled chains, formatted as "pdbid:chainCode"
144    */
145   private List<String> chainNames;
146
147   /*
148    * lookup of pdb file name by key "pdbid:chainCode"
149    */
150   private Map<String, String> chainFile;
151
152   /*
153    * distinct PDB entries (pdb files) associated
154    * with sequences
155    */
156   private PDBEntry[] pdbEntry;
157
158   /*
159    * sequences mapped to each pdbentry
160    */
161   private SequenceI[][] sequence;
162
163   /*
164    * array of target chains for sequences - tied to pdbentry and sequence[]
165    */
166   private String[][] chains;
167
168   /*
169    * datasource protocol for access to PDBEntrylatest
170    */
171   DataSourceType protocol = null;
172
173   protected boolean colourBySequence = true;
174
175   /**
176    * true if all sequences appear to be nucleotide
177    */
178   private boolean nucleotide;
179
180   private boolean finishedInit = false;
181
182   /**
183    * current set of model filenames loaded in the viewer
184    */
185   protected String[] modelFileNames = null;
186
187   public String fileLoadingError;
188
189   protected Thread externalViewerMonitor;
190
191   /**
192    * Constructor
193    * 
194    * @param ssm
195    * @param seqs
196    */
197   public AAStructureBindingModel(StructureSelectionManager ssm,
198           SequenceI[][] seqs)
199   {
200     this.ssm = ssm;
201     this.sequence = seqs;
202     chainNames = new ArrayList<>();
203     chainFile = new HashMap<>();
204   }
205
206   /**
207    * Constructor
208    * 
209    * @param ssm
210    * @param pdbentry
211    * @param sequenceIs
212    * @param protocol
213    */
214   public AAStructureBindingModel(StructureSelectionManager ssm,
215           PDBEntry[] pdbentry, SequenceI[][] sequenceIs,
216           DataSourceType protocol)
217   {
218     this(ssm, sequenceIs);
219     this.nucleotide = Comparison.isNucleotide(sequenceIs);
220     this.pdbEntry = pdbentry;
221     this.protocol = protocol;
222     resolveChains();
223   }
224
225   private boolean resolveChains()
226   {
227     /**
228      * final count of chain mappings discovered
229      */
230     int chainmaps = 0;
231     // JBPNote: JAL-2693 - this should be a list of chain mappings per
232     // [pdbentry][sequence]
233     String[][] newchains = new String[pdbEntry.length][];
234     int pe = 0;
235     for (PDBEntry pdb : pdbEntry)
236     {
237       SequenceI[] seqsForPdb = sequence[pe];
238       if (seqsForPdb != null)
239       {
240         newchains[pe] = new String[seqsForPdb.length];
241         int se = 0;
242         for (SequenceI asq : seqsForPdb)
243         {
244           String chain = (chains != null && chains[pe] != null)
245                   ? chains[pe][se]
246                   : null;
247           SequenceI sq = (asq.getDatasetSequence() == null) ? asq
248                   : asq.getDatasetSequence();
249           if (sq.getAllPDBEntries() != null)
250           {
251             for (PDBEntry pdbentry : sq.getAllPDBEntries())
252             {
253               if (pdb.getFile() != null && pdbentry.getFile() != null
254                       && pdb.getFile().equals(pdbentry.getFile()))
255               {
256                 String chaincode = pdbentry.getChainCode();
257                 if (chaincode != null && chaincode.length() > 0)
258                 {
259                   chain = chaincode;
260                   chainmaps++;
261                   break;
262                 }
263               }
264             }
265           }
266           newchains[pe][se] = chain;
267           se++;
268         }
269         pe++;
270       }
271     }
272
273     chains = newchains;
274     return chainmaps > 0;
275   }
276
277   public StructureSelectionManager getSsm()
278   {
279     return ssm;
280   }
281
282   /**
283    * Returns the i'th PDBEntry (or null)
284    * 
285    * @param i
286    * @return
287    */
288   public PDBEntry getPdbEntry(int i)
289   {
290     return (pdbEntry != null && pdbEntry.length > i) ? pdbEntry[i] : null;
291   }
292
293   /**
294    * Answers true if this binding includes the given PDB id, else false
295    * 
296    * @param pdbId
297    * @return
298    */
299   public boolean hasPdbId(String pdbId)
300   {
301     if (pdbEntry != null)
302     {
303       for (PDBEntry pdb : pdbEntry)
304       {
305         if (pdb.getId().equals(pdbId))
306         {
307           return true;
308         }
309       }
310     }
311     return false;
312   }
313
314   /**
315    * Returns the number of modelled PDB file entries.
316    * 
317    * @return
318    */
319   public int getPdbCount()
320   {
321     return pdbEntry == null ? 0 : pdbEntry.length;
322   }
323
324   public SequenceI[][] getSequence()
325   {
326     return sequence;
327   }
328
329   public String[][] getChains()
330   {
331     return chains;
332   }
333
334   public DataSourceType getProtocol()
335   {
336     return protocol;
337   }
338
339   // TODO may remove this if calling methods can be pulled up here
340   protected void setPdbentry(PDBEntry[] pdbentry)
341   {
342     this.pdbEntry = pdbentry;
343   }
344
345   protected void setSequence(SequenceI[][] sequence)
346   {
347     this.sequence = sequence;
348   }
349
350   protected void setChains(String[][] chains)
351   {
352     this.chains = chains;
353   }
354
355   /**
356    * Construct a title string for the viewer window based on the data Jalview
357    * knows about
358    * 
359    * @param viewerName
360    *          TODO
361    * @param verbose
362    * 
363    * @return
364    */
365   public String getViewerTitle(String viewerName, boolean verbose)
366   {
367     if (getSequence() == null || getSequence().length < 1
368             || getPdbCount() < 1 || getSequence()[0].length < 1)
369     {
370       return ("Jalview " + viewerName + " Window");
371     }
372     // TODO: give a more informative title when multiple structures are
373     // displayed.
374     StringBuilder title = new StringBuilder(64);
375     final PDBEntry pdbe = getPdbEntry(0);
376     title.append(viewerName + " view for " + getSequence()[0][0].getName()
377             + ":" + pdbe.getId());
378
379     if (verbose)
380     {
381       String method = (String) pdbe.getProperty("method");
382       if (method != null)
383       {
384         title.append(" Method: ").append(method);
385       }
386       String chain = (String) pdbe.getProperty("chains");
387       if (chain != null)
388       {
389         title.append(" Chain:").append(chain);
390       }
391     }
392     return title.toString();
393   }
394
395   /**
396    * Called by after closeViewer is called, to release any resources and
397    * references so they can be garbage collected. Override if needed.
398    */
399   protected void releaseUIResources()
400   {
401   }
402
403   @Override
404   public void releaseReferences(Object svl)
405   {
406   }
407
408   public boolean isColourBySequence()
409   {
410     return colourBySequence;
411   }
412
413   /**
414    * Called when the binding thinks the UI needs to be refreshed after a
415    * structure viewer state change. This could be because structures were
416    * loaded, or because an error has occurred. Default does nothing, override as
417    * required.
418    */
419   public void refreshGUI()
420   {
421   }
422
423   /**
424    * Instruct the Jalview binding to update the pdbentries vector if necessary
425    * prior to matching the jmol view's contents to the list of structure files
426    * Jalview knows about. By default does nothing, override as required.
427    */
428   public void refreshPdbEntries()
429   {
430   }
431
432   public void setColourBySequence(boolean colourBySequence)
433   {
434     this.colourBySequence = colourBySequence;
435   }
436
437   protected void addSequenceAndChain(int pe, SequenceI[] seq,
438           String[] tchain)
439   {
440     if (pe < 0 || pe >= getPdbCount())
441     {
442       throw new Error(MessageManager.formatMessage(
443               "error.implementation_error_no_pdbentry_from_index",
444               new Object[]
445               { Integer.valueOf(pe).toString() }));
446     }
447     final String nullChain = "TheNullChain";
448     List<SequenceI> s = new ArrayList<>();
449     List<String> c = new ArrayList<>();
450     if (getChains() == null)
451     {
452       setChains(new String[getPdbCount()][]);
453     }
454     if (getSequence()[pe] != null)
455     {
456       for (int i = 0; i < getSequence()[pe].length; i++)
457       {
458         s.add(getSequence()[pe][i]);
459         if (getChains()[pe] != null)
460         {
461           if (i < getChains()[pe].length)
462           {
463             c.add(getChains()[pe][i]);
464           }
465           else
466           {
467             c.add(nullChain);
468           }
469         }
470         else
471         {
472           if (tchain != null && tchain.length > 0)
473           {
474             c.add(nullChain);
475           }
476         }
477       }
478     }
479     for (int i = 0; i < seq.length; i++)
480     {
481       if (!s.contains(seq[i]))
482       {
483         s.add(seq[i]);
484         if (tchain != null && i < tchain.length)
485         {
486           c.add(tchain[i] == null ? nullChain : tchain[i]);
487         }
488       }
489     }
490     SequenceI[] tmp = s.toArray(new SequenceI[s.size()]);
491     getSequence()[pe] = tmp;
492     if (c.size() > 0)
493     {
494       String[] tch = c.toArray(new String[c.size()]);
495       for (int i = 0; i < tch.length; i++)
496       {
497         if (tch[i] == nullChain)
498         {
499           tch[i] = null;
500         }
501       }
502       getChains()[pe] = tch;
503     }
504     else
505     {
506       getChains()[pe] = null;
507     }
508   }
509
510   /**
511    * add structures and any known sequence associations
512    * 
513    * @returns the pdb entries added to the current set.
514    */
515   public synchronized PDBEntry[] addSequenceAndChain(PDBEntry[] pdbe,
516           SequenceI[][] seq, String[][] chns)
517   {
518     List<PDBEntry> v = new ArrayList<>();
519     List<int[]> rtn = new ArrayList<>();
520     for (int i = 0; i < getPdbCount(); i++)
521     {
522       v.add(getPdbEntry(i));
523     }
524     for (int i = 0; i < pdbe.length; i++)
525     {
526       int r = v.indexOf(pdbe[i]);
527       if (r == -1 || r >= getPdbCount())
528       {
529         rtn.add(new int[] { v.size(), i });
530         v.add(pdbe[i]);
531       }
532       else
533       {
534         // just make sure the sequence/chain entries are all up to date
535         addSequenceAndChain(r, seq[i], chns[i]);
536       }
537     }
538     pdbe = v.toArray(new PDBEntry[v.size()]);
539     setPdbentry(pdbe);
540     if (rtn.size() > 0)
541     {
542       // expand the tied sequence[] and string[] arrays
543       SequenceI[][] sqs = new SequenceI[getPdbCount()][];
544       String[][] sch = new String[getPdbCount()][];
545       System.arraycopy(getSequence(), 0, sqs, 0, getSequence().length);
546       System.arraycopy(getChains(), 0, sch, 0, this.getChains().length);
547       setSequence(sqs);
548       setChains(sch);
549       pdbe = new PDBEntry[rtn.size()];
550       for (int r = 0; r < pdbe.length; r++)
551       {
552         int[] stri = (rtn.get(r));
553         // record the pdb file as a new addition
554         pdbe[r] = getPdbEntry(stri[0]);
555         // and add the new sequence/chain entries
556         addSequenceAndChain(stri[0], seq[stri[1]], chns[stri[1]]);
557       }
558     }
559     else
560     {
561       pdbe = null;
562     }
563     return pdbe;
564   }
565
566   /**
567    * Add sequences to the pe'th pdbentry's sequence set.
568    * 
569    * @param pe
570    * @param seq
571    */
572   public void addSequence(int pe, SequenceI[] seq)
573   {
574     addSequenceAndChain(pe, seq, null);
575   }
576
577   /**
578    * add the given sequences to the mapping scope for the given pdb file handle
579    * 
580    * @param pdbFile
581    *          - pdbFile identifier
582    * @param seq
583    *          - set of sequences it can be mapped to
584    */
585   public void addSequenceForStructFile(String pdbFile, SequenceI[] seq)
586   {
587     for (int pe = 0; pe < getPdbCount(); pe++)
588     {
589       if (getPdbEntry(pe).getFile().equals(pdbFile))
590       {
591         addSequence(pe, seq);
592       }
593     }
594   }
595
596   @Override
597   public abstract void highlightAtoms(List<AtomSpec> atoms);
598
599   protected boolean isNucleotide()
600   {
601     return this.nucleotide;
602   }
603
604   /**
605    * Returns a readable description of all mappings for the wrapped pdbfile to
606    * any mapped sequences
607    * 
608    * @param pdbfile
609    * @param seqs
610    * @return
611    */
612   public String printMappings()
613   {
614     if (pdbEntry == null)
615     {
616       return "";
617     }
618     StringBuilder sb = new StringBuilder(128);
619     for (int pdbe = 0; pdbe < getPdbCount(); pdbe++)
620     {
621       String pdbfile = getPdbEntry(pdbe).getFile();
622       List<SequenceI> seqs = Arrays.asList(getSequence()[pdbe]);
623       sb.append(getSsm().printMappings(pdbfile, seqs));
624     }
625     return sb.toString();
626   }
627
628   /**
629    * Returns the mapped structure position for a given aligned column of a given
630    * sequence, or -1 if the column is gapped, beyond the end of the sequence, or
631    * not mapped to structure.
632    * 
633    * @param seq
634    * @param alignedPos
635    * @param mapping
636    * @return
637    */
638   protected int getMappedPosition(SequenceI seq, int alignedPos,
639           StructureMapping mapping)
640   {
641     if (alignedPos >= seq.getLength())
642     {
643       return -1;
644     }
645
646     if (Comparison.isGap(seq.getCharAt(alignedPos)))
647     {
648       return -1;
649     }
650     int seqPos = seq.findPosition(alignedPos);
651     int pos = mapping.getPDBResNum(seqPos);
652     return pos;
653   }
654
655   /**
656    * Helper method to identify residues that can participate in a structure
657    * superposition command. For each structure, identify a sequence in the
658    * alignment which is mapped to the structure. Identify non-gapped columns in
659    * the sequence which have a mapping to a residue in the structure. Returns
660    * the index of the first structure that has a mapping to the alignment.
661    * 
662    * @param alignment
663    *          the sequence alignment which is the basis of structure
664    *          superposition
665    * @param matched
666    *          a BitSet, where bit j is set to indicate that every structure has
667    *          a mapped residue present in column j (so the column can
668    *          participate in structure alignment)
669    * @param structures
670    *          an array of data beans corresponding to pdb file index
671    * @return
672    */
673   protected int findSuperposableResidues(AlignmentI alignment,
674           BitSet matched,
675           AAStructureBindingModel.SuperposeData[] structures)
676   {
677     int refStructure = -1;
678     String[] files = getStructureFiles();
679     if (files == null)
680     {
681       return -1;
682     }
683     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
684     {
685       StructureMapping[] mappings = getSsm().getMapping(files[pdbfnum]);
686       int lastPos = -1;
687
688       /*
689        * Find the first mapped sequence (if any) for this PDB entry which is in
690        * the alignment
691        */
692       final int seqCountForPdbFile = getSequence()[pdbfnum].length;
693       for (int s = 0; s < seqCountForPdbFile; s++)
694       {
695         for (StructureMapping mapping : mappings)
696         {
697           final SequenceI theSequence = getSequence()[pdbfnum][s];
698           if (mapping.getSequence() == theSequence
699                   && alignment.findIndex(theSequence) > -1)
700           {
701             if (refStructure < 0)
702             {
703               refStructure = pdbfnum;
704             }
705             for (int r = 0; r < alignment.getWidth(); r++)
706             {
707               if (!matched.get(r))
708               {
709                 continue;
710               }
711               int pos = getMappedPosition(theSequence, r, mapping);
712               if (pos < 1 || pos == lastPos)
713               {
714                 matched.clear(r);
715                 continue;
716               }
717               lastPos = pos;
718               structures[pdbfnum].pdbResNo[r] = pos;
719             }
720             String chain = mapping.getChain();
721             if (chain != null && chain.trim().length() > 0)
722             {
723               structures[pdbfnum].chain = chain;
724             }
725             structures[pdbfnum].pdbId = mapping.getPdbId();
726             structures[pdbfnum].isRna = !theSequence.isProtein();
727
728             /*
729              * move on to next pdb file (ignore sequences for other chains
730              * for the same structure)
731              */
732             s = seqCountForPdbFile;
733             break; // fixme break out of two loops here!
734           }
735         }
736       }
737     }
738     return refStructure;
739   }
740
741   /**
742    * Returns true if the structure viewer has loaded all of the files of
743    * interest (identified by the file mapping having been set up), or false if
744    * any are still not loaded after a timeout interval.
745    * 
746    * @param files
747    */
748   protected boolean waitForFileLoad(String[] files)
749   {
750     /*
751      * give up after 10 secs plus 1 sec per file
752      */
753     long starttime = System.currentTimeMillis();
754     long endTime = 10000 + 1000 * files.length + starttime;
755     String notLoaded = null;
756
757     boolean waiting = true;
758     while (waiting && System.currentTimeMillis() < endTime)
759     {
760       waiting = false;
761       for (String file : files)
762       {
763         notLoaded = file;
764         if (file == null)
765         {
766           continue;
767         }
768         try
769         {
770           StructureMapping[] sm = getSsm().getMapping(file);
771           if (sm == null || sm.length == 0)
772           {
773             waiting = true;
774           }
775         } catch (Throwable x)
776         {
777           waiting = true;
778         }
779       }
780     }
781
782     if (waiting)
783     {
784       jalview.bin.Console.errPrintln(
785               "Timed out waiting for structure viewer to load file "
786                       + notLoaded);
787       return false;
788     }
789     return true;
790   }
791
792   @Override
793   public boolean isListeningFor(SequenceI seq)
794   {
795     if (sequence != null)
796     {
797       for (SequenceI[] seqs : sequence)
798       {
799         if (seqs != null)
800         {
801           for (SequenceI s : seqs)
802           {
803             if (s == seq || (s.getDatasetSequence() != null
804                     && s.getDatasetSequence() == seq.getDatasetSequence()))
805             {
806               return true;
807             }
808           }
809         }
810       }
811     }
812     return false;
813   }
814
815   public boolean isFinishedInit()
816   {
817     return finishedInit;
818   }
819
820   public void setFinishedInit(boolean fi)
821   {
822     this.finishedInit = fi;
823   }
824
825   /**
826    * Returns a list of chains mapped in this viewer, formatted as
827    * "pdbid:chainCode"
828    * 
829    * @return
830    */
831   public List<String> getChainNames()
832   {
833     return chainNames;
834   }
835
836   /**
837    * Returns the Jalview panel hosting the structure viewer (if any)
838    * 
839    * @return
840    */
841   public JalviewStructureDisplayI getViewer()
842   {
843     return viewer;
844   }
845
846   public void setViewer(JalviewStructureDisplayI v)
847   {
848     viewer = v;
849   }
850
851   /**
852    * Constructs and sends a command to align structures against a reference
853    * structure, based on one or more sequence alignments. May optionally return
854    * an error or warning message for the alignment command(s).
855    * 
856    * @param alignWith
857    *          an array of one or more alignment views to process
858    * @return
859    */
860   public String superposeStructures(List<AlignmentViewPanel> alignWith)
861   {
862     String error = "";
863     String[] files = getStructureFiles();
864
865     if (!waitForFileLoad(files))
866     {
867       return null;
868     }
869     refreshPdbEntries();
870
871     for (AlignmentViewPanel view : alignWith)
872     {
873       AlignmentI alignment = view.getAlignment();
874       HiddenColumns hiddenCols = alignment.getHiddenColumns();
875       /*
876        * 'matched' bit i will be set for visible alignment columns i where
877        * all sequences have a residue with a mapping to their PDB structure
878        */
879       final int width = alignment.getWidth();
880       BitSet matched = new BitSet();
881       ColumnSelection cs = view.getAlignViewport().getColumnSelection();
882       // restrict to active column selection, if there is one
883       if (cs != null && cs.hasSelectedColumns()
884               && cs.getSelected().size() >= 4)
885       {
886         for (int s : cs.getSelected())
887         {
888           matched.set(s);
889         }
890       }
891       else
892       {
893         for (int m = 0; m < width; m++)
894         {
895           if (hiddenCols == null || hiddenCols.isVisible(m))
896           {
897             matched.set(m);
898           }
899         }
900       }
901       AAStructureBindingModel.SuperposeData[] structures = new AAStructureBindingModel.SuperposeData[files.length];
902       for (int f = 0; f < files.length; f++)
903       {
904         structures[f] = new AAStructureBindingModel.SuperposeData(width,
905                 getModelIdForFile(files[f]));
906       }
907
908       /*
909        * Calculate the superposable alignment columns ('matched'), and the
910        * corresponding structure residue positions (structures.pdbResNo)
911        */
912       int refStructure = findSuperposableResidues(alignment, matched,
913               structures);
914
915       /*
916        * require at least 4 positions to be able to execute superposition
917        */
918       int nmatched = matched.cardinality();
919       if (nmatched < MIN_POS_TO_SUPERPOSE)
920       {
921         String msg = MessageManager
922                 .formatMessage("label.insufficient_residues", nmatched);
923         error += view.getViewName() + ": " + msg + "; ";
924         continue;
925       }
926
927       /*
928        * get a model of the superposable residues in the reference structure 
929        */
930       AtomSpecModel refAtoms = getAtomSpec(structures[refStructure],
931               matched);
932
933       /*
934        * Show all as backbone before doing superposition(s)
935        * (residues used for matching will be shown as ribbon)
936        */
937       // todo better way to ensure synchronous than setting getReply true!!
938       executeCommands(commandGenerator.showBackbone(), true, null);
939
940       AtomSpecType backbone = structures[refStructure].isRna
941               ? AtomSpecType.PHOSPHATE
942               : AtomSpecType.ALPHA;
943       List<AtomSpecModel> models = new ArrayList<AtomSpecModel>();
944       models.add(refAtoms);
945       /*
946        * superpose each (other) structure to the reference in turn
947        */
948       for (int i = 0; i < structures.length; i++)
949       {
950         if (i != refStructure)
951         {
952           AtomSpecModel atomSpec = getAtomSpec(structures[i], matched);
953           List<StructureCommandI> commands = commandGenerator
954                   .superposeStructures(refAtoms, atomSpec, backbone);
955           List<String> replies = executeCommands(commands, true, null);
956           for (String reply : replies)
957           {
958             // return this error (Chimera only) to the user
959             if (reply.toLowerCase(Locale.ROOT)
960                     .contains("unequal numbers of atoms"))
961             {
962               error += "; " + reply;
963             }
964           }
965           models.add(atomSpec);
966         }
967       }
968       List<StructureCommandI> finalView = commandGenerator
969               .centerViewOn(models);
970       if (finalView != null && finalView.size() > 0)
971       {
972         executeCommands(finalView, false, "Centered on Superposition");
973       }
974     }
975     return error;
976   }
977
978   private AtomSpecModel getAtomSpec(
979           AAStructureBindingModel.SuperposeData superposeData,
980           BitSet matched)
981   {
982     AtomSpecModel model = new AtomSpecModel();
983     int nextColumnMatch = matched.nextSetBit(0);
984     while (nextColumnMatch != -1)
985     {
986       int pdbResNum = superposeData.pdbResNo[nextColumnMatch];
987       model.addRange(superposeData.modelId, pdbResNum, pdbResNum,
988               superposeData.chain);
989       nextColumnMatch = matched.nextSetBit(nextColumnMatch + 1);
990     }
991
992     return model;
993   }
994
995   /**
996    * returns the current sequenceRenderer that should be used to colour the
997    * structures
998    * 
999    * @param alignment
1000    * 
1001    * @return
1002    */
1003   public abstract SequenceRenderer getSequenceRenderer(
1004           AlignmentViewPanel alignment);
1005
1006   /**
1007    * Sends a command to the structure viewer to colour each chain with a
1008    * distinct colour (to the extent supported by the viewer)
1009    */
1010   public void colourByChain()
1011   {
1012     colourBySequence = false;
1013
1014     // TODO: JAL-628 colour chains distinctly across all visible models
1015
1016     executeCommand(false, COLOURING_STRUCTURES,
1017             commandGenerator.colourByChain());
1018   }
1019
1020   /**
1021    * Sends a command to the structure viewer to colour each chain with a
1022    * distinct colour (to the extent supported by the viewer)
1023    */
1024   public void colourByCharge()
1025   {
1026     colourBySequence = false;
1027
1028     executeCommands(commandGenerator.colourByCharge(), false,
1029             COLOURING_STRUCTURES);
1030   }
1031
1032   /**
1033    * Sends a command to the structure to apply a colour scheme (defined in
1034    * Jalview but not necessarily applied to the alignment), which defines a
1035    * colour per residue letter. More complex schemes (e.g. that depend on
1036    * consensus) cannot be used here and are ignored.
1037    * 
1038    * @param cs
1039    */
1040   public void colourByJalviewColourScheme(ColourSchemeI cs)
1041   {
1042     colourBySequence = false;
1043
1044     if (cs == null || !cs.isSimple())
1045     {
1046       return;
1047     }
1048
1049     /*
1050      * build a map of {Residue3LetterCode, Color}
1051      */
1052     Map<String, Color> colours = new HashMap<>();
1053     List<String> residues = ResidueProperties.getResidues(isNucleotide(),
1054             false);
1055     for (String resName : residues)
1056     {
1057       char res = resName.length() == 3
1058               ? ResidueProperties.getSingleCharacterCode(resName)
1059               : resName.charAt(0);
1060       Color colour = cs.findColour(res, 0, null, null, 0f);
1061       colours.put(resName, colour);
1062     }
1063
1064     /*
1065      * pass to the command constructor, and send the command
1066      */
1067     List<StructureCommandI> cmd = commandGenerator
1068             .colourByResidues(colours);
1069     executeCommands(cmd, false, COLOURING_STRUCTURES);
1070   }
1071
1072   public void setBackgroundColour(Color col)
1073   {
1074     StructureCommandI cmd = commandGenerator.setBackgroundColour(col);
1075     executeCommand(false, null, cmd);
1076   }
1077
1078   /**
1079    * Execute one structure viewer command. If {@code getReply} is true, may
1080    * optionally return one or more reply messages, else returns null.
1081    * 
1082    * @param cmd
1083    * @param getReply
1084    */
1085   protected abstract List<String> executeCommand(StructureCommandI cmd,
1086           boolean getReply);
1087
1088   /**
1089    * Executes one or more structure viewer commands
1090    * 
1091    * @param commands
1092    * @param getReply
1093    * @param msg
1094    */
1095   public List<String> executeCommands(List<StructureCommandI> commands,
1096           boolean getReply, String msg)
1097   {
1098     return executeCommand(getReply, msg,
1099             commands.toArray(new StructureCommandI[commands.size()]));
1100   }
1101
1102   /**
1103    * Executes one or more structure viewer commands, optionally returning the
1104    * reply, and optionally showing a status message while the command is being
1105    * executed.
1106    * <p>
1107    * If a reply is wanted, the execution is done synchronously (waits),
1108    * otherwise it is done in a separate thread (doesn't wait). WARNING: if you
1109    * are sending commands that need to execute before later calls to
1110    * executeCommand (e.g. mouseovers, which clean up after previous ones) then
1111    * set getReply true to ensure that commands are not executed out of order.
1112    * 
1113    * @param getReply
1114    * @param msg
1115    * @param cmds
1116    * @return
1117    */
1118   protected List<String> executeCommand(boolean getReply, String msg,
1119           StructureCommandI... cmds)
1120   {
1121     JalviewStructureDisplayI theViewer = getViewer();
1122     final long handle = msg == null ? 0 : theViewer.startProgressBar(msg);
1123
1124     if (getReply)
1125     {
1126       /*
1127        * execute and wait for reply
1128        */
1129       List<String> response = new ArrayList<>();
1130       try
1131       {
1132         for (StructureCommandI cmd : cmds)
1133         {
1134           List<String> replies = executeCommand(cmd, true);
1135           if (replies != null)
1136           {
1137             response.addAll(replies);
1138           }
1139         }
1140         return response;
1141       } finally
1142       {
1143         if (msg != null)
1144         {
1145           theViewer.stopProgressBar(null, handle);
1146         }
1147       }
1148     }
1149
1150     /*
1151      * fire and forget
1152      */
1153     String threadName = msg == null ? "StructureCommand" : msg;
1154     new Thread(new Runnable()
1155     {
1156       @Override
1157       public void run()
1158       {
1159         try
1160         {
1161           for (StructureCommandI cmd : cmds)
1162           {
1163             executeCommand(cmd, false);
1164           }
1165         } finally
1166         {
1167           if (msg != null)
1168           {
1169             SwingUtilities.invokeLater(new Runnable()
1170             {
1171               @Override
1172               public void run()
1173               {
1174                 theViewer.stopProgressBar(null, handle);
1175               }
1176             });
1177           }
1178         }
1179       }
1180     }, threadName).start();
1181     return null;
1182   }
1183
1184   /**
1185    * Colours any structures associated with sequences in the given alignment as
1186    * coloured in the alignment view, provided colourBySequence is enabled
1187    */
1188   public void colourBySequence(AlignmentViewPanel alignmentv)
1189   {
1190     if (!colourBySequence || !isLoadingFinished() || getSsm() == null)
1191     {
1192       return;
1193     }
1194     Map<Object, AtomSpecModel> colourMap = buildColoursMap(ssm, sequence,
1195             alignmentv);
1196
1197     List<StructureCommandI> colourBySequenceCommands = commandGenerator
1198             .colourBySequence(colourMap);
1199     executeCommands(colourBySequenceCommands, false, COLOURING_STRUCTURES);
1200   }
1201
1202   /**
1203    * Centre the display in the structure viewer
1204    */
1205   public void focusView()
1206   {
1207     executeCommand(false, null, commandGenerator.focusView());
1208   }
1209
1210   /**
1211    * Generates and executes a command to show only specified chains in the
1212    * structure viewer. The list of chains to show should contain entries
1213    * formatted as "pdbid:chaincode".
1214    * 
1215    * @param toShow
1216    */
1217   public void showChains(List<String> toShow)
1218   {
1219     // todo or reformat toShow list entries as modelNo:pdbId:chainCode ?
1220
1221     /*
1222      * Reformat the pdbid:chainCode values as modelNo:chainCode
1223      * since this is what is needed to construct the viewer command
1224      * todo: find a less messy way to do this
1225      */
1226     List<String> showThese = new ArrayList<>();
1227     for (String chainId : toShow)
1228     {
1229       String[] tokens = chainId.split("\\:");
1230       if (tokens.length == 2)
1231       {
1232         String pdbFile = getFileForChain(chainId);
1233         String model = getModelIdForFile(pdbFile);
1234         showThese.add(model + ":" + tokens[1]);
1235       }
1236     }
1237     executeCommands(commandGenerator.showChains(showThese), false, null);
1238   }
1239
1240   /**
1241    * Answers the structure viewer's model id given a PDB file name. Returns an
1242    * empty string if model id is not found.
1243    * 
1244    * @param chainId
1245    * @return
1246    */
1247   protected abstract String getModelIdForFile(String chainId);
1248
1249   public boolean hasFileLoadingError()
1250   {
1251     return fileLoadingError != null && fileLoadingError.length() > 0;
1252   }
1253
1254   /**
1255    * Returns the FeatureRenderer for the given alignment view
1256    * 
1257    * @param avp
1258    * @return
1259    */
1260   public FeatureRenderer getFeatureRenderer(AlignmentViewPanel avp)
1261   {
1262     AlignmentViewPanel ap = (avp == null) ? getViewer().getAlignmentPanel()
1263             : avp;
1264     if (ap == null)
1265     {
1266       return null;
1267     }
1268     return ap.getFeatureRenderer();
1269   }
1270
1271   protected void setStructureCommands(StructureCommandsI cmd)
1272   {
1273     commandGenerator = cmd;
1274   }
1275
1276   /**
1277    * Records association of one chain id (formatted as "pdbid:chainCode") with
1278    * the corresponding PDB file name
1279    * 
1280    * @param chainId
1281    * @param fileName
1282    */
1283   public void addChainFile(String chainId, String fileName)
1284   {
1285     chainFile.put(chainId, fileName);
1286   }
1287
1288   /**
1289    * Returns the PDB filename for the given chain id (formatted as
1290    * "pdbid:chainCode"), or null if not found
1291    * 
1292    * @param chainId
1293    * @return
1294    */
1295   protected String getFileForChain(String chainId)
1296   {
1297     return chainFile.get(chainId);
1298   }
1299
1300   @Override
1301   public void updateColours(Object source)
1302   {
1303     if (getViewer() == null)
1304     {
1305       // can happen if a viewer was not instantiated or cleaned up and is still
1306       // registered - mostly during tests
1307       return;
1308     }
1309     AlignmentViewPanel ap = (AlignmentViewPanel) source;
1310     // ignore events from panels not used to colour this view
1311     if (!getViewer().isUsedForColourBy(ap))
1312     {
1313       return;
1314     }
1315     if (!isLoadingFromArchive())
1316     {
1317       colourBySequence(ap);
1318     }
1319   }
1320
1321   public StructureCommandsI getCommandGenerator()
1322   {
1323     return commandGenerator;
1324   }
1325
1326   protected abstract ViewerType getViewerType();
1327
1328   /**
1329    * Builds a data structure which records mapped structure residues for each
1330    * colour. From this we can easily generate the viewer commands for colour by
1331    * sequence. Constructs and returns a map of {@code Color} to
1332    * {@code AtomSpecModel}, where the atomspec model holds
1333    * 
1334    * <pre>
1335    *   Model ids
1336    *     Chains
1337    *       Residue positions
1338    * </pre>
1339    * 
1340    * Ordering is by order of addition (for colours), natural ordering (for
1341    * models and chains)
1342    * 
1343    * @param ssm
1344    * @param sequence
1345    * @param viewPanel
1346    * @return
1347    */
1348   protected Map<Object, AtomSpecModel> buildColoursMap(
1349           StructureSelectionManager ssm, SequenceI[][] sequence,
1350           AlignmentViewPanel viewPanel)
1351   {
1352     String[] files = getStructureFiles();
1353     SequenceRenderer sr = getSequenceRenderer(viewPanel);
1354     FeatureRenderer fr = viewPanel.getFeatureRenderer();
1355     FeatureColourFinder finder = new FeatureColourFinder(fr);
1356     AlignViewportI viewport = viewPanel.getAlignViewport();
1357     HiddenColumns cs = viewport.getAlignment().getHiddenColumns();
1358     AlignmentI al = viewport.getAlignment();
1359     Map<Object, AtomSpecModel> colourMap = new LinkedHashMap<>();
1360     Color lastColour = null;
1361
1362     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
1363     {
1364       final String modelId = getModelIdForFile(files[pdbfnum]);
1365       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
1366
1367       if (mapping == null || mapping.length < 1)
1368       {
1369         continue;
1370       }
1371
1372       int startPos = -1, lastPos = -1;
1373       String lastChain = "";
1374       for (int s = 0; s < sequence[pdbfnum].length; s++)
1375       {
1376         for (int sp, m = 0; m < mapping.length; m++)
1377         {
1378           final SequenceI seq = sequence[pdbfnum][s];
1379           if (mapping[m].getSequence() == seq
1380                   && (sp = al.findIndex(seq)) > -1)
1381           {
1382             SequenceI asp = al.getSequenceAt(sp);
1383             for (int r = 0; r < asp.getLength(); r++)
1384             {
1385               // no mapping to gaps in sequence
1386               if (Comparison.isGap(asp.getCharAt(r)))
1387               {
1388                 continue;
1389               }
1390               int pos = mapping[m].getPDBResNum(asp.findPosition(r));
1391
1392               if (pos < 1 || pos == lastPos)
1393               {
1394                 continue;
1395               }
1396
1397               Color colour = sr.getResidueColour(seq, r, finder);
1398
1399               /*
1400                * darker colour for hidden regions
1401                */
1402               if (!cs.isVisible(r))
1403               {
1404                 colour = Color.GRAY;
1405               }
1406
1407               final String chain = mapping[m].getChain();
1408
1409               /*
1410                * Just keep incrementing the end position for this colour range
1411                * _unless_ colour, PDB model or chain has changed, or there is a
1412                * gap in the mapped residue sequence
1413                */
1414               final boolean newColour = !colour.equals(lastColour);
1415               final boolean nonContig = lastPos + 1 != pos;
1416               final boolean newChain = !chain.equals(lastChain);
1417               if (newColour || nonContig || newChain)
1418               {
1419                 if (startPos != -1)
1420                 {
1421                   addAtomSpecRange(colourMap, lastColour, modelId, startPos,
1422                           lastPos, lastChain);
1423                 }
1424                 startPos = pos;
1425               }
1426               lastColour = colour;
1427               lastPos = pos;
1428               lastChain = chain;
1429             }
1430             // final colour range
1431             if (lastColour != null)
1432             {
1433               addAtomSpecRange(colourMap, lastColour, modelId, startPos,
1434                       lastPos, lastChain);
1435             }
1436             // break;
1437           }
1438         }
1439       }
1440     }
1441     return colourMap;
1442   }
1443
1444   /**
1445    * todo better refactoring (map lookup or similar to get viewer structure id)
1446    * 
1447    * @param pdbfnum
1448    * @param file
1449    * @return
1450    */
1451   protected String getModelId(int pdbfnum, String file)
1452   {
1453     return String.valueOf(pdbfnum);
1454   }
1455
1456   /**
1457    * Saves chains, formatted as "pdbId:chainCode", and lookups from this to the
1458    * full PDB file path
1459    * 
1460    * @param pdb
1461    * @param file
1462    */
1463   public void stashFoundChains(StructureFile pdb, String file)
1464   {
1465     for (int i = 0; i < pdb.getChains().size(); i++)
1466     {
1467       String chid = pdb.getId() + ":" + pdb.getChains().elementAt(i).id;
1468       addChainFile(chid, file);
1469       getChainNames().add(chid);
1470     }
1471   }
1472
1473   /**
1474    * Helper method to add one contiguous range to the AtomSpec model for the
1475    * given value (creating the model if necessary). As used by Jalview,
1476    * {@code value} is
1477    * <ul>
1478    * <li>a colour, when building a 'colour structure by sequence' command</li>
1479    * <li>a feature value, when building a 'set Chimera attributes from features'
1480    * command</li>
1481    * </ul>
1482    * 
1483    * @param map
1484    * @param value
1485    * @param model
1486    * @param startPos
1487    * @param endPos
1488    * @param chain
1489    */
1490   public static final void addAtomSpecRange(Map<Object, AtomSpecModel> map,
1491           Object value, String model, int startPos, int endPos,
1492           String chain)
1493   {
1494     /*
1495      * Get/initialize map of data for the colour
1496      */
1497     AtomSpecModel atomSpec = map.get(value);
1498     if (atomSpec == null)
1499     {
1500       atomSpec = new AtomSpecModel();
1501       map.put(value, atomSpec);
1502     }
1503
1504     atomSpec.addRange(model, startPos, endPos, chain);
1505   }
1506
1507   /**
1508    * Returns the file extension (including '.' separator) to use for a saved
1509    * viewer session file. Default is to return null (not supported), override as
1510    * required.
1511    * 
1512    * @return
1513    */
1514   public String getSessionFileExtension()
1515   {
1516     return null;
1517   }
1518
1519   /**
1520    * If supported, saves the state of the structure viewer to a temporary file
1521    * and returns the file. Returns null and logs an error on any failure.
1522    * 
1523    * @return
1524    */
1525   public File saveSession()
1526   {
1527     String prefix = getViewerType().toString();
1528     String suffix = getSessionFileExtension();
1529     File f = null;
1530     try
1531     {
1532       f = File.createTempFile(prefix, suffix);
1533       saveSession(f);
1534     } catch (IOException e)
1535     {
1536       Console.error(String.format("Error saving %s session: %s", prefix,
1537               e.toString()));
1538     }
1539
1540     return f;
1541   }
1542
1543   /**
1544    * Use restoreSession when you want to restore a previously saved sesssion to
1545    * the running viewer instance.
1546    * 
1547    * @param absolutePath
1548    */
1549   public void restoreSession(String absolutePath)
1550   {
1551     String prefix = getViewerType().toString();
1552     try
1553     {
1554
1555       StructureCommandI cmd = commandGenerator.restoreSession(absolutePath);
1556       if (cmd != null)
1557       {
1558         executeCommand(cmd, false);
1559       }
1560     } catch (Throwable e)
1561     {
1562       Console.error(String.format("Error restoring %s session: %s", prefix,
1563               e.toString()));
1564     }
1565
1566   }
1567
1568   /**
1569    * Saves the structure viewer session to the given file
1570    * 
1571    * @param f
1572    */
1573   protected void saveSession(File f)
1574   {
1575     StructureCommandI cmd = commandGenerator.saveSession(f.getPath());
1576     if (cmd != null)
1577     {
1578       executeCommand(cmd, false);
1579     }
1580   }
1581
1582   /**
1583    * Returns true if the viewer is an external structure viewer for which the
1584    * process is still alive, else false (for Jmol, or an external viewer which
1585    * the user has independently closed)
1586    * 
1587    * @return
1588    */
1589   public boolean isViewerRunning()
1590   {
1591     return false;
1592   }
1593
1594   /**
1595    * Closes Jalview's structure viewer panel and releases associated resources.
1596    * If it is managing an external viewer program, and {@code forceClose} is
1597    * true, also asks that program to close.
1598    * 
1599    * @param forceClose
1600    */
1601   public void closeViewer(boolean forceClose)
1602   {
1603     getSsm().removeStructureViewerListener(this, this.getStructureFiles());
1604     releaseUIResources();
1605
1606     /*
1607      * end the thread that closes this panel if the external viewer closes
1608      */
1609     if (externalViewerMonitor != null)
1610     {
1611       externalViewerMonitor.interrupt();
1612       externalViewerMonitor = null;
1613     }
1614
1615     stopListening();
1616
1617     if (forceClose)
1618     {
1619       StructureCommandI cmd = getCommandGenerator().closeViewer();
1620       if (cmd != null)
1621       {
1622         executeCommand(cmd, false);
1623       }
1624     }
1625   }
1626
1627   /**
1628    * Returns the URL of a help page for the structure viewer, or null if none is
1629    * known
1630    * 
1631    * @return
1632    */
1633   public String getHelpURL()
1634   {
1635     return null;
1636   }
1637
1638   /**
1639    * <pre>
1640    * Helper method to build a map of 
1641    *   { featureType, { feature value, AtomSpecModel } }
1642    * </pre>
1643    * 
1644    * @param viewPanel
1645    * @return
1646    */
1647   protected Map<String, Map<Object, AtomSpecModel>> buildFeaturesMap(
1648           AlignmentViewPanel viewPanel)
1649   {
1650     Map<String, Map<Object, AtomSpecModel>> theMap = new LinkedHashMap<>();
1651     String[] files = getStructureFiles();
1652     if (files == null)
1653     {
1654       return theMap;
1655     }
1656
1657     FeatureRenderer fr = viewPanel.getFeatureRenderer();
1658     if (fr == null)
1659     {
1660       return theMap;
1661     }
1662
1663     AlignViewportI viewport = viewPanel.getAlignViewport();
1664     List<String> visibleFeatures = fr.getDisplayedFeatureTypes();
1665
1666     /*
1667      * if alignment is showing features from complement, we also transfer
1668      * these features to the corresponding mapped structure residues
1669      */
1670     boolean showLinkedFeatures = viewport.isShowComplementFeatures();
1671     List<String> complementFeatures = new ArrayList<>();
1672     FeatureRenderer complementRenderer = null;
1673     if (showLinkedFeatures)
1674     {
1675       AlignViewportI comp = fr.getViewport().getCodingComplement();
1676       if (comp != null)
1677       {
1678         complementRenderer = Desktop.getAlignFrameFor(comp)
1679                 .getFeatureRenderer();
1680         complementFeatures = complementRenderer.getDisplayedFeatureTypes();
1681       }
1682     }
1683     if (visibleFeatures.isEmpty() && complementFeatures.isEmpty())
1684     {
1685       return theMap;
1686     }
1687
1688     AlignmentI alignment = viewPanel.getAlignment();
1689     SequenceI[][] seqs = getSequence();
1690
1691     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
1692     {
1693       String modelId = getModelIdForFile(files[pdbfnum]);
1694       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
1695
1696       if (mapping == null || mapping.length < 1)
1697       {
1698         continue;
1699       }
1700
1701       for (int seqNo = 0; seqNo < seqs[pdbfnum].length; seqNo++)
1702       {
1703         for (int m = 0; m < mapping.length; m++)
1704         {
1705           final SequenceI seq = seqs[pdbfnum][seqNo];
1706           int sp = alignment.findIndex(seq);
1707           StructureMapping structureMapping = mapping[m];
1708           if (structureMapping.getSequence() == seq && sp > -1)
1709           {
1710             /*
1711              * found a sequence with a mapping to a structure;
1712              * now scan its features
1713              */
1714             if (!visibleFeatures.isEmpty())
1715             {
1716               scanSequenceFeatures(visibleFeatures, structureMapping, seq,
1717                       theMap, modelId);
1718             }
1719             if (showLinkedFeatures)
1720             {
1721               scanComplementFeatures(complementRenderer, structureMapping,
1722                       seq, theMap, modelId);
1723             }
1724           }
1725         }
1726       }
1727     }
1728     return theMap;
1729   }
1730
1731   /**
1732    * Ask the structure viewer to open a session file. Returns true if
1733    * successful, else false (or not supported).
1734    * 
1735    * @param filepath
1736    * @return
1737    */
1738   public boolean openSession(String filepath)
1739   {
1740     StructureCommandI cmd = getCommandGenerator().openSession(filepath);
1741     if (cmd == null)
1742     {
1743       return false;
1744     }
1745     executeCommand(cmd, true);
1746     // todo: test for failure - how?
1747     return true;
1748   }
1749
1750   /**
1751    * Scans visible features in mapped positions of the CDS/peptide complement,
1752    * and adds any found to the map of attribute values/structure positions
1753    * 
1754    * @param complementRenderer
1755    * @param structureMapping
1756    * @param seq
1757    * @param theMap
1758    * @param modelNumber
1759    */
1760   protected static void scanComplementFeatures(
1761           FeatureRenderer complementRenderer,
1762           StructureMapping structureMapping, SequenceI seq,
1763           Map<String, Map<Object, AtomSpecModel>> theMap,
1764           String modelNumber)
1765   {
1766     /*
1767      * for each sequence residue mapped to a structure position...
1768      */
1769     for (int seqPos : structureMapping.getMapping().keySet())
1770     {
1771       /*
1772        * find visible complementary features at mapped position(s)
1773        */
1774       MappedFeatures mf = complementRenderer
1775               .findComplementFeaturesAtResidue(seq, seqPos);
1776       if (mf != null)
1777       {
1778         for (SequenceFeature sf : mf.features)
1779         {
1780           String type = sf.getType();
1781
1782           /*
1783            * Don't copy features which originated from Chimera
1784            */
1785           if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
1786                   .equals(sf.getFeatureGroup()))
1787           {
1788             continue;
1789           }
1790
1791           /*
1792            * record feature 'value' (score/description/type) as at the
1793            * corresponding structure position
1794            */
1795           List<int[]> mappedRanges = structureMapping
1796                   .getPDBResNumRanges(seqPos, seqPos);
1797
1798           if (!mappedRanges.isEmpty())
1799           {
1800             String value = sf.getDescription();
1801             if (value == null || value.length() == 0)
1802             {
1803               value = type;
1804             }
1805             float score = sf.getScore();
1806             if (score != 0f && !Float.isNaN(score))
1807             {
1808               value = Float.toString(score);
1809             }
1810             Map<Object, AtomSpecModel> featureValues = theMap.get(type);
1811             if (featureValues == null)
1812             {
1813               featureValues = new HashMap<>();
1814               theMap.put(type, featureValues);
1815             }
1816             for (int[] range : mappedRanges)
1817             {
1818               addAtomSpecRange(featureValues, value, modelNumber, range[0],
1819                       range[1], structureMapping.getChain());
1820             }
1821           }
1822         }
1823       }
1824     }
1825   }
1826
1827   /**
1828    * Inspect features on the sequence; for each feature that is visible,
1829    * determine its mapped ranges in the structure (if any) according to the
1830    * given mapping, and add them to the map.
1831    * 
1832    * @param visibleFeatures
1833    * @param mapping
1834    * @param seq
1835    * @param theMap
1836    * @param modelId
1837    */
1838   protected static void scanSequenceFeatures(List<String> visibleFeatures,
1839           StructureMapping mapping, SequenceI seq,
1840           Map<String, Map<Object, AtomSpecModel>> theMap, String modelId)
1841   {
1842     List<SequenceFeature> sfs = seq.getFeatures().getPositionalFeatures(
1843             visibleFeatures.toArray(new String[visibleFeatures.size()]));
1844     for (SequenceFeature sf : sfs)
1845     {
1846       String type = sf.getType();
1847
1848       /*
1849        * Don't copy features which originated from Chimera
1850        */
1851       if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
1852               .equals(sf.getFeatureGroup()))
1853       {
1854         continue;
1855       }
1856
1857       List<int[]> mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(),
1858               sf.getEnd());
1859
1860       if (!mappedRanges.isEmpty())
1861       {
1862         String value = sf.getDescription();
1863         if (value == null || value.length() == 0)
1864         {
1865           value = type;
1866         }
1867         float score = sf.getScore();
1868         if (score != 0f && !Float.isNaN(score))
1869         {
1870           value = Float.toString(score);
1871         }
1872         Map<Object, AtomSpecModel> featureValues = theMap.get(type);
1873         if (featureValues == null)
1874         {
1875           featureValues = new HashMap<>();
1876           theMap.put(type, featureValues);
1877         }
1878         for (int[] range : mappedRanges)
1879         {
1880           addAtomSpecRange(featureValues, value, modelId, range[0],
1881                   range[1], mapping.getChain());
1882         }
1883       }
1884     }
1885   }
1886
1887   /**
1888    * Returns the number of structure files in the structure viewer and mapped to
1889    * Jalview. This may be zero if the files are still in the process of loading
1890    * in the viewer.
1891    * 
1892    * @return
1893    */
1894   public int getMappedStructureCount()
1895   {
1896     String[] files = getStructureFiles();
1897     return files == null ? 0 : files.length;
1898   }
1899
1900   /**
1901    * Starts a thread that waits for the external viewer program process to
1902    * finish, so that we can then close the associated resources. This avoids
1903    * leaving orphaned viewer panels in Jalview if the user closes the external
1904    * viewer.
1905    * 
1906    * @param p
1907    */
1908   protected void startExternalViewerMonitor(Process p)
1909   {
1910     externalViewerMonitor = new Thread(new Runnable()
1911     {
1912
1913       @Override
1914       public void run()
1915       {
1916         try
1917         {
1918           p.waitFor();
1919           JalviewStructureDisplayI display = getViewer();
1920           if (display != null)
1921           {
1922             display.closeViewer(false);
1923           }
1924         } catch (InterruptedException e)
1925         {
1926           // exit thread if Chimera Viewer is closed in Jalview
1927         }
1928       }
1929     });
1930     externalViewerMonitor.start();
1931   }
1932
1933   /**
1934    * If supported by the external structure viewer, sends it commands to notify
1935    * model or selection changes to the specified URL (where Jalview has started
1936    * a listener)
1937    * 
1938    * @param uri
1939    */
1940   protected void startListening(String uri)
1941   {
1942     List<StructureCommandI> commands = getCommandGenerator()
1943             .startNotifications(uri);
1944     if (commands != null)
1945     {
1946       executeCommands(commands, false, null);
1947     }
1948   }
1949
1950   /**
1951    * If supported by the external structure viewer, sends it commands to stop
1952    * notifying model or selection changes
1953    */
1954   protected void stopListening()
1955   {
1956     List<StructureCommandI> commands = getCommandGenerator()
1957             .stopNotifications();
1958     if (commands != null)
1959     {
1960       executeCommands(commands, false, null);
1961     }
1962   }
1963
1964   /**
1965    * If supported by the structure viewer, queries it for all residue attributes
1966    * with the given attribute name, and creates features on corresponding
1967    * residues of the alignment. Returns the number of features added.
1968    * 
1969    * @param attName
1970    * @param alignmentPanel
1971    * @return
1972    */
1973   public int copyStructureAttributesToFeatures(String attName,
1974           AlignmentPanel alignmentPanel)
1975   {
1976     StructureCommandI cmd = getCommandGenerator()
1977             .getResidueAttributes(attName);
1978     if (cmd == null)
1979     {
1980       return 0;
1981     }
1982     List<String> residueAttributes = executeCommand(cmd, true);
1983
1984     int featuresAdded = createFeaturesForAttributes(attName,
1985             residueAttributes);
1986     if (featuresAdded > 0)
1987     {
1988       alignmentPanel.getFeatureRenderer().featuresAdded();
1989     }
1990     return featuresAdded;
1991   }
1992
1993   /**
1994    * Parses {@code residueAttributes} and creates sequence features on any
1995    * mapped alignment residues. Returns the number of features created.
1996    * <p>
1997    * {@code residueAttributes} is the reply from the structure viewer to a
1998    * command to list any residue attributes for the given attribute name. Syntax
1999    * and parsing of this is viewer-specific.
2000    * 
2001    * @param attName
2002    * @param residueAttributes
2003    * @return
2004    */
2005   protected int createFeaturesForAttributes(String attName,
2006           List<String> residueAttributes)
2007   {
2008     return 0;
2009   }
2010
2011   /**
2012    * list the ligands available for display/hiding in the current view
2013    * 
2014    * @return HETATM CODE:Molecule name
2015    */
2016   public Map<String, String> getHetatmNames()
2017   {
2018     return Collections.EMPTY_MAP;
2019   }
2020
2021   /**
2022    * Generates and executes a command to show the given hetatm types as CPK
2023    * 
2024    * @param toShow
2025    *          - one or more of strings from getHetatmNames
2026    */
2027   public void showHetatms(List<String> toShow)
2028   {
2029     executeCommands(commandGenerator.showHetatms(toShow), false,
2030             "Adjusting hetatm visibility");
2031   }
2032
2033 }