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