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