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