JAL-3551 PyMOL command constants tidied
[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.StructureCommand;
61 import jalview.structure.StructureCommandI;
62 import jalview.structure.StructureCommandsI;
63 import jalview.structure.StructureListener;
64 import jalview.structure.StructureMapping;
65 import jalview.structure.StructureSelectionManager;
66 import jalview.util.Comparison;
67 import jalview.util.MessageManager;
68
69 /**
70  * 
71  * A base class to hold common function for protein structure model binding.
72  * Initial version created by refactoring JMol and Chimera binding models, but
73  * other structure viewers could in principle be accommodated in future.
74  * 
75  * @author gmcarstairs
76  *
77  */
78 public abstract class AAStructureBindingModel
79         extends SequenceStructureBindingModel
80         implements StructureListener, StructureSelectionManagerProvider
81 {
82   /**
83    * Data bean class to simplify parameterisation in superposeStructures
84    */
85   public static class SuperposeData
86   {
87     public String filename;
88
89     public String pdbId;
90
91     public String chain = "";
92
93     public boolean isRna;
94
95     /*
96      * The pdb residue number (if any) mapped to columns of the alignment
97      */
98     public int[] pdbResNo; // or use SparseIntArray?
99
100     public String modelId;
101
102     /**
103      * Constructor
104      * 
105      * @param width
106      *          width of alignment (number of columns that may potentially
107      *          participate in superposition)
108      * @param model
109      *          structure viewer model number
110      */
111     public SuperposeData(int width, String model)
112     {
113       pdbResNo = new int[width];
114       modelId = model;
115     }
116   }
117
118   private static final int MIN_POS_TO_SUPERPOSE = 4;
119
120   private static final String COLOURING_STRUCTURES = MessageManager
121           .getString("status.colouring_structures");
122
123   /*
124    * the Jalview panel through which the user interacts
125    * with the structure viewer
126    */
127   private JalviewStructureDisplayI viewer;
128
129   /*
130    * helper that generates command syntax
131    */
132   private StructureCommandsI commandGenerator;
133
134   private StructureSelectionManager ssm;
135
136   /*
137    * modelled chains, formatted as "pdbid:chainCode"
138    */
139   private List<String> chainNames;
140
141   /*
142    * lookup of pdb file name by key "pdbid:chainCode"
143    */
144   private Map<String, String> chainFile;
145
146   /*
147    * distinct PDB entries (pdb files) associated
148    * with sequences
149    */
150   private PDBEntry[] pdbEntry;
151
152   /*
153    * sequences mapped to each pdbentry
154    */
155   private SequenceI[][] sequence;
156
157   /*
158    * array of target chains for sequences - tied to pdbentry and sequence[]
159    */
160   private String[][] chains;
161
162   /*
163    * datasource protocol for access to PDBEntrylatest
164    */
165   DataSourceType protocol = null;
166
167   protected boolean colourBySequence = true;
168
169   private boolean nucleotide;
170
171   private boolean finishedInit = false;
172
173   /**
174    * current set of model filenames loaded in the viewer
175    */
176   protected String[] modelFileNames = null;
177
178   public String fileLoadingError;
179
180   protected Thread externalViewerMonitor;
181
182   /**
183    * Constructor
184    * 
185    * @param ssm
186    * @param seqs
187    */
188   public AAStructureBindingModel(StructureSelectionManager ssm,
189           SequenceI[][] seqs)
190   {
191     this.ssm = ssm;
192     this.sequence = seqs;
193     chainNames = new ArrayList<>();
194     chainFile = new HashMap<>();
195   }
196
197   /**
198    * Constructor
199    * 
200    * @param ssm
201    * @param pdbentry
202    * @param sequenceIs
203    * @param protocol
204    */
205   public AAStructureBindingModel(StructureSelectionManager ssm,
206           PDBEntry[] pdbentry, SequenceI[][] sequenceIs,
207           DataSourceType protocol)
208   {
209     this(ssm, sequenceIs);
210     this.nucleotide = Comparison.isNucleotide(sequenceIs);
211     this.pdbEntry = pdbentry;
212     this.protocol = protocol;
213     resolveChains();
214   }
215
216   private boolean resolveChains()
217   {
218     /**
219      * final count of chain mappings discovered
220      */
221     int chainmaps = 0;
222     // JBPNote: JAL-2693 - this should be a list of chain mappings per
223     // [pdbentry][sequence]
224     String[][] newchains = new String[pdbEntry.length][];
225     int pe = 0;
226     for (PDBEntry pdb : pdbEntry)
227     {
228       SequenceI[] seqsForPdb = sequence[pe];
229       if (seqsForPdb != null)
230       {
231         newchains[pe] = new String[seqsForPdb.length];
232         int se = 0;
233         for (SequenceI asq : seqsForPdb)
234         {
235           String chain = (chains != null && chains[pe] != null)
236                   ? chains[pe][se]
237                   : null;
238           SequenceI sq = (asq.getDatasetSequence() == null) ? asq
239                   : asq.getDatasetSequence();
240           if (sq.getAllPDBEntries() != null)
241           {
242             for (PDBEntry pdbentry : sq.getAllPDBEntries())
243             {
244               if (pdb.getFile() != null && pdbentry.getFile() != null
245                       && pdb.getFile().equals(pdbentry.getFile()))
246               {
247                 String chaincode = pdbentry.getChainCode();
248                 if (chaincode != null && chaincode.length() > 0)
249                 {
250                   chain = chaincode;
251                   chainmaps++;
252                   break;
253                 }
254               }
255             }
256           }
257           newchains[pe][se] = chain;
258           se++;
259         }
260         pe++;
261       }
262     }
263
264     chains = newchains;
265     return chainmaps > 0;
266   }
267
268   public StructureSelectionManager getSsm()
269   {
270     return ssm;
271   }
272
273   /**
274    * Returns the i'th PDBEntry (or null)
275    * 
276    * @param i
277    * @return
278    */
279   public PDBEntry getPdbEntry(int i)
280   {
281     return (pdbEntry != null && pdbEntry.length > i) ? pdbEntry[i] : null;
282   }
283
284   /**
285    * Answers true if this binding includes the given PDB id, else false
286    * 
287    * @param pdbId
288    * @return
289    */
290   public boolean hasPdbId(String pdbId)
291   {
292     if (pdbEntry != null)
293     {
294       for (PDBEntry pdb : pdbEntry)
295       {
296         if (pdb.getId().equals(pdbId))
297         {
298           return true;
299         }
300       }
301     }
302     return false;
303   }
304
305   /**
306    * Returns the number of modelled PDB file entries.
307    * 
308    * @return
309    */
310   public int getPdbCount()
311   {
312     return pdbEntry == null ? 0 : pdbEntry.length;
313   }
314
315   public SequenceI[][] getSequence()
316   {
317     return sequence;
318   }
319
320   public String[][] getChains()
321   {
322     return chains;
323   }
324
325   public DataSourceType getProtocol()
326   {
327     return protocol;
328   }
329
330   // TODO may remove this if calling methods can be pulled up here
331   protected void setPdbentry(PDBEntry[] pdbentry)
332   {
333     this.pdbEntry = pdbentry;
334   }
335
336   protected void setSequence(SequenceI[][] sequence)
337   {
338     this.sequence = sequence;
339   }
340
341   protected void setChains(String[][] chains)
342   {
343     this.chains = chains;
344   }
345
346   /**
347    * Construct a title string for the viewer window based on the data Jalview
348    * knows about
349    * 
350    * @param viewerName
351    *          TODO
352    * @param verbose
353    * 
354    * @return
355    */
356   public String getViewerTitle(String viewerName, boolean verbose)
357   {
358     if (getSequence() == null || getSequence().length < 1
359             || getPdbCount() < 1 || getSequence()[0].length < 1)
360     {
361       return ("Jalview " + viewerName + " Window");
362     }
363     // TODO: give a more informative title when multiple structures are
364     // displayed.
365     StringBuilder title = new StringBuilder(64);
366     final PDBEntry pdbe = getPdbEntry(0);
367     title.append(viewerName + " view for " + getSequence()[0][0].getName()
368             + ":" + pdbe.getId());
369
370     if (verbose)
371     {
372       String method = (String) pdbe.getProperty("method");
373       if (method != null)
374       {
375         title.append(" Method: ").append(method);
376       }
377       String chain = (String) pdbe.getProperty("chains");
378       if (chain != null)
379       {
380         title.append(" Chain:").append(chain);
381       }
382     }
383     return title.toString();
384   }
385
386   /**
387    * Called by after closeViewer is called, to release any resources and
388    * references so they can be garbage collected. Override if needed.
389    */
390   protected void releaseUIResources()
391   {
392   }
393
394   @Override
395   public void releaseReferences(Object svl)
396   {
397   }
398
399   public boolean isColourBySequence()
400   {
401     return colourBySequence;
402   }
403
404   /**
405    * Called when the binding thinks the UI needs to be refreshed after a
406    * structure viewer state change. This could be because structures were
407    * loaded, or because an error has occurred. Default does nothing, override as
408    * required.
409    */
410   public void refreshGUI()
411   {
412   }
413
414   /**
415    * Instruct the Jalview binding to update the pdbentries vector if necessary
416    * prior to matching the jmol view's contents to the list of structure files
417    * Jalview knows about. By default does nothing, override as required.
418    */
419   public void refreshPdbEntries()
420   {
421   }
422
423   public void setColourBySequence(boolean colourBySequence)
424   {
425     this.colourBySequence = colourBySequence;
426   }
427
428   protected void addSequenceAndChain(int pe, SequenceI[] seq,
429           String[] tchain)
430   {
431     if (pe < 0 || pe >= getPdbCount())
432     {
433       throw new Error(MessageManager.formatMessage(
434               "error.implementation_error_no_pdbentry_from_index",
435               new Object[]
436               { Integer.valueOf(pe).toString() }));
437     }
438     final String nullChain = "TheNullChain";
439     List<SequenceI> s = new ArrayList<>();
440     List<String> c = new ArrayList<>();
441     if (getChains() == null)
442     {
443       setChains(new String[getPdbCount()][]);
444     }
445     if (getSequence()[pe] != null)
446     {
447       for (int i = 0; i < getSequence()[pe].length; i++)
448       {
449         s.add(getSequence()[pe][i]);
450         if (getChains()[pe] != null)
451         {
452           if (i < getChains()[pe].length)
453           {
454             c.add(getChains()[pe][i]);
455           }
456           else
457           {
458             c.add(nullChain);
459           }
460         }
461         else
462         {
463           if (tchain != null && tchain.length > 0)
464           {
465             c.add(nullChain);
466           }
467         }
468       }
469     }
470     for (int i = 0; i < seq.length; i++)
471     {
472       if (!s.contains(seq[i]))
473       {
474         s.add(seq[i]);
475         if (tchain != null && i < tchain.length)
476         {
477           c.add(tchain[i] == null ? nullChain : tchain[i]);
478         }
479       }
480     }
481     SequenceI[] tmp = s.toArray(new SequenceI[s.size()]);
482     getSequence()[pe] = tmp;
483     if (c.size() > 0)
484     {
485       String[] tch = c.toArray(new String[c.size()]);
486       for (int i = 0; i < tch.length; i++)
487       {
488         if (tch[i] == nullChain)
489         {
490           tch[i] = null;
491         }
492       }
493       getChains()[pe] = tch;
494     }
495     else
496     {
497       getChains()[pe] = null;
498     }
499   }
500
501   /**
502    * add structures and any known sequence associations
503    * 
504    * @returns the pdb entries added to the current set.
505    */
506   public synchronized PDBEntry[] addSequenceAndChain(PDBEntry[] pdbe,
507           SequenceI[][] seq, String[][] chns)
508   {
509     List<PDBEntry> v = new ArrayList<>();
510     List<int[]> rtn = new ArrayList<>();
511     for (int i = 0; i < getPdbCount(); i++)
512     {
513       v.add(getPdbEntry(i));
514     }
515     for (int i = 0; i < pdbe.length; i++)
516     {
517       int r = v.indexOf(pdbe[i]);
518       if (r == -1 || r >= getPdbCount())
519       {
520         rtn.add(new int[] { v.size(), i });
521         v.add(pdbe[i]);
522       }
523       else
524       {
525         // just make sure the sequence/chain entries are all up to date
526         addSequenceAndChain(r, seq[i], chns[i]);
527       }
528     }
529     pdbe = v.toArray(new PDBEntry[v.size()]);
530     setPdbentry(pdbe);
531     if (rtn.size() > 0)
532     {
533       // expand the tied sequence[] and string[] arrays
534       SequenceI[][] sqs = new SequenceI[getPdbCount()][];
535       String[][] sch = new String[getPdbCount()][];
536       System.arraycopy(getSequence(), 0, sqs, 0, getSequence().length);
537       System.arraycopy(getChains(), 0, sch, 0, this.getChains().length);
538       setSequence(sqs);
539       setChains(sch);
540       pdbe = new PDBEntry[rtn.size()];
541       for (int r = 0; r < pdbe.length; r++)
542       {
543         int[] stri = (rtn.get(r));
544         // record the pdb file as a new addition
545         pdbe[r] = getPdbEntry(stri[0]);
546         // and add the new sequence/chain entries
547         addSequenceAndChain(stri[0], seq[stri[1]], chns[stri[1]]);
548       }
549     }
550     else
551     {
552       pdbe = null;
553     }
554     return pdbe;
555   }
556
557   /**
558    * Add sequences to the pe'th pdbentry's sequence set.
559    * 
560    * @param pe
561    * @param seq
562    */
563   public void addSequence(int pe, SequenceI[] seq)
564   {
565     addSequenceAndChain(pe, seq, null);
566   }
567
568   /**
569    * add the given sequences to the mapping scope for the given pdb file handle
570    * 
571    * @param pdbFile
572    *          - pdbFile identifier
573    * @param seq
574    *          - set of sequences it can be mapped to
575    */
576   public void addSequenceForStructFile(String pdbFile, SequenceI[] seq)
577   {
578     for (int pe = 0; pe < getPdbCount(); pe++)
579     {
580       if (getPdbEntry(pe).getFile().equals(pdbFile))
581       {
582         addSequence(pe, seq);
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(commandGenerator.colourByChain(), false,
985             COLOURING_STRUCTURES);
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(cmd, false, null);
1044   }
1045
1046   /**
1047    * Sends one command to the structure viewer. If {@code getReply} is true, the
1048    * command is sent synchronously, otherwise in a deferred thread.
1049    * <p>
1050    * If a progress message is supplied, this is displayed before command
1051    * execution, and removed afterwards.
1052    * 
1053    * @param cmd
1054    * @param getReply
1055    * @param msg
1056    * @return
1057    */
1058   private List<String> executeCommand(StructureCommandI cmd,
1059           boolean getReply, String msg)
1060   {
1061     final JalviewStructureDisplayI theViewer = getViewer();
1062     final long handle = msg == null ? 0 : theViewer.startProgressBar(msg);
1063     if (getReply)
1064     {
1065       /*
1066        * synchronous (same thread) execution so reply can be returned
1067        */
1068       try
1069       {
1070         return executeCommand(cmd, true);
1071       } finally
1072       {
1073         if (msg != null)
1074         {
1075           theViewer.stopProgressBar(null, handle);
1076         }
1077       }
1078     }
1079     else
1080     {
1081       /*
1082        * asynchronous (new thread) execution if no reply needed
1083        */
1084       SwingUtilities.invokeLater(new Runnable()
1085       {
1086         @Override
1087         public void run()
1088         {
1089           try
1090           {
1091             executeCommand(cmd, false);
1092           } finally
1093           {
1094             if (msg != null)
1095             {
1096               theViewer.stopProgressBar(null, handle);
1097             }
1098           }
1099         }
1100       });
1101       return null;
1102     }
1103   }
1104
1105   /**
1106    * Execute one structure viewer command. If {@code getReply} is true, may
1107    * optionally return one or more reply messages, else returns null.
1108    * 
1109    * @param cmd
1110    * @param getReply
1111    */
1112   protected abstract List<String> executeCommand(StructureCommandI cmd,
1113           boolean getReply);
1114
1115   /**
1116    * Executes one or more structure viewer commands
1117    * 
1118    * @param commands
1119    * @param getReply
1120    * @param msg
1121    */
1122   protected List<String> executeCommands(List<StructureCommandI> commands,
1123           boolean getReply, String msg)
1124   {
1125     List<String> response = getReply ? new ArrayList<>() : null;
1126     for (StructureCommandI cmd : commands)
1127     {
1128       List<String> replies = executeCommand(cmd, getReply, msg);
1129       if (replies != null)
1130       {
1131         response.addAll(replies);
1132       }
1133     }
1134     return response;
1135   }
1136
1137   /**
1138    * Colours any structures associated with sequences in the given alignment as
1139    * coloured in the alignment view, provided colourBySequence is enabled
1140    */
1141   public void colourBySequence(AlignmentViewPanel alignmentv)
1142   {
1143     if (!colourBySequence || !isLoadingFinished() || getSsm() == null)
1144     {
1145       return;
1146     }
1147     Map<Object, AtomSpecModel> colourMap = buildColoursMap(ssm, sequence,
1148             alignmentv);
1149
1150     List<StructureCommandI> colourBySequenceCommands = commandGenerator
1151             .colourBySequence(colourMap);
1152     executeCommands(colourBySequenceCommands, false, COLOURING_STRUCTURES);
1153   }
1154
1155   /**
1156    * Centre the display in the structure viewer
1157    */
1158   public void focusView()
1159   {
1160     executeCommand(commandGenerator.focusView(), false, null);
1161   }
1162
1163   /**
1164    * Generates and executes a command to show only specified chains in the
1165    * structure viewer. The list of chains to show should contain entries
1166    * formatted as "pdbid:chaincode".
1167    * 
1168    * @param toShow
1169    */
1170   public void showChains(List<String> toShow)
1171   {
1172     // todo or reformat toShow list entries as modelNo:pdbId:chainCode ?
1173
1174     /*
1175      * Reformat the pdbid:chainCode values as modelNo:chainCode
1176      * since this is what is needed to construct the viewer command
1177      * todo: find a less messy way to do this
1178      */
1179     List<String> showThese = new ArrayList<>();
1180     for (String chainId : toShow)
1181     {
1182       String[] tokens = chainId.split("\\:");
1183       if (tokens.length == 2)
1184       {
1185         String pdbFile = getFileForChain(chainId);
1186         String model = getModelIdForFile(pdbFile);
1187         showThese.add(model + ":" + tokens[1]);
1188       }
1189     }
1190     executeCommands(commandGenerator.showChains(showThese), false, null);
1191   }
1192
1193   /**
1194    * Answers the structure viewer's model id given a PDB file name. Returns an
1195    * empty string if model id is not found.
1196    * 
1197    * @param chainId
1198    * @return
1199    */
1200   protected abstract String getModelIdForFile(String chainId);
1201
1202   public boolean hasFileLoadingError()
1203   {
1204     return fileLoadingError != null && fileLoadingError.length() > 0;
1205   }
1206
1207   /**
1208    * Returns the FeatureRenderer for the given alignment view, or null if
1209    * feature display is turned off in the view.
1210    * 
1211    * @param avp
1212    * @return
1213    */
1214   public FeatureRenderer getFeatureRenderer(AlignmentViewPanel avp)
1215   {
1216     AlignmentViewPanel ap = (avp == null) ? getViewer().getAlignmentPanel()
1217             : avp;
1218     if (ap == null)
1219     {
1220       return null;
1221     }
1222     return ap.getAlignViewport().isShowSequenceFeatures()
1223             ? ap.getFeatureRenderer()
1224             : null;
1225   }
1226
1227   protected void setStructureCommands(StructureCommandsI cmd)
1228   {
1229     commandGenerator = cmd;
1230   }
1231
1232   /**
1233    * Records association of one chain id (formatted as "pdbid:chainCode") with
1234    * the corresponding PDB file name
1235    * 
1236    * @param chainId
1237    * @param fileName
1238    */
1239   public void addChainFile(String chainId, String fileName)
1240   {
1241     chainFile.put(chainId, fileName);
1242   }
1243
1244   /**
1245    * Returns the PDB filename for the given chain id (formatted as
1246    * "pdbid:chainCode"), or null if not found
1247    * 
1248    * @param chainId
1249    * @return
1250    */
1251   protected String getFileForChain(String chainId)
1252   {
1253     return chainFile.get(chainId);
1254   }
1255
1256   @Override
1257   public void updateColours(Object source)
1258   {
1259     AlignmentViewPanel ap = (AlignmentViewPanel) source;
1260     // ignore events from panels not used to colour this view
1261     if (!getViewer().isUsedForColourBy(ap))
1262     {
1263       return;
1264     }
1265     if (!isLoadingFromArchive())
1266     {
1267       colourBySequence(ap);
1268     }
1269   }
1270
1271   public StructureCommandsI getCommandGenerator()
1272   {
1273     return commandGenerator;
1274   }
1275
1276   protected abstract ViewerType getViewerType();
1277
1278   /**
1279    * Send a structure viewer command asynchronously in a new thread. If the
1280    * progress message is not null, display this message while the command is
1281    * executing.
1282    * 
1283    * @param command
1284    * @param progressMsg
1285    */
1286   protected void sendAsynchronousCommand(StructureCommandI command,
1287           String progressMsg)
1288   {
1289     final JalviewStructureDisplayI theViewer = getViewer();
1290     final long handle = progressMsg == null ? 0
1291             : theViewer.startProgressBar(progressMsg);
1292     SwingUtilities.invokeLater(new Runnable()
1293     {
1294       @Override
1295       public void run()
1296       {
1297         try
1298         {
1299           executeCommand(command, false, null);
1300         } finally
1301         {
1302           if (progressMsg != null)
1303           {
1304             theViewer.stopProgressBar(null, handle);
1305           }
1306         }
1307       }
1308     });
1309
1310   }
1311
1312   /**
1313    * Builds a data structure which records mapped structure residues for each
1314    * colour. From this we can easily generate the viewer commands for colour by
1315    * sequence. Constructs and returns a map of {@code Color} to
1316    * {@code AtomSpecModel}, where the atomspec model holds
1317    * 
1318    * <pre>
1319    *   Model ids
1320    *     Chains
1321    *       Residue positions
1322    * </pre>
1323    * 
1324    * Ordering is by order of addition (for colours), natural ordering (for
1325    * models and chains)
1326    * 
1327    * @param ssm
1328    * @param sequence
1329    * @param viewPanel
1330    * @return
1331    */
1332   protected Map<Object, AtomSpecModel> buildColoursMap(
1333           StructureSelectionManager ssm, SequenceI[][] sequence,
1334           AlignmentViewPanel viewPanel)
1335   {
1336     String[] files = getStructureFiles();
1337     SequenceRenderer sr = getSequenceRenderer(viewPanel);
1338     FeatureRenderer fr = viewPanel.getFeatureRenderer();
1339     FeatureColourFinder finder = new FeatureColourFinder(fr);
1340     AlignViewportI viewport = viewPanel.getAlignViewport();
1341     HiddenColumns cs = viewport.getAlignment().getHiddenColumns();
1342     AlignmentI al = viewport.getAlignment();
1343     Map<Object, AtomSpecModel> colourMap = new LinkedHashMap<>();
1344     Color lastColour = null;
1345
1346     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
1347     {
1348       final String modelId = getModelIdForFile(files[pdbfnum]);
1349       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
1350
1351       if (mapping == null || mapping.length < 1)
1352       {
1353         continue;
1354       }
1355
1356       int startPos = -1, lastPos = -1;
1357       String lastChain = "";
1358       for (int s = 0; s < sequence[pdbfnum].length; s++)
1359       {
1360         for (int sp, m = 0; m < mapping.length; m++)
1361         {
1362           final SequenceI seq = sequence[pdbfnum][s];
1363           if (mapping[m].getSequence() == seq
1364                   && (sp = al.findIndex(seq)) > -1)
1365           {
1366             SequenceI asp = al.getSequenceAt(sp);
1367             for (int r = 0; r < asp.getLength(); r++)
1368             {
1369               // no mapping to gaps in sequence
1370               if (Comparison.isGap(asp.getCharAt(r)))
1371               {
1372                 continue;
1373               }
1374               int pos = mapping[m].getPDBResNum(asp.findPosition(r));
1375
1376               if (pos < 1 || pos == lastPos)
1377               {
1378                 continue;
1379               }
1380
1381               Color colour = sr.getResidueColour(seq, r, finder);
1382
1383               /*
1384                * darker colour for hidden regions
1385                */
1386               if (!cs.isVisible(r))
1387               {
1388                 colour = Color.GRAY;
1389               }
1390
1391               final String chain = mapping[m].getChain();
1392
1393               /*
1394                * Just keep incrementing the end position for this colour range
1395                * _unless_ colour, PDB model or chain has changed, or there is a
1396                * gap in the mapped residue sequence
1397                */
1398               final boolean newColour = !colour.equals(lastColour);
1399               final boolean nonContig = lastPos + 1 != pos;
1400               final boolean newChain = !chain.equals(lastChain);
1401               if (newColour || nonContig || newChain)
1402               {
1403                 if (startPos != -1)
1404                 {
1405                   addAtomSpecRange(colourMap, lastColour, modelId, startPos,
1406                           lastPos, lastChain);
1407                 }
1408                 startPos = pos;
1409               }
1410               lastColour = colour;
1411               lastPos = pos;
1412               lastChain = chain;
1413             }
1414             // final colour range
1415             if (lastColour != null)
1416             {
1417               addAtomSpecRange(colourMap, lastColour, modelId, startPos,
1418                       lastPos, lastChain);
1419             }
1420             // break;
1421           }
1422         }
1423       }
1424     }
1425     return colourMap;
1426   }
1427
1428   /**
1429    * todo better refactoring (map lookup or similar to get viewer structure id)
1430    * 
1431    * @param pdbfnum
1432    * @param file
1433    * @return
1434    */
1435   protected String getModelId(int pdbfnum, String file)
1436   {
1437     return String.valueOf(pdbfnum);
1438   }
1439
1440   /**
1441    * Saves chains, formatted as "pdbId:chainCode", and lookups from this to the
1442    * full PDB file path
1443    * 
1444    * @param pdb
1445    * @param file
1446    */
1447   public void stashFoundChains(StructureFile pdb, String file)
1448   {
1449     for (int i = 0; i < pdb.getChains().size(); i++)
1450     {
1451       String chid = pdb.getId() + ":" + pdb.getChains().elementAt(i).id;
1452       addChainFile(chid, file);
1453       getChainNames().add(chid);
1454     }
1455   }
1456
1457   /**
1458    * Helper method to add one contiguous range to the AtomSpec model for the
1459    * given value (creating the model if necessary). As used by Jalview,
1460    * {@code value} is
1461    * <ul>
1462    * <li>a colour, when building a 'colour structure by sequence' command</li>
1463    * <li>a feature value, when building a 'set Chimera attributes from features'
1464    * command</li>
1465    * </ul>
1466    * 
1467    * @param map
1468    * @param value
1469    * @param model
1470    * @param startPos
1471    * @param endPos
1472    * @param chain
1473    */
1474   public static final void addAtomSpecRange(Map<Object, AtomSpecModel> map,
1475           Object value, String model, int startPos, int endPos,
1476           String chain)
1477   {
1478     /*
1479      * Get/initialize map of data for the colour
1480      */
1481     AtomSpecModel atomSpec = map.get(value);
1482     if (atomSpec == null)
1483     {
1484       atomSpec = new AtomSpecModel();
1485       map.put(value, atomSpec);
1486     }
1487
1488     atomSpec.addRange(model, startPos, endPos, chain);
1489   }
1490
1491   /**
1492    * Returns the file extension (including '.' separator) to use for a saved
1493    * viewer session file. Default is to return null (not supported), override as
1494    * required.
1495    * 
1496    * @return
1497    */
1498   public String getSessionFileExtension()
1499   {
1500     return null;
1501   }
1502
1503   /**
1504    * If supported, saves the state of the structure viewer to a temporary file
1505    * and returns the file. Returns null and logs an error on any failure.
1506    * 
1507    * @return
1508    */
1509   public File saveSession()
1510   {
1511     String prefix = getViewerType().toString();
1512     String suffix = getSessionFileExtension();
1513     File f = null;
1514     try
1515     {
1516       f = File.createTempFile(prefix, suffix);
1517       saveSession(f);
1518     } catch (IOException e)
1519     {
1520       Cache.log.error(String.format("Error saving %s session: %s", prefix,
1521               e.toString()));
1522     }
1523
1524     return f;
1525   }
1526
1527   /**
1528    * Saves the structure viewer session to the given file
1529    * 
1530    * @param f
1531    */
1532   protected void saveSession(File f)
1533   {
1534     StructureCommandI cmd = commandGenerator.saveSession(f.getPath());
1535     if (cmd != null)
1536     {
1537       executeCommand(cmd, false);
1538     }
1539   }
1540
1541   /**
1542    * Returns true if the viewer is an external structure viewer for which the
1543    * process is still alive, else false (for Jmol, or an external viewer which
1544    * the user has independently closed)
1545    * 
1546    * @return
1547    */
1548   public boolean isViewerRunning()
1549   {
1550     return false;
1551   }
1552
1553   /**
1554    * Closes Jalview's structure viewer panel and releases associated resources.
1555    * If it is managing an external viewer program, and {@code forceClose} is
1556    * true, also asks that program to close.
1557    * 
1558    * @param forceClose
1559    */
1560   public void closeViewer(boolean forceClose)
1561   {
1562     getSsm().removeStructureViewerListener(this, this.getStructureFiles());
1563     releaseUIResources();
1564
1565     /*
1566      * end the thread that closes this panel if the external viewer closes
1567      */
1568     if (externalViewerMonitor != null)
1569     {
1570       externalViewerMonitor.interrupt();
1571       externalViewerMonitor = null;
1572     }
1573
1574     stopListening();
1575
1576     if (forceClose)
1577     {
1578       StructureCommandI cmd = getCommandGenerator().closeViewer();
1579       if (cmd != null)
1580       {
1581         executeCommand(cmd, false);
1582       }
1583     }
1584   }
1585
1586   /**
1587    * Returns the URL of a help page for the structure viewer, or null if none is
1588    * known
1589    * 
1590    * @return
1591    */
1592   public String getHelpURL()
1593   {
1594     return null;
1595   }
1596
1597   /**
1598    * <pre>
1599    * Helper method to build a map of 
1600    *   { featureType, { feature value, AtomSpecModel } }
1601    * </pre>
1602    * 
1603    * @param viewPanel
1604    * @return
1605    */
1606   protected Map<String, Map<Object, AtomSpecModel>> buildFeaturesMap(
1607           AlignmentViewPanel viewPanel)
1608   {
1609     Map<String, Map<Object, AtomSpecModel>> theMap = new LinkedHashMap<>();
1610     String[] files = getStructureFiles();
1611     if (files == null)
1612     {
1613       return theMap;
1614     }
1615
1616     FeatureRenderer fr = viewPanel.getFeatureRenderer();
1617     if (fr == null)
1618     {
1619       return theMap;
1620     }
1621
1622     AlignViewportI viewport = viewPanel.getAlignViewport();
1623     List<String> visibleFeatures = fr.getDisplayedFeatureTypes();
1624
1625     /*
1626      * if alignment is showing features from complement, we also transfer
1627      * these features to the corresponding mapped structure residues
1628      */
1629     boolean showLinkedFeatures = viewport.isShowComplementFeatures();
1630     List<String> complementFeatures = new ArrayList<>();
1631     FeatureRenderer complementRenderer = null;
1632     if (showLinkedFeatures)
1633     {
1634       AlignViewportI comp = fr.getViewport().getCodingComplement();
1635       if (comp != null)
1636       {
1637         complementRenderer = Desktop.getAlignFrameFor(comp)
1638                 .getFeatureRenderer();
1639         complementFeatures = complementRenderer.getDisplayedFeatureTypes();
1640       }
1641     }
1642     if (visibleFeatures.isEmpty() && complementFeatures.isEmpty())
1643     {
1644       return theMap;
1645     }
1646
1647     AlignmentI alignment = viewPanel.getAlignment();
1648     SequenceI[][] seqs = getSequence();
1649
1650     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
1651     {
1652       String modelId = getModelIdForFile(files[pdbfnum]);
1653       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
1654
1655       if (mapping == null || mapping.length < 1)
1656       {
1657         continue;
1658       }
1659
1660       for (int seqNo = 0; seqNo < seqs[pdbfnum].length; seqNo++)
1661       {
1662         for (int m = 0; m < mapping.length; m++)
1663         {
1664           final SequenceI seq = seqs[pdbfnum][seqNo];
1665           int sp = alignment.findIndex(seq);
1666           StructureMapping structureMapping = mapping[m];
1667           if (structureMapping.getSequence() == seq && sp > -1)
1668           {
1669             /*
1670              * found a sequence with a mapping to a structure;
1671              * now scan its features
1672              */
1673             if (!visibleFeatures.isEmpty())
1674             {
1675               scanSequenceFeatures(visibleFeatures, structureMapping, seq,
1676                       theMap, modelId);
1677             }
1678             if (showLinkedFeatures)
1679             {
1680               scanComplementFeatures(complementRenderer, structureMapping,
1681                       seq, theMap, modelId);
1682             }
1683           }
1684         }
1685       }
1686     }
1687     return theMap;
1688   }
1689
1690   /**
1691    * Ask the structure viewer to open a session file. Returns true if
1692    * successful, else false (or not supported).
1693    * 
1694    * @param filepath
1695    * @return
1696    */
1697   public boolean openSession(String filepath)
1698   {
1699     StructureCommandI cmd = getCommandGenerator().openSession(filepath);
1700     if (cmd == null)
1701     {
1702       return false;
1703     }
1704     executeCommand(cmd, true);
1705     // todo: test for failure - how?
1706     return true;
1707   }
1708
1709   /**
1710    * Scans visible features in mapped positions of the CDS/peptide complement,
1711    * and adds any found to the map of attribute values/structure positions
1712    * 
1713    * @param complementRenderer
1714    * @param structureMapping
1715    * @param seq
1716    * @param theMap
1717    * @param modelNumber
1718    */
1719   protected static void scanComplementFeatures(
1720           FeatureRenderer complementRenderer,
1721           StructureMapping structureMapping, SequenceI seq,
1722           Map<String, Map<Object, AtomSpecModel>> theMap,
1723           String modelNumber)
1724   {
1725     /*
1726      * for each sequence residue mapped to a structure position...
1727      */
1728     for (int seqPos : structureMapping.getMapping().keySet())
1729     {
1730       /*
1731        * find visible complementary features at mapped position(s)
1732        */
1733       MappedFeatures mf = complementRenderer
1734               .findComplementFeaturesAtResidue(seq, seqPos);
1735       if (mf != null)
1736       {
1737         for (SequenceFeature sf : mf.features)
1738         {
1739           String type = sf.getType();
1740
1741           /*
1742            * Don't copy features which originated from Chimera
1743            */
1744           if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
1745                   .equals(sf.getFeatureGroup()))
1746           {
1747             continue;
1748           }
1749
1750           /*
1751            * record feature 'value' (score/description/type) as at the
1752            * corresponding structure position
1753            */
1754           List<int[]> mappedRanges = structureMapping
1755                   .getPDBResNumRanges(seqPos, seqPos);
1756
1757           if (!mappedRanges.isEmpty())
1758           {
1759             String value = sf.getDescription();
1760             if (value == null || value.length() == 0)
1761             {
1762               value = type;
1763             }
1764             float score = sf.getScore();
1765             if (score != 0f && !Float.isNaN(score))
1766             {
1767               value = Float.toString(score);
1768             }
1769             Map<Object, AtomSpecModel> featureValues = theMap.get(type);
1770             if (featureValues == null)
1771             {
1772               featureValues = new HashMap<>();
1773               theMap.put(type, featureValues);
1774             }
1775             for (int[] range : mappedRanges)
1776             {
1777               addAtomSpecRange(featureValues, value, modelNumber, range[0],
1778                       range[1], structureMapping.getChain());
1779             }
1780           }
1781         }
1782       }
1783     }
1784   }
1785
1786   /**
1787    * Inspect features on the sequence; for each feature that is visible,
1788    * determine its mapped ranges in the structure (if any) according to the
1789    * given mapping, and add them to the map.
1790    * 
1791    * @param visibleFeatures
1792    * @param mapping
1793    * @param seq
1794    * @param theMap
1795    * @param modelId
1796    */
1797   protected static void scanSequenceFeatures(List<String> visibleFeatures,
1798           StructureMapping mapping, SequenceI seq,
1799           Map<String, Map<Object, AtomSpecModel>> theMap, String modelId)
1800   {
1801     List<SequenceFeature> sfs = seq.getFeatures().getPositionalFeatures(
1802             visibleFeatures.toArray(new String[visibleFeatures.size()]));
1803     for (SequenceFeature sf : sfs)
1804     {
1805       String type = sf.getType();
1806
1807       /*
1808        * Don't copy features which originated from Chimera
1809        */
1810       if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
1811               .equals(sf.getFeatureGroup()))
1812       {
1813         continue;
1814       }
1815
1816       List<int[]> mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(),
1817               sf.getEnd());
1818
1819       if (!mappedRanges.isEmpty())
1820       {
1821         String value = sf.getDescription();
1822         if (value == null || value.length() == 0)
1823         {
1824           value = type;
1825         }
1826         float score = sf.getScore();
1827         if (score != 0f && !Float.isNaN(score))
1828         {
1829           value = Float.toString(score);
1830         }
1831         Map<Object, AtomSpecModel> featureValues = theMap.get(type);
1832         if (featureValues == null)
1833         {
1834           featureValues = new HashMap<>();
1835           theMap.put(type, featureValues);
1836         }
1837         for (int[] range : mappedRanges)
1838         {
1839           addAtomSpecRange(featureValues, value, modelId, range[0],
1840                   range[1], mapping.getChain());
1841         }
1842       }
1843     }
1844   }
1845
1846   /**
1847    * Returns the number of structure files in the structure viewer and mapped to
1848    * Jalview. This may be zero if the files are still in the process of loading
1849    * in the viewer.
1850    * 
1851    * @return
1852    */
1853   public int getMappedStructureCount()
1854   {
1855     String[] files = getStructureFiles();
1856     return files == null ? 0 : files.length;
1857   }
1858
1859   /**
1860    * Starts a thread that waits for the external viewer program process to
1861    * finish, so that we can then close the associated resources. This avoids
1862    * leaving orphaned viewer panels in Jalview if the user closes the external
1863    * viewer.
1864    * 
1865    * @param p
1866    */
1867   protected void startExternalViewerMonitor(Process p)
1868   {
1869     externalViewerMonitor = new Thread(new Runnable()
1870     {
1871
1872       @Override
1873       public void run()
1874       {
1875         try
1876         {
1877           p.waitFor();
1878           JalviewStructureDisplayI display = getViewer();
1879           if (display != null)
1880           {
1881             display.closeViewer(false);
1882           }
1883         } catch (InterruptedException e)
1884         {
1885           // exit thread if Chimera Viewer is closed in Jalview
1886         }
1887       }
1888     });
1889     externalViewerMonitor.start();
1890   }
1891
1892   /**
1893    * If supported by the external structure viewer, sends it commands to notify
1894    * model or selection changes to the specified URL (where Jalview has started
1895    * a listener)
1896    * 
1897    * @param uri
1898    */
1899   protected void startListening(String uri)
1900   {
1901     List<StructureCommandI> commands = getCommandGenerator()
1902             .startNotifications(uri);
1903     if (commands != null)
1904     {
1905       executeCommands(commands, false, null);
1906     }
1907   }
1908
1909   /**
1910    * If supported by the external structure viewer, sends it commands to stop
1911    * notifying model or selection changes
1912    */
1913   protected void stopListening()
1914   {
1915     List<StructureCommandI> commands = getCommandGenerator()
1916             .stopNotifications();
1917     if (commands != null)
1918     {
1919       executeCommands(commands, false, null);
1920     }
1921   }
1922
1923   /**
1924    * If supported by the structure viewer, queries it for all residue attributes
1925    * with the given attribute name, and creates features on corresponding
1926    * residues of the alignment. Returns the number of features added.
1927    * 
1928    * @param attName
1929    * @param alignmentPanel
1930    * @return
1931    */
1932   public int copyStructureAttributesToFeatures(String attName,
1933           AlignmentPanel alignmentPanel)
1934   {
1935     StructureCommandI cmd = getCommandGenerator()
1936             .getResidueAttributes(attName);
1937     if (cmd == null)
1938     {
1939       return 0;
1940     }
1941     List<String> residueAttributes = executeCommand(cmd, true);
1942
1943     int featuresAdded = createFeaturesForAttributes(attName,
1944             residueAttributes);
1945     if (featuresAdded > 0)
1946     {
1947       alignmentPanel.getFeatureRenderer().featuresAdded();
1948     }
1949     return featuresAdded;
1950   }
1951
1952   /**
1953    * Parses {@code residueAttributes} and creates sequence features on any
1954    * mapped alignment residues. Returns the number of features created.
1955    * <p>
1956    * {@code residueAttributes} is the reply from the structure viewer to a
1957    * command to list any residue attributes for the given attribute name. Syntax
1958    * and parsing of this is viewer-specific.
1959    * 
1960    * @param attName
1961    * @param residueAttributes
1962    * @return
1963    */
1964   protected int createFeaturesForAttributes(String attName,
1965           List<String> residueAttributes)
1966   {
1967     return 0;
1968   }
1969 }