JAL-3518 removed unneeded override in ChimeraCommands
[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        * while remembering to hide any chains unselected for display
933        */
934       // todo better way to ensure synchronous than setting getReply true!!
935       executeCommands(commandGenerator.showBackbone(), true, null);
936       hideHiddenChains();
937
938       /*
939        * superpose each (other) structure to the reference in turn
940        */
941       for (int i = 0; i < structures.length; i++)
942       {
943         if (i != refStructure)
944         {
945           AtomSpecModel atomSpec = getAtomSpec(structures[i], matched);
946           List<StructureCommandI> commands = commandGenerator
947                   .superposeStructures(refAtoms, atomSpec);
948           List<String> replies = executeCommands(commands, true, null);
949           for (String reply : replies)
950           {
951             // return this error (Chimera only) to the user
952             if (reply.toLowerCase().contains("unequal numbers of atoms"))
953             {
954               error += "; " + reply;
955             }
956           }
957         }
958       }
959     }
960
961     return error;
962   }
963
964   private AtomSpecModel getAtomSpec(
965           AAStructureBindingModel.SuperposeData superposeData,
966           BitSet matched)
967   {
968     AtomSpecModel model = new AtomSpecModel();
969     int nextColumnMatch = matched.nextSetBit(0);
970     while (nextColumnMatch != -1)
971     {
972       int pdbResNum = superposeData.pdbResNo[nextColumnMatch];
973       model.addRange(superposeData.modelId, pdbResNum, pdbResNum,
974               superposeData.chain);
975       nextColumnMatch = matched.nextSetBit(nextColumnMatch + 1);
976     }
977
978     return model;
979   }
980
981   /**
982    * returns the current sequenceRenderer that should be used to colour the
983    * structures
984    * 
985    * @param alignment
986    * 
987    * @return
988    */
989   public abstract SequenceRenderer getSequenceRenderer(
990           AlignmentViewPanel alignment);
991
992   /**
993    * Recolours mapped residues in the structure viewer to match colours in the
994    * given alignment panel, provided colourBySequence is selected. Colours
995    * should also be applied to any hidden mapped residues (so that they are
996    * shown correctly if these get unhidden).
997    * 
998    * @param viewPanel
999    */
1000   protected void colourBySequence(AlignmentViewPanel viewPanel)
1001   {
1002
1003     if (!colourBySequence || !isLoadingFinished() || getSsm() == null)
1004     {
1005       return;
1006     }
1007     Map<Object, AtomSpecModel> colourMap = buildColoursMap(ssm, sequence,
1008             viewPanel);
1009
1010     List<StructureCommandI> colourBySequenceCommands = commandGenerator
1011             .colourBySequence(colourMap);
1012     executeCommands(colourBySequenceCommands, false, null);
1013
1014   }
1015
1016   /**
1017    * Sends a command to the structure viewer to colour each chain with a
1018    * distinct colour (to the extent supported by the viewer)
1019    */
1020   public void colourByChain()
1021   {
1022     colourBySequence = false;
1023     // TODO: JAL-628 colour chains distinctly across all visible models
1024     executeCommand(commandGenerator.colourByChain(), false,
1025             COLOURING_STRUCTURES);
1026   }
1027
1028   /**
1029    * Sends a command to the structure viewer to colour each chain with a
1030    * distinct colour (to the extent supported by the viewer)
1031    */
1032   public void colourByCharge()
1033   {
1034     colourBySequence = false;
1035
1036     executeCommands(commandGenerator.colourByCharge(), false,
1037             COLOURING_STRUCTURES);
1038   }
1039
1040   /**
1041    * Sends a command to the structure to apply a colour scheme (defined in
1042    * Jalview but not necessarily applied to the alignment), which defines a
1043    * colour per residue letter. More complex schemes (e.g. that depend on
1044    * consensus) cannot be used here and are ignored.
1045    * 
1046    * @param cs
1047    */
1048   public void colourByJalviewColourScheme(ColourSchemeI cs)
1049   {
1050     colourBySequence = false;
1051
1052     if (cs == null || !cs.isSimple())
1053     {
1054       return;
1055     }
1056
1057     /*
1058      * build a map of {Residue3LetterCode, Color}
1059      */
1060     Map<String, Color> colours = new HashMap<>();
1061     List<String> residues = ResidueProperties.getResidues(isNucleotide(),
1062             false);
1063     for (String resName : residues)
1064     {
1065       char res = resName.length() == 3
1066               ? ResidueProperties.getSingleCharacterCode(resName)
1067               : resName.charAt(0);
1068       Color colour = cs.findColour(res, 0, null, null, 0f);
1069       colours.put(resName, colour);
1070     }
1071
1072     /*
1073      * pass to the command constructor, and send the command
1074      */
1075     List<StructureCommandI> cmd = commandGenerator
1076             .colourByResidues(colours);
1077     executeCommands(cmd, false, COLOURING_STRUCTURES);
1078   }
1079
1080   public void setBackgroundColour(Color col)
1081   {
1082     StructureCommandI cmd = commandGenerator.setBackgroundColour(col);
1083     executeCommand(cmd, false, null);
1084   }
1085
1086   /**
1087    * Sends one command to the structure viewer. If {@code getReply} is true, the
1088    * command is sent synchronously, otherwise in a deferred thread.
1089    * <p>
1090    * If a progress message is supplied, this is displayed before command
1091    * execution, and removed afterwards.
1092    * 
1093    * @param cmd
1094    * @param getReply
1095    * @param msg
1096    * @return
1097    */
1098   private List<String> executeCommand(StructureCommandI cmd,
1099           boolean getReply, String msg)
1100   {
1101     if (cmd == null)
1102     {
1103       return null; // catch unimplemented commands
1104     }
1105     final JalviewStructureDisplayI theViewer = getViewer();
1106     final long handle = msg == null ? 0 : theViewer.startProgressBar(msg);
1107     if (getReply)
1108     {
1109       /*
1110        * synchronous (same thread) execution so reply can be returned
1111        */
1112       try
1113       {
1114         return executeCommand(cmd, true);
1115       } finally
1116       {
1117         if (msg != null)
1118         {
1119           theViewer.stopProgressBar(null, handle);
1120         }
1121       }
1122     }
1123     else
1124     {
1125       /*
1126        * asynchronous (new thread) execution if no reply needed
1127        */
1128       SwingUtilities.invokeLater(new Runnable()
1129       {
1130         @Override
1131         public void run()
1132         {
1133           try
1134           {
1135             executeCommand(cmd, false);
1136           } finally
1137           {
1138             if (msg != null)
1139             {
1140               theViewer.stopProgressBar(null, handle);
1141             }
1142           }
1143         }
1144       });
1145       return null;
1146     }
1147   }
1148
1149   /**
1150    * Execute one structure viewer command. If {@code getReply} is true, may
1151    * optionally return one or more reply messages, else returns null.
1152    * 
1153    * @param cmd
1154    * @param getReply
1155    */
1156   protected abstract List<String> executeCommand(StructureCommandI cmd,
1157           boolean getReply);
1158
1159   /**
1160    * Executes one or more structure viewer commands
1161    * 
1162    * @param commands
1163    * @param getReply
1164    * @param msg
1165    */
1166   protected List<String> executeCommands(List<StructureCommandI> commands,
1167           boolean getReply, String msg)
1168   {
1169     List<String> response = getReply ? new ArrayList<>() : null;
1170     for (StructureCommandI cmd : commands)
1171     {
1172       List<String> replies = executeCommand(cmd, getReply, msg);
1173       if (replies != null)
1174       {
1175         response.addAll(replies);
1176       }
1177     }
1178     return response;
1179   }
1180
1181   /**
1182    * Recolours the displayed structures, if they are coloured by
1183    * sequence, or 'show only visible alignment' is selected. This supports
1184    * updating structure colours on either change of alignment colours, or change
1185    * to the visible region of the alignment.
1186    */
1187   public void updateStructureColours(AlignmentViewPanel alignmentv)
1188   {
1189     if (!isLoadingFinished())
1190     {
1191       return;
1192     }
1193
1194     /*
1195      * if structure is not coloured by sequence, but restricted to the alignment,
1196      * then redraw it (but don't recolour it) in case hidden regions have changed
1197      * (todo: specific messaging for change of hidden region only)
1198      */
1199     if (!colourBySequence)
1200     {
1201       if (isShowAlignmentOnly())
1202       {
1203         showStructures(alignmentv.getAlignViewport(), false);
1204       }
1205       return;
1206     }
1207     if (getSsm() == null)
1208     {
1209       return;
1210     }
1211     colourBySequence(alignmentv);
1212   }
1213
1214   /**
1215    * Centre the display in the structure viewer
1216    */
1217   public void focusView()
1218   {
1219     executeCommand(commandGenerator.focusView(), false, null);
1220   }
1221
1222   /**
1223    * Answers the structure viewer's model id given a PDB file name. Returns an
1224    * empty string if model id is not found.
1225    * 
1226    * @param chainId
1227    * @return
1228    */
1229   protected abstract String getModelIdForFile(String chainId);
1230
1231   public boolean hasFileLoadingError()
1232   {
1233     return fileLoadingError != null && fileLoadingError.length() > 0;
1234   }
1235
1236   /**
1237    * Sets the flag for whether only mapped visible residues in the alignment
1238    * should be visible in the structure viewer
1239    * 
1240    * @param b
1241    */
1242   public void setShowAlignmentOnly(boolean b)
1243   {
1244     showAlignmentOnly = b;
1245   }
1246
1247   /**
1248    * Answers true if only residues mapped to the alignment should be shown in the
1249    * structure viewer, else false
1250    * 
1251    * @return
1252    */
1253   public boolean isShowAlignmentOnly()
1254   {
1255     return showAlignmentOnly;
1256   }
1257
1258   /**
1259    * Sets the flag for hiding regions of structure which are hidden in the
1260    * alignment (only applies when the structure viewer is restricted to the
1261    * alignment only)
1262    * 
1263    * @param b
1264    */
1265   public void setHideHiddenRegions(boolean b)
1266   {
1267     hideHiddenRegions = b;
1268   }
1269
1270   /**
1271    * Answers true if regions hidden in the alignment should also be hidden in the
1272    * structure viewer, else false (only applies when the structure viewer is
1273    * restricted to the alignment only)
1274    * 
1275    * @return
1276    */
1277   public boolean isHideHiddenRegions()
1278   {
1279     return hideHiddenRegions;
1280   }
1281
1282   /**
1283    * Shows the structures in the viewer, without changing their colouring. This
1284    * is to support toggling of whether the whole structure is shown, or only
1285    * residues mapped to visible regions of the alignment, and/or only selected
1286    * chains.
1287    * 
1288    * @param av
1289    * @param refocus
1290    *          if true, rescale the display to the viewer
1291    */
1292   public void showStructures(AlignViewportI av, boolean refocus)
1293   {
1294     if (isShowAlignmentOnly())
1295     {
1296       StructureCommandI cmd = getCommandGenerator().hideAll();
1297       executeCommand(cmd, false);
1298     }
1299
1300     AtomSpecModel model = isShowAlignmentOnly() ? getShownResidues(av)
1301             : null;
1302     StructureCommandI cmd = getCommandGenerator().showStructures(model);
1303     executeCommand(cmd, false);
1304
1305     hideHiddenChains();
1306
1307     if (refocus)
1308     {
1309       focusView();
1310     }
1311   }
1312
1313   /**
1314    * Hides any chains selected _not_ to be shown 
1315    * (whether mapped to sequence in the alignment or not)
1316    */
1317   protected void hideHiddenChains()
1318   {
1319     StructureCommandI cmd;
1320     for (String pdbChain : chainsToHide)
1321     {
1322       String modelNo = getModelIdForFile(getFileForChain(pdbChain));
1323       if (!"".equals(modelNo))
1324       {
1325         String chainId = pdbChain.split(":")[1];
1326         cmd = getCommandGenerator().hideChain(modelNo, chainId);
1327         executeCommand(cmd, false);
1328       }
1329     }
1330   }
1331
1332   /**
1333    * Sets the list of chains to hide (as "pdbid:chain")
1334    * 
1335    * @param chains
1336    */
1337   public void setChainsToHide(List<String> chains)
1338   {
1339     chainsToHide = chains;
1340   }
1341
1342   /**
1343    * Answers true if the specified structure and chain are selected to be shown in
1344    * the viewer, else false
1345    * 
1346    * @param pdbId
1347    * @param chainId
1348    * @return
1349    */
1350   public boolean isShowChain(String pdbId, String chainId)
1351   {
1352     if (chainsToHide.isEmpty())
1353     {
1354       return true;
1355     }
1356     return !chainsToHide.contains(pdbId + ":" + chainId);
1357   }
1358
1359   @Override
1360   public abstract String[] getStructureFiles();
1361
1362   /**
1363    * Builds a model of residues mapped from sequences to show on structure,
1364    * taking into account user choices of
1365    * <ul>
1366    * <li>whether hidden regions of the alignment are excluded (hidden) or
1367    * included (greyed out)</li>
1368    * <li>which chains are shown</li>
1369    * </ul>
1370    * 
1371    * @param av
1372    * @return
1373    */
1374   protected AtomSpecModel getShownResidues(AlignViewportI av)
1375   {
1376     if (!isShowAlignmentOnly())
1377     {
1378       Cache.log.error(
1379               "getShownResidues only valid for 'show alignment only')");
1380       return null;
1381     }
1382
1383     AlignmentI alignment = av.getAlignment();
1384     final int width = alignment.getWidth();
1385   
1386     String[] files = getStructureFiles();
1387   
1388     AtomSpecModel model = new AtomSpecModel();
1389   
1390     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
1391     {
1392       String fileName = files[pdbfnum];
1393       final String modelId = getModelIdForFile(files[pdbfnum]);
1394       StructureMapping[] mappings = getSsm().getMapping(fileName);
1395   
1396       /*
1397        * Find the first mapped sequence (if any) for this PDB entry which is in
1398        * the alignment
1399        */
1400       final int seqCountForPdbFile = getSequence()[pdbfnum].length;
1401       for (int s = 0; s < seqCountForPdbFile; s++)
1402       {
1403         for (StructureMapping mapping : mappings)
1404         {
1405           final SequenceI theSequence = getSequence()[pdbfnum][s];
1406           if (mapping.getSequence() == theSequence
1407                   && alignment.findIndex(theSequence) > -1)
1408           {
1409             String chainCd = mapping.getChain();
1410             if (!isShowChain(mapping.getPdbId(), chainCd))
1411             {
1412               /*
1413                * chain is not selected to be displayed, so don't
1414                * waste effort computing its structure positions
1415                */
1416               continue;
1417             }
1418             Iterator<int[]> visible;
1419             if (isHideHiddenRegions())
1420             {
1421               /*
1422                * traverse visible columns of the alignment only
1423                */
1424               visible = alignment.getHiddenColumns()
1425                     .getVisContigsIterator(0, width, true);
1426             }
1427             else
1428             {
1429               /*
1430                * traverse all columns (including hidden if any)
1431                */
1432               visible = Collections.singletonList(new int[] { 0, width })
1433                       .iterator();
1434             }
1435             while (visible.hasNext())
1436             {
1437               int[] visibleRegion = visible.next();
1438               int seqStartPos = theSequence.findPosition(visibleRegion[0]);
1439               int seqEndPos = theSequence.findPosition(visibleRegion[1]);
1440               List<int[]> residueRanges = mapping
1441                       .getPDBResNumRanges(seqStartPos, seqEndPos);
1442               if (!residueRanges.isEmpty())
1443               {
1444                 for (int[] range : residueRanges)
1445                 {
1446                   model.addRange(modelId, range[0], range[1], chainCd);
1447                 }
1448               }
1449             }
1450           }
1451         }
1452       }
1453     }
1454   
1455     return model;
1456   }
1457
1458   /**
1459    * Answers the structure viewer's model number for the given PDB file, or -1 if
1460    * not found
1461    * 
1462    * @param fileName
1463    * @param fileIndex
1464    *                    index of the file in the stored array of file names
1465    * @return
1466    */
1467   public int getModelForPdbFile(String fileName, int fileIndex)
1468   {
1469     return fileIndex;
1470   }
1471
1472   /**
1473    * Answers a default structure model specification which is simply the string
1474    * form of the model number. Override if needed to specify submodels.
1475    * 
1476    * @param model
1477    * @return
1478    */
1479   public String getModelSpec(int model)
1480   {
1481     return String.valueOf(model);
1482   }
1483
1484   /**
1485    * Returns the FeatureRenderer for the given alignment view, or null if
1486    * feature display is turned off in the view.
1487    * 
1488    * @param avp
1489    * @return
1490    */
1491   public FeatureRenderer getFeatureRenderer(AlignmentViewPanel avp)
1492   {
1493     AlignmentViewPanel ap = (avp == null) ? getViewer().getAlignmentPanel()
1494             : avp;
1495     if (ap == null)
1496     {
1497       return null;
1498     }
1499     return ap.getAlignViewport().isShowSequenceFeatures()
1500             ? ap.getFeatureRenderer()
1501             : null;
1502   }
1503
1504   protected void setStructureCommands(StructureCommandsI cmd)
1505   {
1506     commandGenerator = cmd;
1507   }
1508
1509   /**
1510    * Records association of one chain id (formatted as "pdbid:chainCode") with
1511    * the corresponding PDB file name
1512    * 
1513    * @param chainId
1514    * @param fileName
1515    */
1516   public void addChainFile(String chainId, String fileName)
1517   {
1518     chainFile.put(chainId, fileName);
1519   }
1520
1521   /**
1522    * Returns the PDB filename for the given chain id (formatted as
1523    * "pdbid:chainCode"), or null if not found
1524    * 
1525    * @param chainId
1526    * @return
1527    */
1528   protected String getFileForChain(String chainId)
1529   {
1530     return chainFile.get(chainId);
1531   }
1532
1533   @Override
1534   public void updateColours(Object source)
1535   {
1536     AlignmentViewPanel ap = (AlignmentViewPanel) source;
1537     // ignore events from panels not used to colour this view
1538     if (!getViewer().isUsedForColourBy(ap))
1539     {
1540       return;
1541     }
1542     if (!isLoadingFromArchive())
1543     {
1544       updateStructureColours(ap);
1545     }
1546   }
1547
1548   public StructureCommandsI getCommandGenerator()
1549   {
1550     return commandGenerator;
1551   }
1552
1553   protected abstract ViewerType getViewerType();
1554
1555   /**
1556    * Send a structure viewer command asynchronously in a new thread. If the
1557    * progress message is not null, display this message while the command is
1558    * executing.
1559    * 
1560    * @param command
1561    * @param progressMsg
1562    */
1563   protected void sendAsynchronousCommand(StructureCommandI command,
1564           String progressMsg)
1565   {
1566     final JalviewStructureDisplayI theViewer = getViewer();
1567     final long handle = progressMsg == null ? 0
1568             : theViewer.startProgressBar(progressMsg);
1569     SwingUtilities.invokeLater(new Runnable()
1570     {
1571       @Override
1572       public void run()
1573       {
1574         try
1575         {
1576           executeCommand(command, false, null);
1577         } finally
1578         {
1579           if (progressMsg != null)
1580           {
1581             theViewer.stopProgressBar(null, handle);
1582           }
1583         }
1584       }
1585     });
1586
1587   }
1588
1589   /**
1590    * Builds a data structure which records mapped structure residues for each
1591    * colour. From this we can easily generate the viewer commands for colour by
1592    * sequence. Constructs and returns a map of {@code Color} to
1593    * {@code AtomSpecModel}, where the atomspec model holds
1594    * 
1595    * <pre>
1596    *   Model ids
1597    *     Chains
1598    *       Residue positions
1599    * </pre>
1600    * 
1601    * Ordering is by order of addition (for colours), natural ordering (for
1602    * models and chains)
1603    * 
1604    * @param ssm
1605    * @param sequence
1606    * @param viewPanel
1607    * @return
1608    */
1609   protected Map<Object, AtomSpecModel> buildColoursMap(
1610           StructureSelectionManager ssm, SequenceI[][] sequence,
1611           AlignmentViewPanel viewPanel)
1612   {
1613     String[] files = getStructureFiles();
1614     SequenceRenderer sr = getSequenceRenderer(viewPanel);
1615     FeatureRenderer fr = viewPanel.getFeatureRenderer();
1616     FeatureColourFinder finder = new FeatureColourFinder(fr);
1617     AlignViewportI viewport = viewPanel.getAlignViewport();
1618     HiddenColumns cs = viewport.getAlignment().getHiddenColumns();
1619     AlignmentI al = viewport.getAlignment();
1620     Map<Object, AtomSpecModel> colourMap = new LinkedHashMap<>();
1621     Color lastColour = null;
1622
1623     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
1624     {
1625       final String modelId = getModelIdForFile(files[pdbfnum]);
1626       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
1627
1628       if (mapping == null || mapping.length < 1)
1629       {
1630         continue;
1631       }
1632
1633       int startPos = -1, lastPos = -1;
1634       String lastChain = "";
1635       for (int s = 0; s < sequence[pdbfnum].length; s++)
1636       {
1637         for (int sp, m = 0; m < mapping.length; m++)
1638         {
1639           final SequenceI seq = sequence[pdbfnum][s];
1640           if (mapping[m].getSequence() == seq
1641                   && (sp = al.findIndex(seq)) > -1)
1642           {
1643             SequenceI asp = al.getSequenceAt(sp);
1644             for (int r = 0; r < asp.getLength(); r++)
1645             {
1646               // no mapping to gaps in sequence
1647               if (Comparison.isGap(asp.getCharAt(r)))
1648               {
1649                 continue;
1650               }
1651               int pos = mapping[m].getPDBResNum(asp.findPosition(r));
1652
1653               if (pos < 1 || pos == lastPos)
1654               {
1655                 continue;
1656               }
1657
1658               Color colour = sr.getResidueColour(seq, r, finder);
1659
1660               /*
1661                * darker colour for hidden regions
1662                */
1663               if (!cs.isVisible(r))
1664               {
1665                 colour = Color.GRAY;
1666               }
1667
1668               final String chain = mapping[m].getChain();
1669
1670               /*
1671                * Just keep incrementing the end position for this colour range
1672                * _unless_ colour, PDB model or chain has changed, or there is a
1673                * gap in the mapped residue sequence
1674                */
1675               final boolean newColour = !colour.equals(lastColour);
1676               final boolean nonContig = lastPos + 1 != pos;
1677               final boolean newChain = !chain.equals(lastChain);
1678               if (newColour || nonContig || newChain)
1679               {
1680                 if (startPos != -1)
1681                 {
1682                   addAtomSpecRange(colourMap, lastColour, modelId, startPos,
1683                           lastPos, lastChain);
1684                 }
1685                 startPos = pos;
1686               }
1687               lastColour = colour;
1688               lastPos = pos;
1689               lastChain = chain;
1690             }
1691             // final colour range
1692             if (lastColour != null)
1693             {
1694               addAtomSpecRange(colourMap, lastColour, modelId, startPos,
1695                       lastPos, lastChain);
1696             }
1697             // break;
1698           }
1699         }
1700       }
1701     }
1702     return colourMap;
1703   }
1704
1705   /**
1706    * todo better refactoring (map lookup or similar to get viewer structure id)
1707    * 
1708    * @param pdbfnum
1709    * @param file
1710    * @return
1711    */
1712   protected String getModelId(int pdbfnum, String file)
1713   {
1714     return String.valueOf(pdbfnum);
1715   }
1716
1717   /**
1718    * Saves chains, formatted as "pdbId:chainCode", and lookups from this to the
1719    * full PDB file path
1720    * 
1721    * @param pdb
1722    * @param file
1723    */
1724   public void stashFoundChains(StructureFile pdb, String file)
1725   {
1726     for (int i = 0; i < pdb.getChains().size(); i++)
1727     {
1728       String chid = pdb.getId() + ":" + pdb.getChains().elementAt(i).id;
1729       addChainFile(chid, file);
1730       getChainNames().add(chid);
1731     }
1732   }
1733
1734   /**
1735    * Helper method to add one contiguous range to the AtomSpec model for the
1736    * given value (creating the model if necessary). As used by Jalview,
1737    * {@code value} is
1738    * <ul>
1739    * <li>a colour, when building a 'colour structure by sequence' command</li>
1740    * <li>a feature value, when building a 'set Chimera attributes from features'
1741    * command</li>
1742    * </ul>
1743    * 
1744    * @param map
1745    * @param value
1746    * @param model
1747    * @param startPos
1748    * @param endPos
1749    * @param chain
1750    */
1751   public static final void addAtomSpecRange(Map<Object, AtomSpecModel> map,
1752           Object value, String model, int startPos, int endPos,
1753           String chain)
1754   {
1755     /*
1756      * Get/initialize map of data for the colour
1757      */
1758     AtomSpecModel atomSpec = map.get(value);
1759     if (atomSpec == null)
1760     {
1761       atomSpec = new AtomSpecModel();
1762       map.put(value, atomSpec);
1763     }
1764
1765     atomSpec.addRange(model, startPos, endPos, chain);
1766   }
1767
1768   /**
1769    * Returns the file extension (including '.' separator) to use for a saved
1770    * viewer session file. Default is to return null (not supported), override as
1771    * required.
1772    * 
1773    * @return
1774    */
1775   public String getSessionFileExtension()
1776   {
1777     return null;
1778   }
1779
1780   /**
1781    * If supported, saves the state of the structure viewer to a temporary file
1782    * and returns the file. Returns null and logs an error on any failure.
1783    * 
1784    * @return
1785    */
1786   public File saveSession()
1787   {
1788     String prefix = getViewerType().toString();
1789     String suffix = getSessionFileExtension();
1790     File f = null;
1791     try
1792     {
1793       f = File.createTempFile(prefix, suffix);
1794       saveSession(f);
1795     } catch (IOException e)
1796     {
1797       Cache.log.error(String.format("Error saving %s session: %s", prefix,
1798               e.toString()));
1799     }
1800
1801     return f;
1802   }
1803
1804   /**
1805    * Saves the structure viewer session to the given file
1806    * 
1807    * @param f
1808    */
1809   protected void saveSession(File f)
1810   {
1811     StructureCommandI cmd = commandGenerator.saveSession(f.getPath());
1812     if (cmd != null)
1813     {
1814       executeCommand(cmd, false);
1815     }
1816   }
1817
1818   /**
1819    * Returns true if the viewer is an external structure viewer for which the
1820    * process is still alive, else false (for Jmol, or an external viewer which
1821    * the user has independently closed)
1822    * 
1823    * @return
1824    */
1825   public boolean isViewerRunning()
1826   {
1827     return false;
1828   }
1829
1830   /**
1831    * Closes Jalview's structure viewer panel and releases associated resources.
1832    * If it is managing an external viewer program, and {@code forceClose} is
1833    * true, also asks that program to close.
1834    * 
1835    * @param forceClose
1836    */
1837   public void closeViewer(boolean forceClose)
1838   {
1839     getSsm().removeStructureViewerListener(this, this.getStructureFiles());
1840     releaseUIResources();
1841
1842     /*
1843      * end the thread that closes this panel if the external viewer closes
1844      */
1845     if (externalViewerMonitor != null)
1846     {
1847       externalViewerMonitor.interrupt();
1848       externalViewerMonitor = null;
1849     }
1850
1851     stopListening();
1852
1853     if (forceClose)
1854     {
1855       StructureCommandI cmd = getCommandGenerator().closeViewer();
1856       if (cmd != null)
1857       {
1858         executeCommand(cmd, false);
1859       }
1860     }
1861   }
1862
1863   /**
1864    * Returns the URL of a help page for the structure viewer, or null if none is
1865    * known
1866    * 
1867    * @return
1868    */
1869   public String getHelpURL()
1870   {
1871     return null;
1872   }
1873
1874   /**
1875    * <pre>
1876    * Helper method to build a map of 
1877    *   { featureType, { feature value, AtomSpecModel } }
1878    * </pre>
1879    * 
1880    * @param viewPanel
1881    * @return
1882    */
1883   protected Map<String, Map<Object, AtomSpecModel>> buildFeaturesMap(
1884           AlignmentViewPanel viewPanel)
1885   {
1886     Map<String, Map<Object, AtomSpecModel>> theMap = new LinkedHashMap<>();
1887     String[] files = getStructureFiles();
1888     if (files == null)
1889     {
1890       return theMap;
1891     }
1892
1893     FeatureRenderer fr = viewPanel.getFeatureRenderer();
1894     if (fr == null)
1895     {
1896       return theMap;
1897     }
1898
1899     AlignViewportI viewport = viewPanel.getAlignViewport();
1900     List<String> visibleFeatures = fr.getDisplayedFeatureTypes();
1901
1902     /*
1903      * if alignment is showing features from complement, we also transfer
1904      * these features to the corresponding mapped structure residues
1905      */
1906     boolean showLinkedFeatures = viewport.isShowComplementFeatures();
1907     List<String> complementFeatures = new ArrayList<>();
1908     FeatureRenderer complementRenderer = null;
1909     if (showLinkedFeatures)
1910     {
1911       AlignViewportI comp = fr.getViewport().getCodingComplement();
1912       if (comp != null)
1913       {
1914         complementRenderer = Desktop.getAlignFrameFor(comp)
1915                 .getFeatureRenderer();
1916         complementFeatures = complementRenderer.getDisplayedFeatureTypes();
1917       }
1918     }
1919     if (visibleFeatures.isEmpty() && complementFeatures.isEmpty())
1920     {
1921       return theMap;
1922     }
1923
1924     AlignmentI alignment = viewPanel.getAlignment();
1925     SequenceI[][] seqs = getSequence();
1926
1927     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
1928     {
1929       String modelId = getModelIdForFile(files[pdbfnum]);
1930       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
1931
1932       if (mapping == null || mapping.length < 1)
1933       {
1934         continue;
1935       }
1936
1937       for (int seqNo = 0; seqNo < seqs[pdbfnum].length; seqNo++)
1938       {
1939         for (int m = 0; m < mapping.length; m++)
1940         {
1941           final SequenceI seq = seqs[pdbfnum][seqNo];
1942           int sp = alignment.findIndex(seq);
1943           StructureMapping structureMapping = mapping[m];
1944           if (structureMapping.getSequence() == seq && sp > -1)
1945           {
1946             /*
1947              * found a sequence with a mapping to a structure;
1948              * now scan its features
1949              */
1950             if (!visibleFeatures.isEmpty())
1951             {
1952               scanSequenceFeatures(visibleFeatures, structureMapping, seq,
1953                       theMap, modelId);
1954             }
1955             if (showLinkedFeatures)
1956             {
1957               scanComplementFeatures(complementRenderer, structureMapping,
1958                       seq, theMap, modelId);
1959             }
1960           }
1961         }
1962       }
1963     }
1964     return theMap;
1965   }
1966
1967   /**
1968    * Ask the structure viewer to open a session file. Returns true if
1969    * successful, else false (or not supported).
1970    * 
1971    * @param filepath
1972    * @return
1973    */
1974   public boolean openSession(String filepath)
1975   {
1976     StructureCommandI cmd = getCommandGenerator().openSession(filepath);
1977     if (cmd == null)
1978     {
1979       return false;
1980     }
1981     executeCommand(cmd, true);
1982     // todo: test for failure - how?
1983     return true;
1984   }
1985
1986   /**
1987    * Scans visible features in mapped positions of the CDS/peptide complement,
1988    * and adds any found to the map of attribute values/structure positions
1989    * 
1990    * @param complementRenderer
1991    * @param structureMapping
1992    * @param seq
1993    * @param theMap
1994    * @param modelNumber
1995    */
1996   protected static void scanComplementFeatures(
1997           FeatureRenderer complementRenderer,
1998           StructureMapping structureMapping, SequenceI seq,
1999           Map<String, Map<Object, AtomSpecModel>> theMap,
2000           String modelNumber)
2001   {
2002     /*
2003      * for each sequence residue mapped to a structure position...
2004      */
2005     for (int seqPos : structureMapping.getMapping().keySet())
2006     {
2007       /*
2008        * find visible complementary features at mapped position(s)
2009        */
2010       MappedFeatures mf = complementRenderer
2011               .findComplementFeaturesAtResidue(seq, seqPos);
2012       if (mf != null)
2013       {
2014         for (SequenceFeature sf : mf.features)
2015         {
2016           String type = sf.getType();
2017
2018           /*
2019            * Don't copy features which originated from Chimera
2020            */
2021           if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
2022                   .equals(sf.getFeatureGroup()))
2023           {
2024             continue;
2025           }
2026
2027           /*
2028            * record feature 'value' (score/description/type) as at the
2029            * corresponding structure position
2030            */
2031           List<int[]> mappedRanges = structureMapping
2032                   .getPDBResNumRanges(seqPos, seqPos);
2033
2034           if (!mappedRanges.isEmpty())
2035           {
2036             String value = sf.getDescription();
2037             if (value == null || value.length() == 0)
2038             {
2039               value = type;
2040             }
2041             float score = sf.getScore();
2042             if (score != 0f && !Float.isNaN(score))
2043             {
2044               value = Float.toString(score);
2045             }
2046             Map<Object, AtomSpecModel> featureValues = theMap.get(type);
2047             if (featureValues == null)
2048             {
2049               featureValues = new HashMap<>();
2050               theMap.put(type, featureValues);
2051             }
2052             for (int[] range : mappedRanges)
2053             {
2054               addAtomSpecRange(featureValues, value, modelNumber, range[0],
2055                       range[1], structureMapping.getChain());
2056             }
2057           }
2058         }
2059       }
2060     }
2061   }
2062
2063   /**
2064    * Inspect features on the sequence; for each feature that is visible,
2065    * determine its mapped ranges in the structure (if any) according to the
2066    * given mapping, and add them to the map.
2067    * 
2068    * @param visibleFeatures
2069    * @param mapping
2070    * @param seq
2071    * @param theMap
2072    * @param modelId
2073    */
2074   protected static void scanSequenceFeatures(List<String> visibleFeatures,
2075           StructureMapping mapping, SequenceI seq,
2076           Map<String, Map<Object, AtomSpecModel>> theMap, String modelId)
2077   {
2078     List<SequenceFeature> sfs = seq.getFeatures().getPositionalFeatures(
2079             visibleFeatures.toArray(new String[visibleFeatures.size()]));
2080     for (SequenceFeature sf : sfs)
2081     {
2082       String type = sf.getType();
2083
2084       /*
2085        * Don't copy features which originated from Chimera
2086        */
2087       if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
2088               .equals(sf.getFeatureGroup()))
2089       {
2090         continue;
2091       }
2092
2093       List<int[]> mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(),
2094               sf.getEnd());
2095
2096       if (!mappedRanges.isEmpty())
2097       {
2098         String value = sf.getDescription();
2099         if (value == null || value.length() == 0)
2100         {
2101           value = type;
2102         }
2103         float score = sf.getScore();
2104         if (score != 0f && !Float.isNaN(score))
2105         {
2106           value = Float.toString(score);
2107         }
2108         Map<Object, AtomSpecModel> featureValues = theMap.get(type);
2109         if (featureValues == null)
2110         {
2111           featureValues = new HashMap<>();
2112           theMap.put(type, featureValues);
2113         }
2114         for (int[] range : mappedRanges)
2115         {
2116           addAtomSpecRange(featureValues, value, modelId, range[0],
2117                   range[1], mapping.getChain());
2118         }
2119       }
2120     }
2121   }
2122
2123   /**
2124    * Returns the number of structure files in the structure viewer and mapped to
2125    * Jalview. This may be zero if the files are still in the process of loading
2126    * in the viewer.
2127    * 
2128    * @return
2129    */
2130   public int getMappedStructureCount()
2131   {
2132     String[] files = getStructureFiles();
2133     return files == null ? 0 : files.length;
2134   }
2135
2136   /**
2137    * Starts a thread that waits for the external viewer program process to
2138    * finish, so that we can then close the associated resources. This avoids
2139    * leaving orphaned viewer panels in Jalview if the user closes the external
2140    * viewer.
2141    * 
2142    * @param p
2143    */
2144   protected void startExternalViewerMonitor(Process p)
2145   {
2146     externalViewerMonitor = new Thread(new Runnable()
2147     {
2148
2149       @Override
2150       public void run()
2151       {
2152         try
2153         {
2154           p.waitFor();
2155           JalviewStructureDisplayI display = getViewer();
2156           if (display != null)
2157           {
2158             display.closeViewer(false);
2159           }
2160         } catch (InterruptedException e)
2161         {
2162           // exit thread if Chimera Viewer is closed in Jalview
2163         }
2164       }
2165     });
2166     externalViewerMonitor.start();
2167   }
2168
2169   /**
2170    * If supported by the external structure viewer, sends it commands to notify
2171    * model or selection changes to the specified URL (where Jalview has started
2172    * a listener)
2173    * 
2174    * @param uri
2175    */
2176   protected void startListening(String uri)
2177   {
2178     List<StructureCommandI> commands = getCommandGenerator()
2179             .startNotifications(uri);
2180     if (commands != null)
2181     {
2182       executeCommands(commands, false, null);
2183     }
2184   }
2185
2186   /**
2187    * If supported by the external structure viewer, sends it commands to stop
2188    * notifying model or selection changes
2189    */
2190   protected void stopListening()
2191   {
2192     List<StructureCommandI> commands = getCommandGenerator()
2193             .stopNotifications();
2194     if (commands != null)
2195     {
2196       executeCommands(commands, false, null);
2197     }
2198   }
2199
2200   /**
2201    * If supported by the structure viewer, queries it for all residue attributes
2202    * with the given attribute name, and creates features on corresponding
2203    * residues of the alignment. Returns the number of features added.
2204    * 
2205    * @param attName
2206    * @param alignmentPanel
2207    * @return
2208    */
2209   public int copyStructureAttributesToFeatures(String attName,
2210           AlignmentPanel alignmentPanel)
2211   {
2212     StructureCommandI cmd = getCommandGenerator()
2213             .getResidueAttributes(attName);
2214     if (cmd == null)
2215     {
2216       return 0;
2217     }
2218     List<String> residueAttributes = executeCommand(cmd, true);
2219
2220     int featuresAdded = createFeaturesForAttributes(attName,
2221             residueAttributes);
2222     if (featuresAdded > 0)
2223     {
2224       alignmentPanel.getFeatureRenderer().featuresAdded();
2225     }
2226     return featuresAdded;
2227   }
2228
2229   /**
2230    * Parses {@code residueAttributes} and creates sequence features on any
2231    * mapped alignment residues. Returns the number of features created.
2232    * <p>
2233    * {@code residueAttributes} is the reply from the structure viewer to a
2234    * command to list any residue attributes for the given attribute name. Syntax
2235    * and parsing of this is viewer-specific.
2236    * 
2237    * @param attName
2238    * @param residueAttributes
2239    * @return
2240    */
2241   protected int createFeaturesForAttributes(String attName,
2242           List<String> residueAttributes)
2243   {
2244     return 0;
2245   }
2246 }