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