JAL-4238 minor errors fixed for JalviewJS structure colouring
[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     if (files == null)
754     {
755       return true;
756     }
757     long starttime = System.currentTimeMillis();
758     long endTime = 10000 + 1000 * files.length + starttime;
759     String notLoaded = null;
760
761     boolean waiting = true;
762     while (waiting && System.currentTimeMillis() < endTime)
763     {
764       waiting = false;
765       for (String file : files)
766       {
767         notLoaded = file;
768         if (file == null)
769         {
770           continue;
771         }
772         try
773         {
774           StructureMapping[] sm = getSsm().getMapping(file);
775           if (sm == null || sm.length == 0)
776           {
777             waiting = true;
778           }
779         } catch (Throwable x)
780         {
781           waiting = true;
782         }
783       }
784     }
785
786     if (waiting)
787     {
788       jalview.bin.Console.errPrintln(
789               "Timed out waiting for structure viewer to load file "
790                       + notLoaded);
791       return false;
792     }
793     return true;
794   }
795
796   @Override
797   public boolean isListeningFor(SequenceI seq)
798   {
799     if (sequence != null)
800     {
801       for (SequenceI[] seqs : sequence)
802       {
803         if (seqs != null)
804         {
805           for (SequenceI s : seqs)
806           {
807             if (s == seq || (s.getDatasetSequence() != null
808                     && s.getDatasetSequence() == seq.getDatasetSequence()))
809             {
810               return true;
811             }
812           }
813         }
814       }
815     }
816     return false;
817   }
818
819   public boolean isFinishedInit()
820   {
821     return finishedInit;
822   }
823
824   public void setFinishedInit(boolean fi)
825   {
826     this.finishedInit = fi;
827   }
828
829   /**
830    * Returns a list of chains mapped in this viewer, formatted as
831    * "pdbid:chainCode"
832    * 
833    * @return
834    */
835   public List<String> getChainNames()
836   {
837     return chainNames;
838   }
839
840   /**
841    * Returns the Jalview panel hosting the structure viewer (if any)
842    * 
843    * @return
844    */
845   public JalviewStructureDisplayI getViewer()
846   {
847     return viewer;
848   }
849
850   public void setViewer(JalviewStructureDisplayI v)
851   {
852     viewer = v;
853   }
854
855   /**
856    * Constructs and sends a command to align structures against a reference
857    * structure, based on one or more sequence alignments. May optionally return
858    * an error or warning message for the alignment command(s).
859    * 
860    * @param alignWith
861    *          an array of one or more alignment views to process
862    * @return
863    */
864   public String superposeStructures(List<AlignmentViewPanel> alignWith)
865   {
866     String error = "";
867     String[] files = getStructureFiles();
868
869     if (!waitForFileLoad(files))
870     {
871       return null;
872     }
873     refreshPdbEntries();
874
875     for (AlignmentViewPanel view : alignWith)
876     {
877       AlignmentI alignment = view.getAlignment();
878       HiddenColumns hiddenCols = alignment.getHiddenColumns();
879       /*
880        * 'matched' bit i will be set for visible alignment columns i where
881        * all sequences have a residue with a mapping to their PDB structure
882        */
883       final int width = alignment.getWidth();
884       BitSet matched = new BitSet();
885       ColumnSelection cs = view.getAlignViewport().getColumnSelection();
886       // restrict to active column selection, if there is one
887       if (cs != null && cs.hasSelectedColumns()
888               && cs.getSelected().size() >= 4)
889       {
890         for (int s : cs.getSelected())
891         {
892           matched.set(s);
893         }
894       }
895       else
896       {
897         for (int m = 0; m < width; m++)
898         {
899           if (hiddenCols == null || hiddenCols.isVisible(m))
900           {
901             matched.set(m);
902           }
903         }
904       }
905       AAStructureBindingModel.SuperposeData[] structures = new AAStructureBindingModel.SuperposeData[files.length];
906       for (int f = 0; f < files.length; f++)
907       {
908         structures[f] = new AAStructureBindingModel.SuperposeData(width,
909                 getModelIdForFile(files[f]));
910       }
911
912       /*
913        * Calculate the superposable alignment columns ('matched'), and the
914        * corresponding structure residue positions (structures.pdbResNo)
915        */
916       int refStructure = findSuperposableResidues(alignment, matched,
917               structures);
918
919       /*
920        * require at least 4 positions to be able to execute superposition
921        */
922       int nmatched = matched.cardinality();
923       if (nmatched < MIN_POS_TO_SUPERPOSE)
924       {
925         String msg = MessageManager
926                 .formatMessage("label.insufficient_residues", nmatched);
927         error += view.getViewName() + ": " + msg + "; ";
928         continue;
929       }
930
931       /*
932        * get a model of the superposable residues in the reference structure 
933        */
934       AtomSpecModel refAtoms = getAtomSpec(structures[refStructure],
935               matched);
936
937       /*
938        * Show all as backbone before doing superposition(s)
939        * (residues used for matching will be shown as ribbon)
940        */
941       // todo better way to ensure synchronous than setting getReply true!!
942       executeCommands(commandGenerator.showBackbone(), true, null);
943
944       AtomSpecType backbone = structures[refStructure].isRna
945               ? AtomSpecType.PHOSPHATE
946               : AtomSpecType.ALPHA;
947       List<AtomSpecModel> models = new ArrayList<AtomSpecModel>();
948       models.add(refAtoms);
949       /*
950        * superpose each (other) structure to the reference in turn
951        */
952       for (int i = 0; i < structures.length; i++)
953       {
954         if (i != refStructure)
955         {
956           AtomSpecModel atomSpec = getAtomSpec(structures[i], matched);
957           List<StructureCommandI> commands = commandGenerator
958                   .superposeStructures(refAtoms, atomSpec, backbone);
959           List<String> replies = executeCommands(commands, true, null);
960           for (String reply : replies)
961           {
962             // return this error (Chimera only) to the user
963             if (reply.toLowerCase(Locale.ROOT)
964                     .contains("unequal numbers of atoms"))
965             {
966               error += "; " + reply;
967             }
968           }
969           models.add(atomSpec);
970         }
971       }
972       List<StructureCommandI> finalView = commandGenerator
973               .centerViewOn(models);
974       if (finalView != null && finalView.size() > 0)
975       {
976         executeCommands(finalView, false, "Centered on Superposition");
977       }
978     }
979     return error;
980   }
981
982   private AtomSpecModel getAtomSpec(
983           AAStructureBindingModel.SuperposeData superposeData,
984           BitSet matched)
985   {
986     AtomSpecModel model = new AtomSpecModel();
987     int nextColumnMatch = matched.nextSetBit(0);
988     while (nextColumnMatch != -1)
989     {
990       int pdbResNum = superposeData.pdbResNo[nextColumnMatch];
991       model.addRange(superposeData.modelId, pdbResNum, pdbResNum,
992               superposeData.chain);
993       nextColumnMatch = matched.nextSetBit(nextColumnMatch + 1);
994     }
995
996     return model;
997   }
998
999   /**
1000    * returns the current sequenceRenderer that should be used to colour the
1001    * structures
1002    * 
1003    * @param alignment
1004    * 
1005    * @return
1006    */
1007   public abstract SequenceRenderer getSequenceRenderer(
1008           AlignmentViewPanel alignment);
1009
1010   /**
1011    * Sends a command to the structure viewer to colour each chain with a
1012    * distinct colour (to the extent supported by the viewer)
1013    */
1014   public void colourByChain()
1015   {
1016     colourBySequence = false;
1017
1018     // TODO: JAL-628 colour chains distinctly across all visible models
1019
1020     executeCommand(false, COLOURING_STRUCTURES,
1021             commandGenerator.colourByChain());
1022   }
1023
1024   /**
1025    * Sends a command to the structure viewer to colour each chain with a
1026    * distinct colour (to the extent supported by the viewer)
1027    */
1028   public void colourByCharge()
1029   {
1030     colourBySequence = false;
1031
1032     executeCommands(commandGenerator.colourByCharge(), false,
1033             COLOURING_STRUCTURES);
1034   }
1035
1036   /**
1037    * Sends a command to the structure to apply a colour scheme (defined in
1038    * Jalview but not necessarily applied to the alignment), which defines a
1039    * colour per residue letter. More complex schemes (e.g. that depend on
1040    * consensus) cannot be used here and are ignored.
1041    * 
1042    * @param cs
1043    */
1044   public void colourByJalviewColourScheme(ColourSchemeI cs)
1045   {
1046     colourBySequence = false;
1047
1048     if (cs == null || !cs.isSimple())
1049     {
1050       return;
1051     }
1052
1053     /*
1054      * build a map of {Residue3LetterCode, Color}
1055      */
1056     Map<String, Color> colours = new HashMap<>();
1057     List<String> residues = ResidueProperties.getResidues(isNucleotide(),
1058             false);
1059     for (String resName : residues)
1060     {
1061       char res = resName.length() == 3
1062               ? ResidueProperties.getSingleCharacterCode(resName)
1063               : resName.charAt(0);
1064       Color colour = cs.findColour(res, 0, null, null, 0f);
1065       colours.put(resName, colour);
1066     }
1067
1068     /*
1069      * pass to the command constructor, and send the command
1070      */
1071     List<StructureCommandI> cmd = commandGenerator
1072             .colourByResidues(colours);
1073     executeCommands(cmd, false, COLOURING_STRUCTURES);
1074   }
1075
1076   public void setBackgroundColour(Color col)
1077   {
1078     StructureCommandI cmd = commandGenerator.setBackgroundColour(col);
1079     executeCommand(false, null, cmd);
1080   }
1081
1082   /**
1083    * Execute one structure viewer command. If {@code getReply} is true, may
1084    * optionally return one or more reply messages, else returns null.
1085    * 
1086    * @param cmd
1087    * @param getReply
1088    */
1089   protected abstract List<String> executeCommand(StructureCommandI cmd,
1090           boolean getReply);
1091
1092   /**
1093    * Executes one or more structure viewer commands
1094    * 
1095    * @param commands
1096    * @param getReply
1097    * @param msg
1098    */
1099   public List<String> executeCommands(List<StructureCommandI> commands,
1100           boolean getReply, String msg)
1101   {
1102     if (commands == null)
1103     {
1104       return null;
1105     }
1106     return executeCommand(getReply, msg,
1107             commands.toArray(new StructureCommandI[commands.size()]));
1108   }
1109
1110   /**
1111    * Executes one or more structure viewer commands, optionally returning the
1112    * reply, and optionally showing a status message while the command is being
1113    * executed.
1114    * <p>
1115    * If a reply is wanted, the execution is done synchronously (waits),
1116    * otherwise it is done in a separate thread (doesn't wait). WARNING: if you
1117    * are sending commands that need to execute before later calls to
1118    * executeCommand (e.g. mouseovers, which clean up after previous ones) then
1119    * set getReply true to ensure that commands are not executed out of order.
1120    * 
1121    * @param getReply
1122    * @param msg
1123    * @param cmds
1124    * @return
1125    */
1126   protected List<String> executeCommand(boolean getReply, String msg,
1127           StructureCommandI... cmds)
1128   {
1129     JalviewStructureDisplayI theViewer = getViewer();
1130     final long handle = msg == null ? 0 : theViewer.startProgressBar(msg);
1131
1132     if (getReply)
1133     {
1134       /*
1135        * execute and wait for reply
1136        */
1137       List<String> response = new ArrayList<>();
1138       try
1139       {
1140         for (StructureCommandI cmd : cmds)
1141         {
1142           List<String> replies = executeCommand(cmd, true);
1143           if (replies != null)
1144           {
1145             response.addAll(replies);
1146           }
1147         }
1148         return response;
1149       } finally
1150       {
1151         if (msg != null)
1152         {
1153           theViewer.stopProgressBar(null, handle);
1154         }
1155       }
1156     }
1157
1158     /*
1159      * fire and forget
1160      */
1161     String threadName = msg == null ? "StructureCommand" : msg;
1162     new Thread(new Runnable()
1163     {
1164       @Override
1165       public void run()
1166       {
1167         try
1168         {
1169           for (StructureCommandI cmd : cmds)
1170           {
1171             executeCommand(cmd, false);
1172           }
1173         } finally
1174         {
1175           if (msg != null)
1176           {
1177             SwingUtilities.invokeLater(new Runnable()
1178             {
1179               @Override
1180               public void run()
1181               {
1182                 theViewer.stopProgressBar(null, handle);
1183               }
1184             });
1185           }
1186         }
1187       }
1188     }, threadName).start();
1189     return null;
1190   }
1191
1192   /**
1193    * Colours any structures associated with sequences in the given alignment as
1194    * coloured in the alignment view, provided colourBySequence is enabled
1195    */
1196   public void colourBySequence(AlignmentViewPanel alignmentv)
1197   {
1198     if (!colourBySequence || !isLoadingFinished() || getSsm() == null)
1199     {
1200       return;
1201     }
1202     Map<Object, AtomSpecModel> colourMap = buildColoursMap(ssm, sequence,
1203             alignmentv);
1204
1205     List<StructureCommandI> colourBySequenceCommands = commandGenerator
1206             .colourBySequence(colourMap);
1207     executeCommands(colourBySequenceCommands, false, COLOURING_STRUCTURES);
1208   }
1209
1210   /**
1211    * Centre the display in the structure viewer
1212    */
1213   public void focusView()
1214   {
1215     executeCommand(false, null, commandGenerator.focusView());
1216   }
1217
1218   /**
1219    * Generates and executes a command to show only specified chains in the
1220    * structure viewer. The list of chains to show should contain entries
1221    * formatted as "pdbid:chaincode".
1222    * 
1223    * @param toShow
1224    */
1225   public void showChains(List<String> toShow)
1226   {
1227     // todo or reformat toShow list entries as modelNo:pdbId:chainCode ?
1228
1229     /*
1230      * Reformat the pdbid:chainCode values as modelNo:chainCode
1231      * since this is what is needed to construct the viewer command
1232      * todo: find a less messy way to do this
1233      */
1234     List<String> showThese = new ArrayList<>();
1235     for (String chainId : toShow)
1236     {
1237       String[] tokens = chainId.split("\\:");
1238       if (tokens.length == 2)
1239       {
1240         String pdbFile = getFileForChain(chainId);
1241         String model = getModelIdForFile(pdbFile);
1242         showThese.add(model + ":" + tokens[1]);
1243       }
1244     }
1245     executeCommands(commandGenerator.showChains(showThese), false, null);
1246   }
1247
1248   /**
1249    * Answers the structure viewer's model id given a PDB file name. Returns an
1250    * empty string if model id is not found.
1251    * 
1252    * @param chainId
1253    * @return
1254    */
1255   protected abstract String getModelIdForFile(String chainId);
1256
1257   public boolean hasFileLoadingError()
1258   {
1259     return fileLoadingError != null && fileLoadingError.length() > 0;
1260   }
1261
1262   /**
1263    * Returns the FeatureRenderer for the given alignment view
1264    * 
1265    * @param avp
1266    * @return
1267    */
1268   public FeatureRenderer getFeatureRenderer(AlignmentViewPanel avp)
1269   {
1270     AlignmentViewPanel ap = (avp == null) ? getViewer().getAlignmentPanel()
1271             : avp;
1272     if (ap == null)
1273     {
1274       return null;
1275     }
1276     return ap.getFeatureRenderer();
1277   }
1278
1279   protected void setStructureCommands(StructureCommandsI cmd)
1280   {
1281     commandGenerator = cmd;
1282   }
1283
1284   /**
1285    * Records association of one chain id (formatted as "pdbid:chainCode") with
1286    * the corresponding PDB file name
1287    * 
1288    * @param chainId
1289    * @param fileName
1290    */
1291   public void addChainFile(String chainId, String fileName)
1292   {
1293     chainFile.put(chainId, fileName);
1294   }
1295
1296   /**
1297    * Returns the PDB filename for the given chain id (formatted as
1298    * "pdbid:chainCode"), or null if not found
1299    * 
1300    * @param chainId
1301    * @return
1302    */
1303   protected String getFileForChain(String chainId)
1304   {
1305     return chainFile.get(chainId);
1306   }
1307
1308   @Override
1309   public void updateColours(Object source)
1310   {
1311     if (getViewer() == null)
1312     {
1313       // can happen if a viewer was not instantiated or cleaned up and is still
1314       // registered - mostly during tests
1315       return;
1316     }
1317     AlignmentViewPanel ap = (AlignmentViewPanel) source;
1318     // ignore events from panels not used to colour this view
1319     if (!getViewer().isUsedForColourBy(ap))
1320     {
1321       return;
1322     }
1323     if (!isLoadingFromArchive())
1324     {
1325       colourBySequence(ap);
1326     }
1327   }
1328
1329   public StructureCommandsI getCommandGenerator()
1330   {
1331     return commandGenerator;
1332   }
1333
1334   protected abstract ViewerType getViewerType();
1335
1336   /**
1337    * Builds a data structure which records mapped structure residues for each
1338    * colour. From this we can easily generate the viewer commands for colour by
1339    * sequence. Constructs and returns a map of {@code Color} to
1340    * {@code AtomSpecModel}, where the atomspec model holds
1341    * 
1342    * <pre>
1343    *   Model ids
1344    *     Chains
1345    *       Residue positions
1346    * </pre>
1347    * 
1348    * Ordering is by order of addition (for colours), natural ordering (for
1349    * models and chains)
1350    * 
1351    * @param ssm
1352    * @param sequence
1353    * @param viewPanel
1354    * @return
1355    */
1356   protected Map<Object, AtomSpecModel> buildColoursMap(
1357           StructureSelectionManager ssm, SequenceI[][] sequence,
1358           AlignmentViewPanel viewPanel)
1359   {
1360     String[] files = getStructureFiles();
1361     if (files == null)
1362     {
1363       return null;
1364     }
1365     SequenceRenderer sr = getSequenceRenderer(viewPanel);
1366     FeatureRenderer fr = viewPanel.getFeatureRenderer();
1367     FeatureColourFinder finder = new FeatureColourFinder(fr);
1368     AlignViewportI viewport = viewPanel.getAlignViewport();
1369     HiddenColumns cs = viewport.getAlignment().getHiddenColumns();
1370     AlignmentI al = viewport.getAlignment();
1371     Map<Object, AtomSpecModel> colourMap = new LinkedHashMap<>();
1372     Color lastColour = null;
1373
1374     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
1375     {
1376       final String modelId = getModelIdForFile(files[pdbfnum]);
1377       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
1378
1379       if (mapping == null || mapping.length < 1)
1380       {
1381         continue;
1382       }
1383
1384       int startPos = -1, lastPos = -1;
1385       String lastChain = "";
1386       for (int s = 0; s < sequence[pdbfnum].length; s++)
1387       {
1388         for (int sp, m = 0; m < mapping.length; m++)
1389         {
1390           final SequenceI seq = sequence[pdbfnum][s];
1391           if (mapping[m].getSequence() == seq
1392                   && (sp = al.findIndex(seq)) > -1)
1393           {
1394             SequenceI asp = al.getSequenceAt(sp);
1395             for (int r = 0; r < asp.getLength(); r++)
1396             {
1397               // no mapping to gaps in sequence
1398               if (Comparison.isGap(asp.getCharAt(r)))
1399               {
1400                 continue;
1401               }
1402               int pos = mapping[m].getPDBResNum(asp.findPosition(r));
1403
1404               if (pos < 1 || pos == lastPos)
1405               {
1406                 continue;
1407               }
1408
1409               Color colour = sr.getResidueColour(seq, r, finder);
1410
1411               /*
1412                * darker colour for hidden regions
1413                */
1414               if (!cs.isVisible(r))
1415               {
1416                 colour = Color.GRAY;
1417               }
1418
1419               final String chain = mapping[m].getChain();
1420
1421               /*
1422                * Just keep incrementing the end position for this colour range
1423                * _unless_ colour, PDB model or chain has changed, or there is a
1424                * gap in the mapped residue sequence
1425                */
1426               final boolean newColour = !colour.equals(lastColour);
1427               final boolean nonContig = lastPos + 1 != pos;
1428               final boolean newChain = !chain.equals(lastChain);
1429               if (newColour || nonContig || newChain)
1430               {
1431                 if (startPos != -1)
1432                 {
1433                   addAtomSpecRange(colourMap, lastColour, modelId, startPos,
1434                           lastPos, lastChain);
1435                 }
1436                 startPos = pos;
1437               }
1438               lastColour = colour;
1439               lastPos = pos;
1440               lastChain = chain;
1441             }
1442             // final colour range
1443             if (lastColour != null)
1444             {
1445               addAtomSpecRange(colourMap, lastColour, modelId, startPos,
1446                       lastPos, lastChain);
1447             }
1448             // break;
1449           }
1450         }
1451       }
1452     }
1453     return colourMap;
1454   }
1455
1456   /**
1457    * todo better refactoring (map lookup or similar to get viewer structure id)
1458    * 
1459    * @param pdbfnum
1460    * @param file
1461    * @return
1462    */
1463   protected String getModelId(int pdbfnum, String file)
1464   {
1465     return String.valueOf(pdbfnum);
1466   }
1467
1468   /**
1469    * Saves chains, formatted as "pdbId:chainCode", and lookups from this to the
1470    * full PDB file path
1471    * 
1472    * @param pdb
1473    * @param file
1474    */
1475   public void stashFoundChains(StructureFile pdb, String file)
1476   {
1477     for (int i = 0; i < pdb.getChains().size(); i++)
1478     {
1479       String chid = pdb.getId() + ":" + pdb.getChains().elementAt(i).id;
1480       addChainFile(chid, file);
1481       getChainNames().add(chid);
1482     }
1483   }
1484
1485   /**
1486    * Helper method to add one contiguous range to the AtomSpec model for the
1487    * given value (creating the model if necessary). As used by Jalview,
1488    * {@code value} is
1489    * <ul>
1490    * <li>a colour, when building a 'colour structure by sequence' command</li>
1491    * <li>a feature value, when building a 'set Chimera attributes from features'
1492    * command</li>
1493    * </ul>
1494    * 
1495    * @param map
1496    * @param value
1497    * @param model
1498    * @param startPos
1499    * @param endPos
1500    * @param chain
1501    */
1502   public static final void addAtomSpecRange(Map<Object, AtomSpecModel> map,
1503           Object value, String model, int startPos, int endPos,
1504           String chain)
1505   {
1506     /*
1507      * Get/initialize map of data for the colour
1508      */
1509     AtomSpecModel atomSpec = map.get(value);
1510     if (atomSpec == null)
1511     {
1512       atomSpec = new AtomSpecModel();
1513       map.put(value, atomSpec);
1514     }
1515
1516     atomSpec.addRange(model, startPos, endPos, chain);
1517   }
1518
1519   /**
1520    * Returns the file extension (including '.' separator) to use for a saved
1521    * viewer session file. Default is to return null (not supported), override as
1522    * required.
1523    * 
1524    * @return
1525    */
1526   public String getSessionFileExtension()
1527   {
1528     return null;
1529   }
1530
1531   /**
1532    * If supported, saves the state of the structure viewer to a temporary file
1533    * and returns the file. Returns null and logs an error on any failure.
1534    * 
1535    * @return
1536    */
1537   public File saveSession()
1538   {
1539     String prefix = getViewerType().toString();
1540     String suffix = getSessionFileExtension();
1541     File f = null;
1542     try
1543     {
1544       f = File.createTempFile(prefix, suffix);
1545       saveSession(f);
1546     } catch (IOException e)
1547     {
1548       Console.error(String.format("Error saving %s session: %s", prefix,
1549               e.toString()));
1550     }
1551
1552     return f;
1553   }
1554
1555   /**
1556    * Use restoreSession when you want to restore a previously saved sesssion to
1557    * the running viewer instance.
1558    * 
1559    * @param absolutePath
1560    */
1561   public void restoreSession(String absolutePath)
1562   {
1563     String prefix = getViewerType().toString();
1564     try
1565     {
1566
1567       StructureCommandI cmd = commandGenerator.restoreSession(absolutePath);
1568       if (cmd != null)
1569       {
1570         executeCommand(cmd, false);
1571       }
1572     } catch (Throwable e)
1573     {
1574       Console.error(String.format("Error restoring %s session: %s", prefix,
1575               e.toString()));
1576     }
1577
1578   }
1579
1580   /**
1581    * Saves the structure viewer session to the given file
1582    * 
1583    * @param f
1584    */
1585   protected void saveSession(File f)
1586   {
1587     StructureCommandI cmd = commandGenerator.saveSession(f.getPath());
1588     if (cmd != null)
1589     {
1590       executeCommand(cmd, false);
1591     }
1592   }
1593
1594   /**
1595    * Returns true if the viewer is an external structure viewer for which the
1596    * process is still alive, else false (for Jmol, or an external viewer which
1597    * the user has independently closed)
1598    * 
1599    * @return
1600    */
1601   public boolean isViewerRunning()
1602   {
1603     return false;
1604   }
1605
1606   /**
1607    * Closes Jalview's structure viewer panel and releases associated resources.
1608    * If it is managing an external viewer program, and {@code forceClose} is
1609    * true, also asks that program to close.
1610    * 
1611    * @param forceClose
1612    */
1613   public void closeViewer(boolean forceClose)
1614   {
1615     getSsm().removeStructureViewerListener(this, this.getStructureFiles());
1616     releaseUIResources();
1617
1618     /*
1619      * end the thread that closes this panel if the external viewer closes
1620      */
1621     if (externalViewerMonitor != null)
1622     {
1623       externalViewerMonitor.interrupt();
1624       externalViewerMonitor = null;
1625     }
1626
1627     stopListening();
1628
1629     if (forceClose)
1630     {
1631       StructureCommandI cmd = getCommandGenerator().closeViewer();
1632       if (cmd != null)
1633       {
1634         executeCommand(cmd, false);
1635       }
1636     }
1637   }
1638
1639   /**
1640    * Returns the URL of a help page for the structure viewer, or null if none is
1641    * known
1642    * 
1643    * @return
1644    */
1645   public String getHelpURL()
1646   {
1647     return null;
1648   }
1649
1650   /**
1651    * <pre>
1652    * Helper method to build a map of 
1653    *   { featureType, { feature value, AtomSpecModel } }
1654    * </pre>
1655    * 
1656    * @param viewPanel
1657    * @return
1658    */
1659   protected Map<String, Map<Object, AtomSpecModel>> buildFeaturesMap(
1660           AlignmentViewPanel viewPanel)
1661   {
1662     Map<String, Map<Object, AtomSpecModel>> theMap = new LinkedHashMap<>();
1663     String[] files = getStructureFiles();
1664     if (files == null)
1665     {
1666       return theMap;
1667     }
1668
1669     FeatureRenderer fr = viewPanel.getFeatureRenderer();
1670     if (fr == null)
1671     {
1672       return theMap;
1673     }
1674
1675     AlignViewportI viewport = viewPanel.getAlignViewport();
1676     List<String> visibleFeatures = fr.getDisplayedFeatureTypes();
1677
1678     /*
1679      * if alignment is showing features from complement, we also transfer
1680      * these features to the corresponding mapped structure residues
1681      */
1682     boolean showLinkedFeatures = viewport.isShowComplementFeatures();
1683     List<String> complementFeatures = new ArrayList<>();
1684     FeatureRenderer complementRenderer = null;
1685     if (showLinkedFeatures)
1686     {
1687       AlignViewportI comp = fr.getViewport().getCodingComplement();
1688       if (comp != null)
1689       {
1690         complementRenderer = Desktop.getAlignFrameFor(comp)
1691                 .getFeatureRenderer();
1692         complementFeatures = complementRenderer.getDisplayedFeatureTypes();
1693       }
1694     }
1695     if (visibleFeatures.isEmpty() && complementFeatures.isEmpty())
1696     {
1697       return theMap;
1698     }
1699
1700     AlignmentI alignment = viewPanel.getAlignment();
1701     SequenceI[][] seqs = getSequence();
1702
1703     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
1704     {
1705       String modelId = getModelIdForFile(files[pdbfnum]);
1706       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
1707
1708       if (mapping == null || mapping.length < 1)
1709       {
1710         continue;
1711       }
1712
1713       for (int seqNo = 0; seqNo < seqs[pdbfnum].length; seqNo++)
1714       {
1715         for (int m = 0; m < mapping.length; m++)
1716         {
1717           final SequenceI seq = seqs[pdbfnum][seqNo];
1718           int sp = alignment.findIndex(seq);
1719           StructureMapping structureMapping = mapping[m];
1720           if (structureMapping.getSequence() == seq && sp > -1)
1721           {
1722             /*
1723              * found a sequence with a mapping to a structure;
1724              * now scan its features
1725              */
1726             if (!visibleFeatures.isEmpty())
1727             {
1728               scanSequenceFeatures(visibleFeatures, structureMapping, seq,
1729                       theMap, modelId);
1730             }
1731             if (showLinkedFeatures)
1732             {
1733               scanComplementFeatures(complementRenderer, structureMapping,
1734                       seq, theMap, modelId);
1735             }
1736           }
1737         }
1738       }
1739     }
1740     return theMap;
1741   }
1742
1743   /**
1744    * Ask the structure viewer to open a session file. Returns true if
1745    * successful, else false (or not supported).
1746    * 
1747    * @param filepath
1748    * @return
1749    */
1750   public boolean openSession(String filepath)
1751   {
1752     StructureCommandI cmd = getCommandGenerator().openSession(filepath);
1753     if (cmd == null)
1754     {
1755       return false;
1756     }
1757     executeCommand(cmd, true);
1758     // todo: test for failure - how?
1759     return true;
1760   }
1761
1762   /**
1763    * Scans visible features in mapped positions of the CDS/peptide complement,
1764    * and adds any found to the map of attribute values/structure positions
1765    * 
1766    * @param complementRenderer
1767    * @param structureMapping
1768    * @param seq
1769    * @param theMap
1770    * @param modelNumber
1771    */
1772   protected static void scanComplementFeatures(
1773           FeatureRenderer complementRenderer,
1774           StructureMapping structureMapping, SequenceI seq,
1775           Map<String, Map<Object, AtomSpecModel>> theMap,
1776           String modelNumber)
1777   {
1778     /*
1779      * for each sequence residue mapped to a structure position...
1780      */
1781     for (int seqPos : structureMapping.getMapping().keySet())
1782     {
1783       /*
1784        * find visible complementary features at mapped position(s)
1785        */
1786       MappedFeatures mf = complementRenderer
1787               .findComplementFeaturesAtResidue(seq, seqPos);
1788       if (mf != null)
1789       {
1790         for (SequenceFeature sf : mf.features)
1791         {
1792           String type = sf.getType();
1793
1794           /*
1795            * Don't copy features which originated from Chimera
1796            */
1797           if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
1798                   .equals(sf.getFeatureGroup()))
1799           {
1800             continue;
1801           }
1802
1803           /*
1804            * record feature 'value' (score/description/type) as at the
1805            * corresponding structure position
1806            */
1807           List<int[]> mappedRanges = structureMapping
1808                   .getPDBResNumRanges(seqPos, seqPos);
1809
1810           if (!mappedRanges.isEmpty())
1811           {
1812             String value = sf.getDescription();
1813             if (value == null || value.length() == 0)
1814             {
1815               value = type;
1816             }
1817             float score = sf.getScore();
1818             if (score != 0f && !Float.isNaN(score))
1819             {
1820               value = Float.toString(score);
1821             }
1822             Map<Object, AtomSpecModel> featureValues = theMap.get(type);
1823             if (featureValues == null)
1824             {
1825               featureValues = new HashMap<>();
1826               theMap.put(type, featureValues);
1827             }
1828             for (int[] range : mappedRanges)
1829             {
1830               addAtomSpecRange(featureValues, value, modelNumber, range[0],
1831                       range[1], structureMapping.getChain());
1832             }
1833           }
1834         }
1835       }
1836     }
1837   }
1838
1839   /**
1840    * Inspect features on the sequence; for each feature that is visible,
1841    * determine its mapped ranges in the structure (if any) according to the
1842    * given mapping, and add them to the map.
1843    * 
1844    * @param visibleFeatures
1845    * @param mapping
1846    * @param seq
1847    * @param theMap
1848    * @param modelId
1849    */
1850   protected static void scanSequenceFeatures(List<String> visibleFeatures,
1851           StructureMapping mapping, SequenceI seq,
1852           Map<String, Map<Object, AtomSpecModel>> theMap, String modelId)
1853   {
1854     List<SequenceFeature> sfs = seq.getFeatures().getPositionalFeatures(
1855             visibleFeatures.toArray(new String[visibleFeatures.size()]));
1856     for (SequenceFeature sf : sfs)
1857     {
1858       String type = sf.getType();
1859
1860       /*
1861        * Don't copy features which originated from Chimera
1862        */
1863       if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
1864               .equals(sf.getFeatureGroup()))
1865       {
1866         continue;
1867       }
1868
1869       List<int[]> mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(),
1870               sf.getEnd());
1871
1872       if (!mappedRanges.isEmpty())
1873       {
1874         String value = sf.getDescription();
1875         if (value == null || value.length() == 0)
1876         {
1877           value = type;
1878         }
1879         float score = sf.getScore();
1880         if (score != 0f && !Float.isNaN(score))
1881         {
1882           value = Float.toString(score);
1883         }
1884         Map<Object, AtomSpecModel> featureValues = theMap.get(type);
1885         if (featureValues == null)
1886         {
1887           featureValues = new HashMap<>();
1888           theMap.put(type, featureValues);
1889         }
1890         for (int[] range : mappedRanges)
1891         {
1892           addAtomSpecRange(featureValues, value, modelId, range[0],
1893                   range[1], mapping.getChain());
1894         }
1895       }
1896     }
1897   }
1898
1899   /**
1900    * Returns the number of structure files in the structure viewer and mapped to
1901    * Jalview. This may be zero if the files are still in the process of loading
1902    * in the viewer.
1903    * 
1904    * @return
1905    */
1906   public int getMappedStructureCount()
1907   {
1908     String[] files = getStructureFiles();
1909     return files == null ? 0 : files.length;
1910   }
1911
1912   /**
1913    * Starts a thread that waits for the external viewer program process to
1914    * finish, so that we can then close the associated resources. This avoids
1915    * leaving orphaned viewer panels in Jalview if the user closes the external
1916    * viewer.
1917    * 
1918    * @param p
1919    */
1920   protected void startExternalViewerMonitor(Process p)
1921   {
1922     externalViewerMonitor = new Thread(new Runnable()
1923     {
1924
1925       @Override
1926       public void run()
1927       {
1928         try
1929         {
1930           p.waitFor();
1931           JalviewStructureDisplayI display = getViewer();
1932           if (display != null)
1933           {
1934             display.closeViewer(false);
1935           }
1936         } catch (InterruptedException e)
1937         {
1938           // exit thread if Chimera Viewer is closed in Jalview
1939         }
1940       }
1941     });
1942     externalViewerMonitor.start();
1943   }
1944
1945   /**
1946    * If supported by the external structure viewer, sends it commands to notify
1947    * model or selection changes to the specified URL (where Jalview has started
1948    * a listener)
1949    * 
1950    * @param uri
1951    */
1952   protected void startListening(String uri)
1953   {
1954     List<StructureCommandI> commands = getCommandGenerator()
1955             .startNotifications(uri);
1956     if (commands != null)
1957     {
1958       executeCommands(commands, false, null);
1959     }
1960   }
1961
1962   /**
1963    * If supported by the external structure viewer, sends it commands to stop
1964    * notifying model or selection changes
1965    */
1966   protected void stopListening()
1967   {
1968     List<StructureCommandI> commands = getCommandGenerator()
1969             .stopNotifications();
1970     if (commands != null)
1971     {
1972       executeCommands(commands, false, null);
1973     }
1974   }
1975
1976   /**
1977    * If supported by the structure viewer, queries it for all residue attributes
1978    * with the given attribute name, and creates features on corresponding
1979    * residues of the alignment. Returns the number of features added.
1980    * 
1981    * @param attName
1982    * @param alignmentPanel
1983    * @return
1984    */
1985   public int copyStructureAttributesToFeatures(String attName,
1986           AlignmentPanel alignmentPanel)
1987   {
1988     StructureCommandI cmd = getCommandGenerator()
1989             .getResidueAttributes(attName);
1990     if (cmd == null)
1991     {
1992       return 0;
1993     }
1994     List<String> residueAttributes = executeCommand(cmd, true);
1995
1996     int featuresAdded = createFeaturesForAttributes(attName,
1997             residueAttributes);
1998     if (featuresAdded > 0)
1999     {
2000       alignmentPanel.getFeatureRenderer().featuresAdded();
2001     }
2002     return featuresAdded;
2003   }
2004
2005   /**
2006    * Parses {@code residueAttributes} and creates sequence features on any
2007    * mapped alignment residues. Returns the number of features created.
2008    * <p>
2009    * {@code residueAttributes} is the reply from the structure viewer to a
2010    * command to list any residue attributes for the given attribute name. Syntax
2011    * and parsing of this is viewer-specific.
2012    * 
2013    * @param attName
2014    * @param residueAttributes
2015    * @return
2016    */
2017   protected int createFeaturesForAttributes(String attName,
2018           List<String> residueAttributes)
2019   {
2020     return 0;
2021   }
2022
2023   /**
2024    * list the ligands available for display/hiding in the current view
2025    * 
2026    * @return HETATM CODE:Molecule name
2027    */
2028   public Map<String, String> getHetatmNames()
2029   {
2030     return Collections.EMPTY_MAP;
2031   }
2032
2033   /**
2034    * Generates and executes a command to show the given hetatm types as CPK
2035    * 
2036    * @param toShow
2037    *          - one or more of strings from getHetatmNames
2038    */
2039   public void showHetatms(List<String> toShow)
2040   {
2041     executeCommands(commandGenerator.showHetatms(toShow), false,
2042             "Adjusting hetatm visibility");
2043   }
2044
2045 }