JAL-3518 tidy method, remove obsolete comment
[jalview.git] / src / jalview / structures / models / AAStructureBindingModel.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.structures.models;
22
23 import java.awt.Color;
24 import java.io.File;
25 import java.io.IOException;
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.BitSet;
29 import java.util.HashMap;
30 import java.util.LinkedHashMap;
31 import java.util.List;
32 import java.util.Map;
33
34 import javax.swing.SwingUtilities;
35
36 import jalview.api.AlignViewportI;
37 import jalview.api.AlignmentViewPanel;
38 import jalview.api.FeatureRenderer;
39 import jalview.api.SequenceRenderer;
40 import jalview.api.StructureSelectionManagerProvider;
41 import jalview.api.structures.JalviewStructureDisplayI;
42 import jalview.bin.Cache;
43 import jalview.datamodel.AlignmentI;
44 import jalview.datamodel.HiddenColumns;
45 import jalview.datamodel.PDBEntry;
46 import jalview.datamodel.SequenceI;
47 import jalview.gui.StructureViewer.ViewerType;
48 import jalview.io.DataSourceType;
49 import jalview.io.StructureFile;
50 import jalview.renderer.seqfeatures.FeatureColourFinder;
51 import jalview.schemes.ColourSchemeI;
52 import jalview.schemes.ResidueProperties;
53 import jalview.structure.AtomSpec;
54 import jalview.structure.AtomSpecModel;
55 import jalview.structure.StructureCommandI;
56 import jalview.structure.StructureCommandsI;
57 import jalview.structure.StructureListener;
58 import jalview.structure.StructureMapping;
59 import jalview.structure.StructureSelectionManager;
60 import jalview.util.Comparison;
61 import jalview.util.MessageManager;
62
63 /**
64  * 
65  * A base class to hold common function for protein structure model binding.
66  * Initial version created by refactoring JMol and Chimera binding models, but
67  * other structure viewers could in principle be accommodated in future.
68  * 
69  * @author gmcarstairs
70  *
71  */
72 public abstract class AAStructureBindingModel
73         extends SequenceStructureBindingModel
74         implements StructureListener, StructureSelectionManagerProvider
75 {
76   /**
77    * Data bean class to simplify parameterisation in superposeStructures
78    */
79   public static class SuperposeData
80   {
81     public String filename;
82   
83     public String pdbId;
84   
85     public String chain = "";
86   
87     public boolean isRna;
88   
89     /*
90      * The pdb residue number (if any) mapped to columns of the alignment
91      */
92     public int[] pdbResNo; // or use SparseIntArray?
93   
94     public String modelId;
95   
96     /**
97      * Constructor
98      * 
99      * @param width
100      *          width of alignment (number of columns that may potentially
101      *          participate in superposition)
102      * @param model
103      *          structure viewer model number
104      */
105     public SuperposeData(int width, String model)
106     {
107       pdbResNo = new int[width];
108       modelId = model;
109     }
110   }
111
112   private static final int MIN_POS_TO_SUPERPOSE = 4;
113
114   private static final String COLOURING_STRUCTURES = MessageManager
115           .getString("status.colouring_structures");
116
117   /*
118    * the Jalview panel through which the user interacts
119    * with the structure viewer
120    */
121   private JalviewStructureDisplayI viewer;
122
123   /*
124    * helper that generates command syntax
125    */
126   private StructureCommandsI commandGenerator;
127
128   private StructureSelectionManager ssm;
129
130   /*
131    * modelled chains, formatted as "pdbid:chainCode"
132    */
133   private List<String> chainNames;
134
135   /*
136    * lookup of pdb file name by key "pdbid:chainCode"
137    */
138   private Map<String, String> chainFile;
139
140   /*
141    * distinct PDB entries (pdb files) associated
142    * with sequences
143    */
144   private PDBEntry[] pdbEntry;
145
146   /*
147    * sequences mapped to each pdbentry
148    */
149   private SequenceI[][] sequence;
150
151   /*
152    * array of target chains for sequences - tied to pdbentry and sequence[]
153    */
154   private String[][] chains;
155
156   /*
157    * datasource protocol for access to PDBEntrylatest
158    */
159   DataSourceType protocol = null;
160
161   protected boolean colourBySequence = true;
162
163   private boolean nucleotide;
164
165   private boolean finishedInit = false;
166
167   /**
168    * current set of model filenames loaded in the viewer
169    */
170   protected String[] modelFileNames = null;
171
172   public String fileLoadingError;
173
174   /**
175    * Constructor
176    * 
177    * @param ssm
178    * @param seqs
179    */
180   public AAStructureBindingModel(StructureSelectionManager ssm,
181           SequenceI[][] seqs)
182   {
183     this.ssm = ssm;
184     this.sequence = seqs;
185     chainNames = new ArrayList<>();
186     chainFile = new HashMap<>();
187   }
188
189   /**
190    * Constructor
191    * 
192    * @param ssm
193    * @param pdbentry
194    * @param sequenceIs
195    * @param protocol
196    */
197   public AAStructureBindingModel(StructureSelectionManager ssm,
198           PDBEntry[] pdbentry, SequenceI[][] sequenceIs,
199           DataSourceType protocol)
200   {
201     this(ssm, sequenceIs);
202     this.nucleotide = Comparison.isNucleotide(sequenceIs);
203     this.pdbEntry = pdbentry;
204     this.protocol = protocol;
205     resolveChains();
206   }
207
208   private boolean resolveChains()
209   {
210     /**
211      * final count of chain mappings discovered
212      */
213     int chainmaps = 0;
214     // JBPNote: JAL-2693 - this should be a list of chain mappings per
215     // [pdbentry][sequence]
216     String[][] newchains = new String[pdbEntry.length][];
217     int pe = 0;
218     for (PDBEntry pdb : pdbEntry)
219     {
220       SequenceI[] seqsForPdb = sequence[pe];
221       if (seqsForPdb != null)
222       {
223         newchains[pe] = new String[seqsForPdb.length];
224         int se = 0;
225         for (SequenceI asq : seqsForPdb)
226         {
227           String chain = (chains != null && chains[pe] != null)
228                   ? chains[pe][se]
229                   : null;
230           SequenceI sq = (asq.getDatasetSequence() == null) ? asq
231                   : asq.getDatasetSequence();
232           if (sq.getAllPDBEntries() != null)
233           {
234             for (PDBEntry pdbentry : sq.getAllPDBEntries())
235             {
236               if (pdb.getFile() != null && pdbentry.getFile() != null
237                       && pdb.getFile().equals(pdbentry.getFile()))
238               {
239                 String chaincode = pdbentry.getChainCode();
240                 if (chaincode != null && chaincode.length() > 0)
241                 {
242                   chain = chaincode;
243                   chainmaps++;
244                   break;
245                 }
246               }
247             }
248           }
249           newchains[pe][se] = chain;
250           se++;
251         }
252         pe++;
253       }
254     }
255
256     chains = newchains;
257     return chainmaps > 0;
258   }
259   public StructureSelectionManager getSsm()
260   {
261     return ssm;
262   }
263
264   /**
265    * Returns the i'th PDBEntry (or null)
266    * 
267    * @param i
268    * @return
269    */
270   public PDBEntry getPdbEntry(int i)
271   {
272     return (pdbEntry != null && pdbEntry.length > i) ? pdbEntry[i] : null;
273   }
274
275   /**
276    * Answers true if this binding includes the given PDB id, else false
277    * 
278    * @param pdbId
279    * @return
280    */
281   public boolean hasPdbId(String pdbId)
282   {
283     if (pdbEntry != null)
284     {
285       for (PDBEntry pdb : pdbEntry)
286       {
287         if (pdb.getId().equals(pdbId))
288         {
289           return true;
290         }
291       }
292     }
293     return false;
294   }
295
296   /**
297    * Returns the number of modelled PDB file entries.
298    * 
299    * @return
300    */
301   public int getPdbCount()
302   {
303     return pdbEntry == null ? 0 : pdbEntry.length;
304   }
305
306   public SequenceI[][] getSequence()
307   {
308     return sequence;
309   }
310
311   public String[][] getChains()
312   {
313     return chains;
314   }
315
316   public DataSourceType getProtocol()
317   {
318     return protocol;
319   }
320
321   // TODO may remove this if calling methods can be pulled up here
322   protected void setPdbentry(PDBEntry[] pdbentry)
323   {
324     this.pdbEntry = pdbentry;
325   }
326
327   protected void setSequence(SequenceI[][] sequence)
328   {
329     this.sequence = sequence;
330   }
331
332   protected void setChains(String[][] chains)
333   {
334     this.chains = chains;
335   }
336
337   /**
338    * Construct a title string for the viewer window based on the data Jalview
339    * knows about
340    * 
341    * @param viewerName
342    *          TODO
343    * @param verbose
344    * 
345    * @return
346    */
347   public String getViewerTitle(String viewerName, boolean verbose)
348   {
349     if (getSequence() == null || getSequence().length < 1
350             || getPdbCount() < 1 || getSequence()[0].length < 1)
351     {
352       return ("Jalview " + viewerName + " Window");
353     }
354     // TODO: give a more informative title when multiple structures are
355     // displayed.
356     StringBuilder title = new StringBuilder(64);
357     final PDBEntry pdbe = getPdbEntry(0);
358     title.append(viewerName + " view for " + getSequence()[0][0].getName()
359             + ":" + pdbe.getId());
360
361     if (verbose)
362     {
363       String method = (String) pdbe.getProperty("method");
364       if (method != null)
365       {
366         title.append(" Method: ").append(method);
367       }
368       String chain = (String) pdbe.getProperty("chains");
369       if (chain != null)
370       {
371         title.append(" Chain:").append(chain);
372       }
373     }
374     return title.toString();
375   }
376
377   /**
378    * Called by after closeViewer is called, to release any resources and
379    * references so they can be garbage collected. Override if needed.
380    */
381   protected void releaseUIResources()
382   {
383   }
384
385   @Override
386   public void releaseReferences(Object svl)
387   {
388   }
389
390   public boolean isColourBySequence()
391   {
392     return colourBySequence;
393   }
394
395   /**
396    * Called when the binding thinks the UI needs to be refreshed after a
397    * structure viewer state change. This could be because structures were
398    * loaded, or because an error has occurred. Default does nothing, override as
399    * required.
400    */
401   public void refreshGUI()
402   {
403   }
404
405   /**
406    * Instruct the Jalview binding to update the pdbentries vector if necessary
407    * prior to matching the jmol view's contents to the list of structure files
408    * Jalview knows about. By default does nothing, override as required.
409    */
410   public void refreshPdbEntries()
411   {
412   }
413
414   public void setColourBySequence(boolean colourBySequence)
415   {
416     this.colourBySequence = colourBySequence;
417   }
418
419   protected void addSequenceAndChain(int pe, SequenceI[] seq,
420           String[] tchain)
421   {
422     if (pe < 0 || pe >= getPdbCount())
423     {
424       throw new Error(MessageManager.formatMessage(
425               "error.implementation_error_no_pdbentry_from_index",
426               new Object[]
427               { Integer.valueOf(pe).toString() }));
428     }
429     final String nullChain = "TheNullChain";
430     List<SequenceI> s = new ArrayList<>();
431     List<String> c = new ArrayList<>();
432     if (getChains() == null)
433     {
434       setChains(new String[getPdbCount()][]);
435     }
436     if (getSequence()[pe] != null)
437     {
438       for (int i = 0; i < getSequence()[pe].length; i++)
439       {
440         s.add(getSequence()[pe][i]);
441         if (getChains()[pe] != null)
442         {
443           if (i < getChains()[pe].length)
444           {
445             c.add(getChains()[pe][i]);
446           }
447           else
448           {
449             c.add(nullChain);
450           }
451         }
452         else
453         {
454           if (tchain != null && tchain.length > 0)
455           {
456             c.add(nullChain);
457           }
458         }
459       }
460     }
461     for (int i = 0; i < seq.length; i++)
462     {
463       if (!s.contains(seq[i]))
464       {
465         s.add(seq[i]);
466         if (tchain != null && i < tchain.length)
467         {
468           c.add(tchain[i] == null ? nullChain : tchain[i]);
469         }
470       }
471     }
472     SequenceI[] tmp = s.toArray(new SequenceI[s.size()]);
473     getSequence()[pe] = tmp;
474     if (c.size() > 0)
475     {
476       String[] tch = c.toArray(new String[c.size()]);
477       for (int i = 0; i < tch.length; i++)
478       {
479         if (tch[i] == nullChain)
480         {
481           tch[i] = null;
482         }
483       }
484       getChains()[pe] = tch;
485     }
486     else
487     {
488       getChains()[pe] = null;
489     }
490   }
491
492   /**
493    * add structures and any known sequence associations
494    * 
495    * @returns the pdb entries added to the current set.
496    */
497   public synchronized PDBEntry[] addSequenceAndChain(PDBEntry[] pdbe,
498           SequenceI[][] seq, String[][] chns)
499   {
500     List<PDBEntry> v = new ArrayList<>();
501     List<int[]> rtn = new ArrayList<>();
502     for (int i = 0; i < getPdbCount(); i++)
503     {
504       v.add(getPdbEntry(i));
505     }
506     for (int i = 0; i < pdbe.length; i++)
507     {
508       int r = v.indexOf(pdbe[i]);
509       if (r == -1 || r >= getPdbCount())
510       {
511         rtn.add(new int[] { v.size(), i });
512         v.add(pdbe[i]);
513       }
514       else
515       {
516         // just make sure the sequence/chain entries are all up to date
517         addSequenceAndChain(r, seq[i], chns[i]);
518       }
519     }
520     pdbe = v.toArray(new PDBEntry[v.size()]);
521     setPdbentry(pdbe);
522     if (rtn.size() > 0)
523     {
524       // expand the tied sequence[] and string[] arrays
525       SequenceI[][] sqs = new SequenceI[getPdbCount()][];
526       String[][] sch = new String[getPdbCount()][];
527       System.arraycopy(getSequence(), 0, sqs, 0, getSequence().length);
528       System.arraycopy(getChains(), 0, sch, 0, this.getChains().length);
529       setSequence(sqs);
530       setChains(sch);
531       pdbe = new PDBEntry[rtn.size()];
532       for (int r = 0; r < pdbe.length; r++)
533       {
534         int[] stri = (rtn.get(r));
535         // record the pdb file as a new addition
536         pdbe[r] = getPdbEntry(stri[0]);
537         // and add the new sequence/chain entries
538         addSequenceAndChain(stri[0], seq[stri[1]], chns[stri[1]]);
539       }
540     }
541     else
542     {
543       pdbe = null;
544     }
545     return pdbe;
546   }
547
548   /**
549    * Add sequences to the pe'th pdbentry's sequence set.
550    * 
551    * @param pe
552    * @param seq
553    */
554   public void addSequence(int pe, SequenceI[] seq)
555   {
556     addSequenceAndChain(pe, seq, null);
557   }
558
559   /**
560    * add the given sequences to the mapping scope for the given pdb file handle
561    * 
562    * @param pdbFile
563    *          - pdbFile identifier
564    * @param seq
565    *          - set of sequences it can be mapped to
566    */
567   public void addSequenceForStructFile(String pdbFile, SequenceI[] seq)
568   {
569     for (int pe = 0; pe < getPdbCount(); pe++)
570     {
571       if (getPdbEntry(pe).getFile().equals(pdbFile))
572       {
573         addSequence(pe, seq);
574       }
575     }
576   }
577
578   @Override
579   public abstract void highlightAtoms(List<AtomSpec> atoms);
580
581   protected boolean isNucleotide()
582   {
583     return this.nucleotide;
584   }
585
586   /**
587    * Returns a readable description of all mappings for the wrapped pdbfile to
588    * any mapped sequences
589    * 
590    * @param pdbfile
591    * @param seqs
592    * @return
593    */
594   public String printMappings()
595   {
596     if (pdbEntry == null)
597     {
598       return "";
599     }
600     StringBuilder sb = new StringBuilder(128);
601     for (int pdbe = 0; pdbe < getPdbCount(); pdbe++)
602     {
603       String pdbfile = getPdbEntry(pdbe).getFile();
604       List<SequenceI> seqs = Arrays.asList(getSequence()[pdbe]);
605       sb.append(getSsm().printMappings(pdbfile, seqs));
606     }
607     return sb.toString();
608   }
609
610   /**
611    * Returns the mapped structure position for a given aligned column of a given
612    * sequence, or -1 if the column is gapped, beyond the end of the sequence, or
613    * not mapped to structure.
614    * 
615    * @param seq
616    * @param alignedPos
617    * @param mapping
618    * @return
619    */
620   protected int getMappedPosition(SequenceI seq, int alignedPos,
621           StructureMapping mapping)
622   {
623     if (alignedPos >= seq.getLength())
624     {
625       return -1;
626     }
627
628     if (Comparison.isGap(seq.getCharAt(alignedPos)))
629     {
630       return -1;
631     }
632     int seqPos = seq.findPosition(alignedPos);
633     int pos = mapping.getPDBResNum(seqPos);
634     return pos;
635   }
636
637   /**
638    * Helper method to identify residues that can participate in a structure
639    * superposition command. For each structure, identify a sequence in the
640    * alignment which is mapped to the structure. Identify non-gapped columns in
641    * the sequence which have a mapping to a residue in the structure. Returns
642    * the index of the first structure that has a mapping to the alignment.
643    * 
644    * @param alignment
645    *          the sequence alignment which is the basis of structure
646    *          superposition
647    * @param matched
648    *          a BitSet, where bit j is set to indicate that every structure has
649    *          a mapped residue present in column j (so the column can
650    *          participate in structure alignment)
651    * @param structures
652    *          an array of data beans corresponding to pdb file index
653    * @return
654    */
655   protected int findSuperposableResidues(AlignmentI alignment,
656           BitSet matched, AAStructureBindingModel.SuperposeData[] structures)
657   {
658     int refStructure = -1;
659     String[] files = getStructureFiles();
660     if (files == null)
661     {
662       return -1;
663     }
664     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
665     {
666       StructureMapping[] mappings = getSsm().getMapping(files[pdbfnum]);
667       int lastPos = -1;
668
669       /*
670        * Find the first mapped sequence (if any) for this PDB entry which is in
671        * the alignment
672        */
673       final int seqCountForPdbFile = getSequence()[pdbfnum].length;
674       for (int s = 0; s < seqCountForPdbFile; s++)
675       {
676         for (StructureMapping mapping : mappings)
677         {
678           final SequenceI theSequence = getSequence()[pdbfnum][s];
679           if (mapping.getSequence() == theSequence
680                   && alignment.findIndex(theSequence) > -1)
681           {
682             if (refStructure < 0)
683             {
684               refStructure = pdbfnum;
685             }
686             for (int r = 0; r < alignment.getWidth(); r++)
687             {
688               if (!matched.get(r))
689               {
690                 continue;
691               }
692               int pos = getMappedPosition(theSequence, r, mapping);
693               if (pos < 1 || pos == lastPos)
694               {
695                 matched.clear(r);
696                 continue;
697               }
698               lastPos = pos;
699               structures[pdbfnum].pdbResNo[r] = pos;
700             }
701             String chain = mapping.getChain();
702             if (chain != null && chain.trim().length() > 0)
703             {
704               structures[pdbfnum].chain = chain;
705             }
706             structures[pdbfnum].pdbId = mapping.getPdbId();
707             structures[pdbfnum].isRna = theSequence.getRNA() != null;
708
709             /*
710              * move on to next pdb file (ignore sequences for other chains
711              * for the same structure)
712              */
713             s = seqCountForPdbFile;
714             break; // fixme break out of two loops here!
715           }
716         }
717       }
718     }
719     return refStructure;
720   }
721
722   /**
723    * Returns true if the structure viewer has loaded all of the files of
724    * interest (identified by the file mapping having been set up), or false if
725    * any are still not loaded after a timeout interval.
726    * 
727    * @param files
728    */
729   protected boolean waitForFileLoad(String[] files)
730   {
731     /*
732      * give up after 10 secs plus 1 sec per file
733      */
734     long starttime = System.currentTimeMillis();
735     long endTime = 10000 + 1000 * files.length + starttime;
736     String notLoaded = null;
737
738     boolean waiting = true;
739     while (waiting && System.currentTimeMillis() < endTime)
740     {
741       waiting = false;
742       for (String file : files)
743       {
744         notLoaded = file;
745         if (file == null)
746         {
747           continue;
748         }
749         try
750         {
751           StructureMapping[] sm = getSsm().getMapping(file);
752           if (sm == null || sm.length == 0)
753           {
754             waiting = true;
755           }
756         } catch (Throwable x)
757         {
758           waiting = true;
759         }
760       }
761     }
762
763     if (waiting)
764     {
765       System.err.println(
766               "Timed out waiting for structure viewer to load file "
767                       + notLoaded);
768       return false;
769     }
770     return true;
771   }
772
773   @Override
774   public boolean isListeningFor(SequenceI seq)
775   {
776     if (sequence != null)
777     {
778       for (SequenceI[] seqs : sequence)
779       {
780         if (seqs != null)
781         {
782           for (SequenceI s : seqs)
783           {
784             if (s == seq || (s.getDatasetSequence() != null
785                     && s.getDatasetSequence() == seq.getDatasetSequence()))
786             {
787               return true;
788             }
789           }
790         }
791       }
792     }
793     return false;
794   }
795
796   public boolean isFinishedInit()
797   {
798     return finishedInit;
799   }
800
801   public void setFinishedInit(boolean fi)
802   {
803     this.finishedInit = fi;
804   }
805
806   /**
807    * Returns a list of chains mapped in this viewer, formatted as
808    * "pdbid:chainCode"
809    * 
810    * @return
811    */
812   public List<String> getChainNames()
813   {
814     return chainNames;
815   }
816
817   /**
818    * Returns the Jalview panel hosting the structure viewer (if any)
819    * 
820    * @return
821    */
822   public JalviewStructureDisplayI getViewer()
823   {
824     return viewer;
825   }
826
827   public void setViewer(JalviewStructureDisplayI v)
828   {
829     viewer = v;
830   }
831
832   /**
833    * Constructs and sends a command to align structures against a reference
834    * structure, based on one or more sequence alignments. May optionally return
835    * an error or warning message for the alignment command(s).
836    * 
837    * @param alignWith
838    *          an array of one or more alignment views to process
839    * @return
840    */
841   public String superposeStructures(List<AlignmentViewPanel> alignWith)
842   {
843     String error = "";
844     String[] files = getStructureFiles();
845
846     if (!waitForFileLoad(files))
847     {
848       return null;
849     }
850     refreshPdbEntries();
851
852     for (AlignmentViewPanel view : alignWith)
853     {
854       AlignmentI alignment = view.getAlignment();
855       HiddenColumns hiddenCols = alignment.getHiddenColumns();
856
857       /*
858        * 'matched' bit i will be set for visible alignment columns i where
859        * all sequences have a residue with a mapping to their PDB structure
860        */
861       BitSet matched = new BitSet();
862       final int width = alignment.getWidth();
863       for (int m = 0; m < width; m++)
864       {
865         if (hiddenCols == null || hiddenCols.isVisible(m))
866         {
867           matched.set(m);
868         }
869       }
870
871       AAStructureBindingModel.SuperposeData[] structures = new AAStructureBindingModel.SuperposeData[files.length];
872       for (int f = 0; f < files.length; f++)
873       {
874         structures[f] = new AAStructureBindingModel.SuperposeData(width,
875                 getModelIdForFile(files[f]));
876       }
877
878       /*
879        * Calculate the superposable alignment columns ('matched'), and the
880        * corresponding structure residue positions (structures.pdbResNo)
881        */
882       int refStructure = findSuperposableResidues(alignment,
883               matched, structures);
884
885       /*
886        * require at least 4 positions to be able to execute superposition
887        */
888       int nmatched = matched.cardinality();
889       if (nmatched < MIN_POS_TO_SUPERPOSE)
890       {
891         String msg = MessageManager.formatMessage("label.insufficient_residues",
892                 nmatched);
893         error += view.getViewName() + ": " + msg + "; ";
894         continue;
895       }
896
897       /*
898        * get a model of the superposable residues in the reference structure 
899        */
900       AtomSpecModel refAtoms = getAtomSpec(structures[refStructure],
901               matched);
902
903       /*
904        * Show all as backbone before doing superposition(s)
905        * (residues used for matching will be shown as ribbon)
906        */
907       // todo better way to ensure synchronous than setting getReply true!!
908       executeCommands(commandGenerator.showBackbone(), true, null);
909
910       /*
911        * superpose each (other) structure to the reference in turn
912        */
913       for (int i = 0; i < structures.length; i++)
914       {
915         if (i != refStructure)
916         {
917           AtomSpecModel atomSpec = getAtomSpec(structures[i], matched);
918           List<StructureCommandI> commands = commandGenerator
919                   .superposeStructures(refAtoms, atomSpec);
920           List<String> replies = executeCommands(commands, true, null);
921           for (String reply : replies)
922           {
923             // return this error (Chimera only) to the user
924             if (reply.toLowerCase().contains("unequal numbers of atoms"))
925             {
926               error += "; " + reply;
927             }
928           }
929         }
930       }
931     }
932
933     return error;
934   }
935
936   private AtomSpecModel getAtomSpec(AAStructureBindingModel.SuperposeData superposeData,
937           BitSet matched)
938   {
939     AtomSpecModel model = new AtomSpecModel();
940     int nextColumnMatch = matched.nextSetBit(0);
941     while (nextColumnMatch != -1)
942     {
943       int pdbResNum = superposeData.pdbResNo[nextColumnMatch];
944       model.addRange(superposeData.modelId, pdbResNum, pdbResNum,
945               superposeData.chain);
946       nextColumnMatch = matched.nextSetBit(nextColumnMatch + 1);
947     }
948
949     return model;
950   }
951
952   /**
953    * returns the current sequenceRenderer that should be used to colour the
954    * structures
955    * 
956    * @param alignment
957    * 
958    * @return
959    */
960   public abstract SequenceRenderer getSequenceRenderer(
961           AlignmentViewPanel alignment);
962
963   /**
964    * Sends a command to the structure viewer to colour each chain with a
965    * distinct colour (to the extent supported by the viewer)
966    */
967   public void colourByChain()
968   {
969     colourBySequence = false;
970
971     // TODO: JAL-628 colour chains distinctly across all visible models
972
973     executeCommand(commandGenerator.colourByChain(), false,
974             COLOURING_STRUCTURES);
975   }
976
977   /**
978    * Sends a command to the structure viewer to colour each chain with a
979    * distinct colour (to the extent supported by the viewer)
980    */
981   public void colourByCharge()
982   {
983     colourBySequence = false;
984
985     executeCommands(commandGenerator.colourByCharge(), false,
986             COLOURING_STRUCTURES);
987   }
988
989   /**
990    * Sends a command to the structure to apply a colour scheme (defined in
991    * Jalview but not necessarily applied to the alignment), which defines a
992    * colour per residue letter. More complex schemes (e.g. that depend on
993    * consensus) cannot be used here and are ignored.
994    * 
995    * @param cs
996    */
997   public void colourByJalviewColourScheme(ColourSchemeI cs)
998   {
999     colourBySequence = false;
1000
1001     if (cs == null || !cs.isSimple())
1002     {
1003       return;
1004     }
1005     
1006     /*
1007      * build a map of {Residue3LetterCode, Color}
1008      */
1009     Map<String, Color> colours = new HashMap<>();
1010     List<String> residues = ResidueProperties.getResidues(isNucleotide(),
1011             false);
1012     for (String resName : residues)
1013     {
1014       char res = resName.length() == 3
1015               ? ResidueProperties.getSingleCharacterCode(resName)
1016               : resName.charAt(0);
1017       Color colour = cs.findColour(res, 0, null, null, 0f);
1018       colours.put(resName, colour);
1019     }
1020
1021     /*
1022      * pass to the command constructor, and send the command
1023      */
1024     List<StructureCommandI> cmd = commandGenerator
1025             .colourByResidues(colours);
1026     executeCommands(cmd, false, COLOURING_STRUCTURES);
1027   }
1028
1029   public void setBackgroundColour(Color col)
1030   {
1031     StructureCommandI cmd = commandGenerator.setBackgroundColour(col);
1032     executeCommand(cmd, false, null);
1033   }
1034
1035   /**
1036    * Sends one command to the structure viewer. If {@code getReply} is true, the
1037    * command is sent synchronously, otherwise in a deferred thread.
1038    * <p>
1039    * If a progress message is supplied, this is displayed before command
1040    * execution, and removed afterwards.
1041    * 
1042    * @param cmd
1043    * @param getReply
1044    * @param msg
1045    * @return
1046    */
1047   private List<String> executeCommand(StructureCommandI cmd,
1048           boolean getReply, String msg)
1049   {
1050     if (getReply)
1051     {
1052       /*
1053        * synchronous (same thread) execution so reply can be returned
1054        */
1055       final JalviewStructureDisplayI theViewer = getViewer();
1056       final long handle = msg == null ? 0 : theViewer.startProgressBar(msg);
1057       try
1058       {
1059         return executeCommand(cmd, getReply);
1060       } finally
1061       {
1062         if (msg != null)
1063         {
1064           theViewer.stopProgressBar(null, handle);
1065         }
1066       }
1067     }
1068     else
1069     {
1070       /*
1071        * asynchronous (new thread) execution if no reply needed
1072        */
1073       final JalviewStructureDisplayI theViewer = getViewer();
1074       final long handle = msg == null ? 0 : theViewer.startProgressBar(msg);
1075       
1076       SwingUtilities.invokeLater(new Runnable()
1077       {
1078         @Override
1079         public void run()
1080         {
1081           try
1082           {
1083             executeCommand(cmd, false);
1084           } finally
1085           {
1086             if (msg != null)
1087             {
1088               theViewer.stopProgressBar(null, handle);
1089             }
1090           }
1091         }
1092       });
1093       return null;
1094     }
1095   }
1096
1097   /**
1098    * Execute one structure viewer command. If {@code getReply} is true, may
1099    * optionally return one or more reply messages, else returns null.
1100    * 
1101    * @param cmd
1102    * @param getReply
1103    */
1104   protected abstract List<String> executeCommand(StructureCommandI cmd,
1105           boolean getReply);
1106
1107   /**
1108    * A helper method that converts list of commands to a vararg array
1109    * 
1110    * @param commands
1111    * @param getReply
1112    * @param msg
1113    */
1114   private List<String> executeCommands(List<StructureCommandI> commands,
1115           boolean getReply, String msg)
1116   {
1117     return executeCommands(getReply, msg,
1118             commands.toArray(new StructureCommandI[commands.size()]));
1119   }
1120
1121   /**
1122    * Executes one or more structure viewer commands. If a progress message is
1123    * provided, it is shown first, and removed after all commands have been run.
1124    * 
1125    * @param getReply
1126    * @param msg
1127    * @param commands
1128    * @return
1129    */
1130   protected List<String> executeCommands(boolean getReply, String msg,
1131           StructureCommandI[] commands)
1132   {
1133     // todo: tidy this up
1134
1135     /*
1136      * show progress message if specified
1137      */
1138     final JalviewStructureDisplayI theViewer = getViewer();
1139     final long handle = msg == null ? 0 : theViewer.startProgressBar(msg);
1140
1141     List<String> response = getReply ? new ArrayList<>() : null;
1142     try
1143     {
1144       for (StructureCommandI cmd : commands)
1145       {
1146         List<String> replies = executeCommand(cmd, getReply, null);
1147         if (getReply && replies != null)
1148         {
1149           response.addAll(replies);
1150         }
1151       }
1152       return response;
1153     } finally
1154     {
1155       if (msg != null)
1156       {
1157         theViewer.stopProgressBar(null, handle);
1158       }
1159     }
1160   }
1161
1162   /**
1163    * colour any structures associated with sequences in the given alignment
1164    * using the getFeatureRenderer() and getSequenceRenderer() renderers but only
1165    * if colourBySequence is enabled.
1166    */
1167   public void colourBySequence(AlignmentViewPanel alignmentv)
1168   {
1169     if (!colourBySequence || !isLoadingFinished())
1170     {
1171       return;
1172     }
1173     if (getSsm() == null)
1174     {
1175       return;
1176     }
1177     String[] files = getStructureFiles();
1178
1179     SequenceRenderer sr = getSequenceRenderer(alignmentv);
1180     Map<Object, AtomSpecModel> colourMap = buildColoursMap(ssm, files,
1181             sequence, sr, alignmentv);
1182
1183     List<StructureCommandI> colourBySequenceCommands = commandGenerator
1184             .colourBySequence(colourMap);
1185     executeCommands(colourBySequenceCommands, false, null);
1186   }
1187
1188   /**
1189    * Centre the display in the structure viewer
1190    */
1191   public void focusView()
1192   {
1193     executeCommand(commandGenerator.focusView(), false, null);
1194   }
1195
1196   /**
1197    * Generates and executes a command to show only specified chains in the
1198    * structure viewer. The list of chains to show should contain entries
1199    * formatted as "pdbid:chaincode".
1200    * 
1201    * @param toShow
1202    */
1203   public void showChains(List<String> toShow)
1204   {
1205     // todo or reformat toShow list entries as modelNo:pdbId:chainCode ?
1206
1207     /*
1208      * Reformat the pdbid:chainCode values as modelNo:chainCode
1209      * since this is what is needed to construct the viewer command
1210      * todo: find a less messy way to do this
1211      */
1212     List<String> showThese = new ArrayList<>();
1213     for (String chainId : toShow)
1214     {
1215       String[] tokens = chainId.split("\\:");
1216       if (tokens.length == 2)
1217       {
1218         String pdbFile = getFileForChain(chainId);
1219         String model = getModelIdForFile(pdbFile);
1220         showThese.add(model + ":" + tokens[1]);
1221       }
1222     }
1223     executeCommands(commandGenerator.showChains(showThese), false, null);
1224   }
1225
1226   /**
1227    * Answers the structure viewer's model id given a PDB file name. Returns an
1228    * empty string if model id is not found.
1229    * 
1230    * @param chainId
1231    * @return
1232    */
1233   protected abstract String getModelIdForFile(String chainId);
1234
1235   public boolean hasFileLoadingError()
1236   {
1237     return fileLoadingError != null && fileLoadingError.length() > 0;
1238   }
1239
1240   /**
1241    * Returns the FeatureRenderer for the given alignment view, or null if
1242    * feature display is turned off in the view.
1243    * 
1244    * @param avp
1245    * @return
1246    */
1247   public FeatureRenderer getFeatureRenderer(AlignmentViewPanel avp)
1248   {
1249     AlignmentViewPanel ap = (avp == null) ? getViewer().getAlignmentPanel()
1250             : avp;
1251     return ap.getAlignViewport().isShowSequenceFeatures()
1252             ? ap.getFeatureRenderer()
1253             : null;
1254   }
1255
1256   protected void setStructureCommands(StructureCommandsI cmd)
1257   {
1258     commandGenerator = cmd;
1259   }
1260
1261   /**
1262    * Records association of one chain id (formatted as "pdbid:chainCode") with
1263    * the corresponding PDB file name
1264    * 
1265    * @param chainId
1266    * @param fileName
1267    */
1268   public void addChainFile(String chainId, String fileName)
1269   {
1270     chainFile.put(chainId, fileName);
1271   }
1272
1273   /**
1274    * Returns the PDB filename for the given chain id (formatted as
1275    * "pdbid:chainCode"), or null if not found
1276    * 
1277    * @param chainId
1278    * @return
1279    */
1280   protected String getFileForChain(String chainId)
1281   {
1282     return chainFile.get(chainId);
1283   }
1284
1285   @Override
1286   public void updateColours(Object source)
1287   {
1288     AlignmentViewPanel ap = (AlignmentViewPanel) source;
1289     // ignore events from panels not used to colour this view
1290     if (!getViewer().isUsedForColourBy(ap))
1291     {
1292       return;
1293     }
1294     if (!isLoadingFromArchive())
1295     {
1296       colourBySequence(ap);
1297     }
1298   }
1299
1300   public StructureCommandsI getCommandGenerator()
1301   {
1302     return commandGenerator;
1303   }
1304
1305   protected abstract ViewerType getViewerType();
1306
1307   /**
1308    * Send a structure viewer command asynchronously in a new thread. If the
1309    * progress message is not null, display this message while the command is
1310    * executing.
1311    * 
1312    * @param command
1313    * @param progressMsg
1314    */
1315   protected void sendAsynchronousCommand(StructureCommandI command,
1316           String progressMsg)
1317   {
1318     final JalviewStructureDisplayI theViewer = getViewer();
1319     final long handle = progressMsg == null ? 0
1320             : theViewer.startProgressBar(progressMsg);
1321     SwingUtilities.invokeLater(new Runnable()
1322     {
1323       @Override
1324       public void run()
1325       {
1326         try
1327         {
1328           executeCommand(command, false, null);
1329         } finally
1330         {
1331           if (progressMsg != null)
1332           {
1333             theViewer.stopProgressBar(null, handle);
1334           }
1335         }
1336       }
1337     });
1338
1339   }
1340
1341   /**
1342    * Builds a data structure which records mapped structure residues for each
1343    * colour. From this we can easily generate the viewer commands for colour by
1344    * sequence. Constructs and returns a map of {@code Color} to
1345    * {@code AtomSpecModel}, where the atomspec model holds
1346    * 
1347    * <pre>
1348    *   Model ids
1349    *     Chains
1350    *       Residue positions
1351    * </pre>
1352    * 
1353    * Ordering is by order of addition (for colours), natural ordering (for
1354    * models and chains)
1355    * 
1356    * @param ssm
1357    * @param files
1358    * @param sequence
1359    * @param sr
1360    * @param viewPanel
1361    * @return
1362    */
1363   protected Map<Object, AtomSpecModel> buildColoursMap(
1364           StructureSelectionManager ssm, String[] files,
1365           SequenceI[][] sequence, SequenceRenderer sr, AlignmentViewPanel viewPanel)
1366   {
1367     FeatureRenderer fr = viewPanel.getFeatureRenderer();
1368     FeatureColourFinder finder = new FeatureColourFinder(fr);
1369     AlignViewportI viewport = viewPanel.getAlignViewport();
1370     HiddenColumns cs = viewport.getAlignment().getHiddenColumns();
1371     AlignmentI al = viewport.getAlignment();
1372     Map<Object, AtomSpecModel> colourMap = new LinkedHashMap<>();
1373     Color lastColour = null;
1374   
1375     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
1376     {
1377       final String modelId = getModelIdForFile(files[pdbfnum]);
1378       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
1379   
1380       if (mapping == null || mapping.length < 1)
1381       {
1382         continue;
1383       }
1384   
1385       int startPos = -1, lastPos = -1;
1386       String lastChain = "";
1387       for (int s = 0; s < sequence[pdbfnum].length; s++)
1388       {
1389         for (int sp, m = 0; m < mapping.length; m++)
1390         {
1391           final SequenceI seq = sequence[pdbfnum][s];
1392           if (mapping[m].getSequence() == seq
1393                   && (sp = al.findIndex(seq)) > -1)
1394           {
1395             SequenceI asp = al.getSequenceAt(sp);
1396             for (int r = 0; r < asp.getLength(); r++)
1397             {
1398               // no mapping to gaps in sequence
1399               if (Comparison.isGap(asp.getCharAt(r)))
1400               {
1401                 continue;
1402               }
1403               int pos = mapping[m].getPDBResNum(asp.findPosition(r));
1404   
1405               if (pos < 1 || pos == lastPos)
1406               {
1407                 continue;
1408               }
1409   
1410               Color colour = sr.getResidueColour(seq, r, finder);
1411   
1412               /*
1413                * darker colour for hidden regions
1414                */
1415               if (!cs.isVisible(r))
1416               {
1417                 colour = Color.GRAY;
1418               }
1419   
1420               final String chain = mapping[m].getChain();
1421   
1422               /*
1423                * Just keep incrementing the end position for this colour range
1424                * _unless_ colour, PDB model or chain has changed, or there is a
1425                * gap in the mapped residue sequence
1426                */
1427               final boolean newColour = !colour.equals(lastColour);
1428               final boolean nonContig = lastPos + 1 != pos;
1429               final boolean newChain = !chain.equals(lastChain);
1430               if (newColour || nonContig || newChain)
1431               {
1432                 if (startPos != -1)
1433                 {
1434                   addAtomSpecRange(colourMap, lastColour, modelId,
1435                           startPos, lastPos, lastChain);
1436                 }
1437                 startPos = pos;
1438               }
1439               lastColour = colour;
1440               lastPos = pos;
1441               lastChain = chain;
1442             }
1443             // final colour range
1444             if (lastColour != null)
1445             {
1446               addAtomSpecRange(colourMap, lastColour, modelId, startPos,
1447                       lastPos, lastChain);
1448             }
1449             // break;
1450           }
1451         }
1452       }
1453     }
1454     return colourMap;
1455   }
1456
1457   /**
1458    * todo better refactoring (map lookup or similar to get viewer structure id)
1459    * 
1460    * @param pdbfnum
1461    * @param file
1462    * @return
1463    */
1464   protected String getModelId(int pdbfnum, String file)
1465   {
1466     return String.valueOf(pdbfnum);
1467   }
1468
1469   /**
1470    * Saves chains, formatted as "pdbId:chainCode", and lookups from this to the
1471    * full PDB file path
1472    * 
1473    * @param pdb
1474    * @param file
1475    */
1476   public void stashFoundChains(StructureFile pdb, String file)
1477   {
1478     for (int i = 0; i < pdb.getChains().size(); i++)
1479     {
1480       String chid = pdb.getId() + ":" + pdb.getChains().elementAt(i).id;
1481       addChainFile(chid, file);
1482       getChainNames().add(chid);
1483     }
1484   }
1485
1486   /**
1487    * Helper method to add one contiguous range to the AtomSpec model for the given
1488    * value (creating the model if necessary). As used by Jalview, {@code value} is
1489    * <ul>
1490    * <li>a colour, when building a 'colour structure by sequence' command</li>
1491    * <li>a feature value, when building a 'set Chimera attributes from features'
1492    * command</li>
1493    * </ul>
1494    * 
1495    * @param map
1496    * @param value
1497    * @param model
1498    * @param startPos
1499    * @param endPos
1500    * @param chain
1501    */
1502   public static final void addAtomSpecRange(Map<Object, AtomSpecModel> map,
1503           Object value,
1504           String model, int startPos, int endPos, String chain)
1505   {
1506     /*
1507      * Get/initialize map of data for the colour
1508      */
1509     AtomSpecModel atomSpec = map.get(value);
1510     if (atomSpec == null)
1511     {
1512       atomSpec = new AtomSpecModel();
1513       map.put(value, atomSpec);
1514     }
1515   
1516     atomSpec.addRange(model, startPos, endPos, chain);
1517   }
1518
1519   /**
1520    * Returns the file extension (including '.' separator) to use for a saved
1521    * viewer session file. Default is to return null (not supported), override as
1522    * required.
1523    * 
1524    * @return
1525    */
1526   public String getSessionFileExtension()
1527   {
1528     return null;
1529   }
1530
1531   /**
1532    * If supported, saves the state of the structure viewer to a temporary file
1533    * and returns the file. Returns null and logs an error on any failure.
1534    * 
1535    * @return
1536    */
1537   public File saveSession()
1538   {
1539     String prefix = getViewerType().toString();
1540     String suffix = getSessionFileExtension();
1541     File f = null;
1542     try
1543     {
1544       f = File.createTempFile(prefix, suffix);
1545       saveSession(f);
1546     } catch (IOException e)
1547     {
1548       Cache.log.error(String.format("Error saving %s session: %s",
1549               prefix, e.toString()));
1550     }
1551
1552     return f;
1553   }
1554
1555   /**
1556    * Saves the structure viewer session to the given file
1557    * 
1558    * @param f
1559    */
1560   protected void saveSession(File f)
1561   {
1562     StructureCommandI cmd = commandGenerator
1563             .saveSession(f.getPath());
1564     if (cmd != null)
1565     {
1566       executeCommand(cmd, false);
1567     }
1568   }
1569 }