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