JAL-2694 StructureSelectionManager.getMapping takes a list of Chain IDs per sequence...
[jalview.git] / src / jalview / structure / StructureSelectionManager.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.structure;
22
23 import jalview.analysis.AlignSeq;
24 import jalview.api.StructureSelectionManagerProvider;
25 import jalview.commands.CommandI;
26 import jalview.commands.EditCommand;
27 import jalview.commands.OrderCommand;
28 import jalview.datamodel.AlignedCodonFrame;
29 import jalview.datamodel.AlignmentAnnotation;
30 import jalview.datamodel.AlignmentI;
31 import jalview.datamodel.Annotation;
32 import jalview.datamodel.HiddenColumns;
33 import jalview.datamodel.PDBEntry;
34 import jalview.datamodel.SearchResults;
35 import jalview.datamodel.SearchResultsI;
36 import jalview.datamodel.SequenceI;
37 import jalview.ext.jmol.JmolParser;
38 import jalview.gui.IProgressIndicator;
39 import jalview.io.AppletFormatAdapter;
40 import jalview.io.DataSourceType;
41 import jalview.io.StructureFile;
42 import jalview.util.MappingUtils;
43 import jalview.util.MessageManager;
44 import jalview.ws.sifts.SiftsClient;
45 import jalview.ws.sifts.SiftsException;
46 import jalview.ws.sifts.SiftsSettings;
47
48 import java.io.PrintStream;
49 import java.util.ArrayList;
50 import java.util.Arrays;
51 import java.util.Collections;
52 import java.util.Enumeration;
53 import java.util.HashMap;
54 import java.util.IdentityHashMap;
55 import java.util.List;
56 import java.util.Map;
57 import java.util.Vector;
58
59 import MCview.Atom;
60 import MCview.PDBChain;
61 import MCview.PDBfile;
62
63 public class StructureSelectionManager
64 {
65   public final static String NEWLINE = System.lineSeparator();
66
67   static IdentityHashMap<StructureSelectionManagerProvider, StructureSelectionManager> instances;
68
69   private List<StructureMapping> mappings = new ArrayList<>();
70
71   private boolean processSecondaryStructure = false;
72
73   private boolean secStructServices = false;
74
75   private boolean addTempFacAnnot = false;
76
77   private IProgressIndicator progressIndicator;
78
79   private SiftsClient siftsClient = null;
80
81   private long progressSessionId;
82
83   /*
84    * Set of any registered mappings between (dataset) sequences.
85    */
86   private List<AlignedCodonFrame> seqmappings = new ArrayList<>();
87
88   private List<CommandListener> commandListeners = new ArrayList<>();
89
90   private List<SelectionListener> sel_listeners = new ArrayList<>();
91
92   /**
93    * @return true if will try to use external services for processing secondary
94    *         structure
95    */
96   public boolean isSecStructServices()
97   {
98     return secStructServices;
99   }
100
101   /**
102    * control use of external services for processing secondary structure
103    * 
104    * @param secStructServices
105    */
106   public void setSecStructServices(boolean secStructServices)
107   {
108     this.secStructServices = secStructServices;
109   }
110
111   /**
112    * flag controlling addition of any kind of structural annotation
113    * 
114    * @return true if temperature factor annotation will be added
115    */
116   public boolean isAddTempFacAnnot()
117   {
118     return addTempFacAnnot;
119   }
120
121   /**
122    * set flag controlling addition of structural annotation
123    * 
124    * @param addTempFacAnnot
125    */
126   public void setAddTempFacAnnot(boolean addTempFacAnnot)
127   {
128     this.addTempFacAnnot = addTempFacAnnot;
129   }
130
131   /**
132    * 
133    * @return if true, the structure manager will attempt to add secondary
134    *         structure lines for unannotated sequences
135    */
136
137   public boolean isProcessSecondaryStructure()
138   {
139     return processSecondaryStructure;
140   }
141
142   /**
143    * Control whether structure manager will try to annotate mapped sequences
144    * with secondary structure from PDB data.
145    * 
146    * @param enable
147    */
148   public void setProcessSecondaryStructure(boolean enable)
149   {
150     processSecondaryStructure = enable;
151   }
152
153   /**
154    * debug function - write all mappings to stdout
155    */
156   public void reportMapping()
157   {
158     if (mappings.isEmpty())
159     {
160       System.err.println("reportMapping: No PDB/Sequence mappings.");
161     }
162     else
163     {
164       System.err.println(
165               "reportMapping: There are " + mappings.size() + " mappings.");
166       int i = 0;
167       for (StructureMapping sm : mappings)
168       {
169         System.err.println("mapping " + i++ + " : " + sm.pdbfile);
170       }
171     }
172   }
173
174   /**
175    * map between the PDB IDs (or structure identifiers) used by Jalview and the
176    * absolute filenames for PDB data that corresponds to it
177    */
178   Map<String, String> pdbIdFileName = new HashMap<>();
179
180   Map<String, String> pdbFileNameId = new HashMap<>();
181
182   public void registerPDBFile(String idForFile, String absoluteFile)
183   {
184     pdbIdFileName.put(idForFile, absoluteFile);
185     pdbFileNameId.put(absoluteFile, idForFile);
186   }
187
188   public String findIdForPDBFile(String idOrFile)
189   {
190     String id = pdbFileNameId.get(idOrFile);
191     return id;
192   }
193
194   public String findFileForPDBId(String idOrFile)
195   {
196     String id = pdbIdFileName.get(idOrFile);
197     return id;
198   }
199
200   public boolean isPDBFileRegistered(String idOrFile)
201   {
202     return pdbFileNameId.containsKey(idOrFile)
203             || pdbIdFileName.containsKey(idOrFile);
204   }
205
206   private static StructureSelectionManager nullProvider = null;
207
208   public static StructureSelectionManager getStructureSelectionManager(
209           StructureSelectionManagerProvider context)
210   {
211     if (context == null)
212     {
213       if (nullProvider == null)
214       {
215         if (instances != null)
216         {
217           throw new Error(MessageManager.getString(
218                   "error.implementation_error_structure_selection_manager_null"),
219                   new NullPointerException(MessageManager
220                           .getString("exception.ssm_context_is_null")));
221         }
222         else
223         {
224           nullProvider = new StructureSelectionManager();
225         }
226         return nullProvider;
227       }
228     }
229     if (instances == null)
230     {
231       instances = new java.util.IdentityHashMap<>();
232     }
233     StructureSelectionManager instance = instances.get(context);
234     if (instance == null)
235     {
236       if (nullProvider != null)
237       {
238         instance = nullProvider;
239       }
240       else
241       {
242         instance = new StructureSelectionManager();
243       }
244       instances.put(context, instance);
245     }
246     return instance;
247   }
248
249   /**
250    * flag controlling whether SeqMappings are relayed from received sequence
251    * mouse over events to other sequences
252    */
253   boolean relaySeqMappings = true;
254
255   /**
256    * Enable or disable relay of seqMapping events to other sequences. You might
257    * want to do this if there are many sequence mappings and the host computer
258    * is slow
259    * 
260    * @param relay
261    */
262   public void setRelaySeqMappings(boolean relay)
263   {
264     relaySeqMappings = relay;
265   }
266
267   /**
268    * get the state of the relay seqMappings flag.
269    * 
270    * @return true if sequence mouse overs are being relayed to other mapped
271    *         sequences
272    */
273   public boolean isRelaySeqMappingsEnabled()
274   {
275     return relaySeqMappings;
276   }
277
278   Vector listeners = new Vector();
279
280   /**
281    * register a listener for alignment sequence mouseover events
282    * 
283    * @param svl
284    */
285   public void addStructureViewerListener(Object svl)
286   {
287     if (!listeners.contains(svl))
288     {
289       listeners.addElement(svl);
290     }
291   }
292
293   /**
294    * Returns the file name for a mapped PDB id (or null if not mapped).
295    * 
296    * @param pdbid
297    * @return
298    */
299   public String alreadyMappedToFile(String pdbid)
300   {
301     for (StructureMapping sm : mappings)
302     {
303       if (sm.getPdbId().equals(pdbid))
304       {
305         return sm.pdbfile;
306       }
307     }
308     return null;
309   }
310
311   /**
312    * Import structure data and register a structure mapping for broadcasting
313    * colouring, mouseovers and selection events (convenience wrapper).
314    * 
315    * @param sequence
316    *          - one or more sequences to be mapped to pdbFile
317    * @param targetChains
318    *          - optional chain specification for mapping each sequence to pdb
319    *          (may be nill, individual elements may be nill)
320    * @param pdbFile
321    *          - structure data resource
322    * @param protocol
323    *          - how to resolve data from resource
324    * @return null or the structure data parsed as a pdb file
325    */
326   synchronized public StructureFile setMapping(SequenceI[] sequence,
327           List<String>[] targetChains, String pdbFile,
328           DataSourceType protocol)
329   {
330     return setMapping(true, sequence, targetChains, pdbFile, protocol);
331   }
332
333   /**
334    * create sequence structure mappings between each sequence and the given
335    * pdbFile (retrieved via the given protocol).
336    * 
337    * @param forStructureView
338    *          when true, record the mapping for use in mouseOvers
339    * 
340    * @param sequenceArray
341    *          - one or more sequences to be mapped to pdbFile
342    * @param targetChainIds
343    *          - optional chain specification for mapping each sequence to pdb
344    *          (may be nill, individual elements may be nill) - JBPNote: JAL-2693
345    *          - this should be List<List<String>>, empty lists indicate no
346    *          predefined mappings
347    * @param pdbFile
348    *          - structure data resource
349    * @param sourceType
350    *          - how to resolve data from resource
351    * @return null or the structure data parsed as a pdb file
352    */
353   synchronized public StructureFile setMapping(boolean forStructureView,
354           SequenceI[] sequenceArray, List<String>[] targetChainIds,
355           String pdbFile, DataSourceType sourceType)
356   {
357     /*
358      * There will be better ways of doing this in the future, for now we'll use
359      * the tried and tested MCview pdb mapping
360      */
361     boolean parseSecStr = processSecondaryStructure;
362     if (isPDBFileRegistered(pdbFile))
363     {
364       for (SequenceI sq : sequenceArray)
365       {
366         SequenceI ds = sq;
367         while (ds.getDatasetSequence() != null)
368         {
369           ds = ds.getDatasetSequence();
370         }
371         ;
372         if (ds.getAnnotation() != null)
373         {
374           for (AlignmentAnnotation ala : ds.getAnnotation())
375           {
376             // false if any annotation present from this structure
377             // JBPNote this fails for jmol/chimera view because the *file* is
378             // passed, not the structure data ID -
379             if (PDBfile.isCalcIdForFile(ala, findIdForPDBFile(pdbFile)))
380             {
381               parseSecStr = false;
382             }
383           }
384         }
385       }
386     }
387     StructureFile pdb = null;
388     boolean isMapUsingSIFTs = SiftsSettings.isMapWithSifts();
389     try
390     {
391       sourceType = AppletFormatAdapter.checkProtocol(pdbFile);
392       pdb = new JmolParser(pdbFile, sourceType);
393
394       if (pdb.getId() != null && pdb.getId().trim().length() > 0
395               && DataSourceType.FILE == sourceType)
396       {
397         registerPDBFile(pdb.getId().trim(), pdbFile);
398       }
399       // if PDBId is unavailable then skip SIFTS mapping execution path
400       isMapUsingSIFTs = isMapUsingSIFTs && pdb.isPPDBIdAvailable();
401
402     } catch (Exception ex)
403     {
404       ex.printStackTrace();
405       return null;
406     }
407
408     try
409     {
410       if (isMapUsingSIFTs)
411       {
412         siftsClient = new SiftsClient(pdb);
413       }
414     } catch (SiftsException e)
415     {
416       isMapUsingSIFTs = false;
417       e.printStackTrace();
418     }
419
420     List<String> targetChainId = new ArrayList();
421     String _targetChainId;
422     for (int s = 0; s < sequenceArray.length; s++)
423     {
424       boolean infChain = true;
425       final SequenceI seq = sequenceArray[s];
426       SequenceI ds = seq;
427       while (ds.getDatasetSequence() != null)
428       {
429         ds = ds.getDatasetSequence();
430       }
431
432       if (targetChainIds != null && targetChainIds[s] != null)
433       {
434         infChain = false;
435         targetChainId = targetChainIds[s];
436       }
437       else if (seq.getName().indexOf("|") > -1)
438       {
439         targetChainId = new ArrayList();
440         _targetChainId=seq.getName()
441                 .substring(seq.getName().lastIndexOf("|") + 1);
442         if (_targetChainId.length() > 1)
443         {
444           if (_targetChainId.trim().length() == 0)
445           {
446             _targetChainId = " ";
447           }
448           else
449           {
450             // not a valid chain identifier
451             _targetChainId = "";
452           }
453         }
454         targetChainId.add(_targetChainId);
455       }
456       else
457       {
458         _targetChainId = "";
459       }
460       
461
462       /*
463        * Attempt pairwise alignment of the sequence with each chain in the PDB,
464        * and remember the highest scoring chain
465        */
466       float max = -10;
467       AlignSeq maxAlignseq = null;
468       String maxChainId = " ";
469       PDBChain maxChain = null;
470       boolean first = true;
471       List<PDBChain> maximalChain = new ArrayList<>();
472       List<AlignSeq> maximalAlignseqs = new ArrayList<>();
473       List<String> maximalChainIds = new ArrayList<>();
474       for (PDBChain chain : pdb.getChains())
475       {
476         if (targetChainId.size() > 0 && !targetChainId.contains(chain.id)
477                 && !infChain)
478         {
479           continue; // don't try to map chains don't match.
480         }
481         // TODO: correctly determine sequence type for mixed na/peptide
482         // structures
483         final String type = chain.isNa ? AlignSeq.DNA : AlignSeq.PEP;
484         AlignSeq as = AlignSeq.doGlobalNWAlignment(seq, chain.sequence,
485                 type);
486         // equivalent to:
487         // AlignSeq as = new AlignSeq(sequence[s], chain.sequence, type);
488         // as.calcScoreMatrix();
489         // as.traceAlignment();
490
491         if (targetChainId.size() > 0 && targetChainId.contains(chain.id))
492         {
493           // Don't care - just pick this chain as the mapping
494           maxChain = chain;
495           max = as.maxscore;
496           maxAlignseq = as;
497           maxChainId = chain.id;
498           // if targetChainId is specified then it is expected to be unique, so
499           // precisely one maximal chain will be added
500           maximalChainIds.add(chain.id);
501           maximalChain.add(chain);
502           maximalAlignseqs.add(as);
503         }
504         else
505         {
506           // select chains with maximal mappings to this sequence
507           if (first || as.maxscore > max)
508           {
509             // clear out old maximal mappings (if any)
510             max = as.maxscore;
511             maximalChain.clear();
512             maxChain = chain;
513             max = as.maxscore;
514             maxAlignseq = as;
515             maxChainId = chain.id;
516             first = false;
517           }
518           if (as.maxscore == max)
519           {
520             maximalChainIds.add(chain.id);
521             maximalChain.add(chain);
522             maximalAlignseqs.add(as);
523           }
524         }
525       }
526       if (maxChain == null)
527       {
528         continue;
529       }
530
531       if (sourceType == DataSourceType.PASTE)
532       {
533         pdbFile = "INLINE" + pdb.getId();
534       }
535
536       List<StructureMapping> seqToStrucMapping = new ArrayList<>();
537       if (isMapUsingSIFTs && seq.isProtein())
538       {
539         setProgressBar(null);
540         setProgressBar(MessageManager
541                 .getString("status.obtaining_mapping_with_sifts"));
542         jalview.datamodel.Mapping sqmpping = maxAlignseq
543                 .getMappingFromS1(false);
544         if (targetChainId != null && !targetChainId.isEmpty())
545         {
546           List<StructureMapping> siftsMappings;
547           try
548           {
549             siftsMappings = getStructureMapping(seq, pdbFile, targetChainId,
550                     pdb, maxChain, sqmpping, maxAlignseq);
551             for (StructureMapping siftsMapping : siftsMappings)
552             {
553               seqToStrucMapping.add(siftsMapping);
554             maxChain.makeExactMapping(maxAlignseq, seq);
555             maxChain.transferRESNUMFeatures(seq, null);// FIXME: is this
556                                                        // "IEA:SIFTS" ?
557             maxChain.transferResidueAnnotation(siftsMapping, sqmpping);
558             ds.addPDBId(maxChain.sequence.getAllPDBEntries().get(0));
559             }
560
561           } catch (SiftsException e)
562           {
563             // fall back to NW alignment
564             System.err.println(e.getMessage());
565             StructureMapping nwMapping = getNWMappings(seq, pdbFile,
566                     maximalChainIds, maximalChain, pdb, maximalAlignseqs)
567                             .get(0);
568             seqToStrucMapping.add(nwMapping);
569             maxChain.makeExactMapping(maxAlignseq, seq);
570             maxChain.transferRESNUMFeatures(seq, null); // FIXME: is this
571                                                         // "IEA:Jalview" ?
572             maxChain.transferResidueAnnotation(nwMapping, sqmpping);
573             ds.addPDBId(maxChain.sequence.getAllPDBEntries().get(0));
574           }
575         }
576         else
577         {
578           List<StructureMapping> foundSiftsMappings = new ArrayList<>();
579           for (PDBChain chain : pdb.getChains())
580           {
581             try
582             {
583               foundSiftsMappings.addAll(getStructureMapping(seq, pdbFile,
584                       Arrays.asList(new String[]
585                       { chain.id }), pdb, chain, sqmpping, maxAlignseq));
586             } catch (SiftsException e)
587             {
588               System.err.println(e.getMessage());
589             }
590           }
591           if (!foundSiftsMappings.isEmpty())
592           {
593             seqToStrucMapping.addAll(foundSiftsMappings);
594             maxChain.makeExactMapping(maxAlignseq, seq);
595             maxChain.transferRESNUMFeatures(seq, null);// FIXME: is this
596                                                        // "IEA:SIFTS" ?
597             maxChain.transferResidueAnnotation(foundSiftsMappings.get(0),
598                     sqmpping);
599             ds.addPDBId(sqmpping.getTo().getAllPDBEntries().get(0));
600           }
601           else
602           {
603             StructureMapping nwMapping = getNWMappings(seq, pdbFile,
604                     maximalChainIds, maximalChain, pdb, maximalAlignseqs)
605                             .get(0);
606             seqToStrucMapping.add(nwMapping);
607             maxChain.transferRESNUMFeatures(seq, null); // FIXME: is this
608                                                         // "IEA:Jalview" ?
609             maxChain.transferResidueAnnotation(nwMapping, sqmpping);
610             ds.addPDBId(maxChain.sequence.getAllPDBEntries().get(0));
611           }
612         }
613       }
614       else
615       {
616         setProgressBar(null);
617         setProgressBar(MessageManager
618                 .getString("status.obtaining_mapping_with_nw_alignment"));
619         List<StructureMapping> nwMapping = getNWMappings(seq, pdbFile,
620                 maximalChainIds, maximalChain, pdb, maximalAlignseqs);
621         seqToStrucMapping.addAll(nwMapping);
622         for (PDBChain mc : maximalChain)
623         {
624           ds.addPDBId(mc.sequence.getAllPDBEntries().get(0));
625         }
626         ;
627
628       }
629
630       if (forStructureView)
631       {
632         mappings.addAll(seqToStrucMapping);
633       }
634     }
635     return pdb;
636   }
637
638   public void addStructureMapping(StructureMapping sm)
639   {
640     mappings.add(sm);
641   }
642
643   /**
644    * retrieve a mapping for seq from SIFTs using associated DBRefEntry for
645    * uniprot or PDB
646    * 
647    * @param seq
648    * @param pdbFile
649    * @param targetChainId
650    * @param pdb
651    * @param maxChain
652    * @param sqmpping
653    * @param maxAlignseq
654    * @return
655    * @throws SiftsException
656    */
657   private List<StructureMapping> getStructureMapping(SequenceI seq,
658           String pdbFile, List<String> targetChainIdList, StructureFile pdb,
659           PDBChain maxChain, jalview.datamodel.Mapping sqmpping,
660           AlignSeq maxAlignseq) throws SiftsException
661   {
662     List<StructureMapping> currChainMappingList = new ArrayList();
663     int p = 0;
664     do
665     {
666       String targetChainId = (targetChainIdList == null
667               || p >= targetChainIdList.size()) ? null
668                       : targetChainIdList.get(p);
669       StructureMapping curChainMapping = siftsClient
670
671             .getSiftsStructureMapping(seq, pdbFile, targetChainId);
672     try
673     {
674       PDBChain chain = pdb.findChain(targetChainId);
675       if (chain != null)
676       {
677         chain.transferResidueAnnotation(curChainMapping, sqmpping);
678           currChainMappingList.add(curChainMapping);
679       }
680     } catch (Exception e)
681     {
682       e.printStackTrace();
683     }
684     } while (targetChainIdList != null && ++p < targetChainIdList.size());
685     return currChainMappingList;
686   }
687
688   private List<StructureMapping> getNWMappings(SequenceI seq,
689           String pdbFile,
690           List<String> maximalChainId, List<PDBChain> maximalChain,
691           StructureFile pdb, List<AlignSeq> maximalAlignseq)
692   {
693     List<StructureMapping> nwMappings = new ArrayList();
694     for (int ch = 0; ch < maximalChain.size(); ch++)
695     {
696       String maxChainId = maximalChainId.get(ch);
697       PDBChain maxChain = maximalChain.get(ch);
698       AlignSeq maxAlignseq = maximalAlignseq.get(ch);
699     final StringBuilder mappingDetails = new StringBuilder(128);
700     mappingDetails.append(NEWLINE)
701             .append("Sequence \u27f7 Structure mapping details");
702     mappingDetails.append(NEWLINE);
703     mappingDetails
704             .append("Method: inferred with Needleman & Wunsch alignment");
705     mappingDetails.append(NEWLINE).append("PDB Sequence is :")
706             .append(NEWLINE).append("Sequence = ")
707             .append(maxChain.sequence.getSequenceAsString());
708     mappingDetails.append(NEWLINE).append("No of residues = ")
709             .append(maxChain.residues.size()).append(NEWLINE)
710             .append(NEWLINE);
711     PrintStream ps = new PrintStream(System.out)
712     {
713       @Override
714       public void print(String x)
715       {
716         mappingDetails.append(x);
717       }
718
719       @Override
720       public void println()
721       {
722         mappingDetails.append(NEWLINE);
723       }
724     };
725
726     maxAlignseq.printAlignment(ps);
727
728     mappingDetails.append(NEWLINE).append("PDB start/end ");
729     mappingDetails.append(String.valueOf(maxAlignseq.seq2start))
730             .append(" ");
731     mappingDetails.append(String.valueOf(maxAlignseq.seq2end));
732     mappingDetails.append(NEWLINE).append("SEQ start/end ");
733     mappingDetails
734             .append(String
735                     .valueOf(maxAlignseq.seq1start + (seq.getStart() - 1)))
736             .append(" ");
737     mappingDetails.append(
738             String.valueOf(maxAlignseq.seq1end + (seq.getStart() - 1)));
739     mappingDetails.append(NEWLINE);
740     maxChain.makeExactMapping(maxAlignseq, seq);
741     jalview.datamodel.Mapping sqmpping = maxAlignseq
742             .getMappingFromS1(false);
743     maxChain.transferRESNUMFeatures(seq, null);
744
745     HashMap<Integer, int[]> mapping = new HashMap<>();
746     int resNum = -10000;
747     int index = 0;
748     char insCode = ' ';
749
750     do
751     {
752       Atom tmp = maxChain.atoms.elementAt(index);
753       if ((resNum != tmp.resNumber || insCode != tmp.insCode)
754               && tmp.alignmentMapping != -1)
755       {
756         resNum = tmp.resNumber;
757         insCode = tmp.insCode;
758         if (tmp.alignmentMapping >= -1)
759         {
760           mapping.put(tmp.alignmentMapping + 1,
761                   new int[]
762                   { tmp.resNumber, tmp.atomIndex });
763         }
764       }
765
766       index++;
767     } while (index < maxChain.atoms.size());
768
769     StructureMapping nwMapping = new StructureMapping(seq, pdbFile,
770             pdb.getId(), maxChainId, mapping, mappingDetails.toString());
771     maxChain.transferResidueAnnotation(nwMapping, sqmpping);
772       nwMappings.add(nwMapping);
773     }
774     return nwMappings;
775   }
776
777   public void removeStructureViewerListener(Object svl, String[] pdbfiles)
778   {
779     listeners.removeElement(svl);
780     if (svl instanceof SequenceListener)
781     {
782       for (int i = 0; i < listeners.size(); i++)
783       {
784         if (listeners.elementAt(i) instanceof StructureListener)
785         {
786           ((StructureListener) listeners.elementAt(i))
787                   .releaseReferences(svl);
788         }
789       }
790     }
791
792     if (pdbfiles == null)
793     {
794       return;
795     }
796
797     /*
798      * Remove mappings to the closed listener's PDB files, but first check if
799      * another listener is still interested
800      */
801     List<String> pdbs = new ArrayList<>(Arrays.asList(pdbfiles));
802
803     StructureListener sl;
804     for (int i = 0; i < listeners.size(); i++)
805     {
806       if (listeners.elementAt(i) instanceof StructureListener)
807       {
808         sl = (StructureListener) listeners.elementAt(i);
809         for (String pdbfile : sl.getStructureFiles())
810         {
811           pdbs.remove(pdbfile);
812         }
813       }
814     }
815
816     /*
817      * Rebuild the mappings set, retaining only those which are for 'other' PDB
818      * files
819      */
820     if (pdbs.size() > 0)
821     {
822       List<StructureMapping> tmp = new ArrayList<>();
823       for (StructureMapping sm : mappings)
824       {
825         if (!pdbs.contains(sm.pdbfile))
826         {
827           tmp.add(sm);
828         }
829       }
830
831       mappings = tmp;
832     }
833   }
834
835   /**
836    * Propagate mouseover of a single position in a structure
837    * 
838    * @param pdbResNum
839    * @param chain
840    * @param pdbfile
841    */
842   public void mouseOverStructure(int pdbResNum, String chain,
843           String pdbfile)
844   {
845     AtomSpec atomSpec = new AtomSpec(pdbfile, chain, pdbResNum, 0);
846     List<AtomSpec> atoms = Collections.singletonList(atomSpec);
847     mouseOverStructure(atoms);
848   }
849
850   /**
851    * Propagate mouseover or selection of multiple positions in a structure
852    * 
853    * @param atoms
854    */
855   public void mouseOverStructure(List<AtomSpec> atoms)
856   {
857     if (listeners == null)
858     {
859       // old or prematurely sent event
860       return;
861     }
862     boolean hasSequenceListener = false;
863     for (int i = 0; i < listeners.size(); i++)
864     {
865       if (listeners.elementAt(i) instanceof SequenceListener)
866       {
867         hasSequenceListener = true;
868       }
869     }
870     if (!hasSequenceListener)
871     {
872       return;
873     }
874
875     SearchResultsI results = findAlignmentPositionsForStructurePositions(
876             atoms);
877     for (Object li : listeners)
878     {
879       if (li instanceof SequenceListener)
880       {
881         ((SequenceListener) li).highlightSequence(results);
882       }
883     }
884   }
885
886   /**
887    * Constructs a SearchResults object holding regions (if any) in the Jalview
888    * alignment which have a mapping to the structure viewer positions in the
889    * supplied list
890    * 
891    * @param atoms
892    * @return
893    */
894   public SearchResultsI findAlignmentPositionsForStructurePositions(
895           List<AtomSpec> atoms)
896   {
897     SearchResultsI results = new SearchResults();
898     for (AtomSpec atom : atoms)
899     {
900       SequenceI lastseq = null;
901       int lastipos = -1;
902       for (StructureMapping sm : mappings)
903       {
904         if (sm.pdbfile.equals(atom.getPdbFile())
905                 && sm.pdbchain.equals(atom.getChain()))
906         {
907           int indexpos = sm.getSeqPos(atom.getPdbResNum());
908           if (lastipos != indexpos && lastseq != sm.sequence)
909           {
910             results.addResult(sm.sequence, indexpos, indexpos);
911             lastipos = indexpos;
912             lastseq = sm.sequence;
913             // construct highlighted sequence list
914             for (AlignedCodonFrame acf : seqmappings)
915             {
916               acf.markMappedRegion(sm.sequence, indexpos, results);
917             }
918           }
919         }
920       }
921     }
922     return results;
923   }
924
925   /**
926    * highlight regions associated with a position (indexpos) in seq
927    * 
928    * @param seq
929    *          the sequence that the mouse over occurred on
930    * @param indexpos
931    *          the absolute position being mouseovered in seq (0 to seq.length())
932    * @param seqPos
933    *          the sequence position (if -1, seq.findPosition is called to
934    *          resolve the residue number)
935    */
936   public void mouseOverSequence(SequenceI seq, int indexpos, int seqPos,
937           VamsasSource source)
938   {
939     boolean hasSequenceListeners = handlingVamsasMo
940             || !seqmappings.isEmpty();
941     SearchResultsI results = null;
942     if (seqPos == -1)
943     {
944       seqPos = seq.findPosition(indexpos);
945     }
946     for (int i = 0; i < listeners.size(); i++)
947     {
948       Object listener = listeners.elementAt(i);
949       if (listener == source)
950       {
951         // TODO listener (e.g. SeqPanel) is never == source (AlignViewport)
952         // Temporary fudge with SequenceListener.getVamsasSource()
953         continue;
954       }
955       if (listener instanceof StructureListener)
956       {
957         highlightStructure((StructureListener) listener, seq, seqPos);
958       }
959       else
960       {
961         if (listener instanceof SequenceListener)
962         {
963           final SequenceListener seqListener = (SequenceListener) listener;
964           if (hasSequenceListeners
965                   && seqListener.getVamsasSource() != source)
966           {
967             if (relaySeqMappings)
968             {
969               if (results == null)
970               {
971                 results = MappingUtils.buildSearchResults(seq, seqPos,
972                         seqmappings);
973               }
974               if (handlingVamsasMo)
975               {
976                 results.addResult(seq, seqPos, seqPos);
977
978               }
979               if (!results.isEmpty())
980               {
981                 seqListener.highlightSequence(results);
982               }
983             }
984           }
985         }
986         else if (listener instanceof VamsasListener && !handlingVamsasMo)
987         {
988           ((VamsasListener) listener).mouseOverSequence(seq, indexpos,
989                   source);
990         }
991         else if (listener instanceof SecondaryStructureListener)
992         {
993           ((SecondaryStructureListener) listener).mouseOverSequence(seq,
994                   indexpos, seqPos);
995         }
996       }
997     }
998   }
999
1000   /**
1001    * Send suitable messages to a StructureListener to highlight atoms
1002    * corresponding to the given sequence position(s)
1003    * 
1004    * @param sl
1005    * @param seq
1006    * @param positions
1007    */
1008   public void highlightStructure(StructureListener sl, SequenceI seq,
1009           int... positions)
1010   {
1011     if (!sl.isListeningFor(seq))
1012     {
1013       return;
1014     }
1015     int atomNo;
1016     List<AtomSpec> atoms = new ArrayList<>();
1017     for (StructureMapping sm : mappings)
1018     {
1019       if (sm.sequence == seq || sm.sequence == seq.getDatasetSequence()
1020               || (sm.sequence.getDatasetSequence() != null && sm.sequence
1021                       .getDatasetSequence() == seq.getDatasetSequence()))
1022       {
1023         for (int index : positions)
1024         {
1025           atomNo = sm.getAtomNum(index);
1026
1027           if (atomNo > 0)
1028           {
1029             atoms.add(new AtomSpec(sm.pdbfile, sm.pdbchain,
1030                     sm.getPDBResNum(index), atomNo));
1031           }
1032         }
1033       }
1034     }
1035     sl.highlightAtoms(atoms);
1036   }
1037
1038   /**
1039    * true if a mouse over event from an external (ie Vamsas) source is being
1040    * handled
1041    */
1042   boolean handlingVamsasMo = false;
1043
1044   long lastmsg = 0;
1045
1046   /**
1047    * as mouseOverSequence but only route event to SequenceListeners
1048    * 
1049    * @param sequenceI
1050    * @param position
1051    *          in an alignment sequence
1052    */
1053   public void mouseOverVamsasSequence(SequenceI sequenceI, int position,
1054           VamsasSource source)
1055   {
1056     handlingVamsasMo = true;
1057     long msg = sequenceI.hashCode() * (1 + position);
1058     if (lastmsg != msg)
1059     {
1060       lastmsg = msg;
1061       mouseOverSequence(sequenceI, position, -1, source);
1062     }
1063     handlingVamsasMo = false;
1064   }
1065
1066   public Annotation[] colourSequenceFromStructure(SequenceI seq,
1067           String pdbid)
1068   {
1069     return null;
1070     // THIS WILL NOT BE AVAILABLE IN JALVIEW 2.3,
1071     // UNTIL THE COLOUR BY ANNOTATION IS REWORKED
1072     /*
1073      * Annotation [] annotations = new Annotation[seq.getLength()];
1074      * 
1075      * StructureListener sl; int atomNo = 0; for (int i = 0; i <
1076      * listeners.size(); i++) { if (listeners.elementAt(i) instanceof
1077      * StructureListener) { sl = (StructureListener) listeners.elementAt(i);
1078      * 
1079      * for (int j = 0; j < mappings.length; j++) {
1080      * 
1081      * if (mappings[j].sequence == seq && mappings[j].getPdbId().equals(pdbid)
1082      * && mappings[j].pdbfile.equals(sl.getPdbFile())) {
1083      * System.out.println(pdbid+" "+mappings[j].getPdbId() +"
1084      * "+mappings[j].pdbfile);
1085      * 
1086      * java.awt.Color col; for(int index=0; index<seq.getLength(); index++) {
1087      * if(jalview.util.Comparison.isGap(seq.getCharAt(index))) continue;
1088      * 
1089      * atomNo = mappings[j].getAtomNum(seq.findPosition(index)); col =
1090      * java.awt.Color.white; if (atomNo > 0) { col = sl.getColour(atomNo,
1091      * mappings[j].getPDBResNum(index), mappings[j].pdbchain,
1092      * mappings[j].pdbfile); }
1093      * 
1094      * annotations[index] = new Annotation("X",null,' ',0,col); } return
1095      * annotations; } } } }
1096      * 
1097      * return annotations;
1098      */
1099   }
1100
1101   public void structureSelectionChanged()
1102   {
1103   }
1104
1105   public void sequenceSelectionChanged()
1106   {
1107   }
1108
1109   public void sequenceColoursChanged(Object source)
1110   {
1111     StructureListener sl;
1112     for (int i = 0; i < listeners.size(); i++)
1113     {
1114       if (listeners.elementAt(i) instanceof StructureListener)
1115       {
1116         sl = (StructureListener) listeners.elementAt(i);
1117         sl.updateColours(source);
1118       }
1119     }
1120   }
1121
1122   public StructureMapping[] getMapping(String pdbfile)
1123   {
1124     List<StructureMapping> tmp = new ArrayList<>();
1125     for (StructureMapping sm : mappings)
1126     {
1127       if (sm.pdbfile.equals(pdbfile))
1128       {
1129         tmp.add(sm);
1130       }
1131     }
1132     return tmp.toArray(new StructureMapping[tmp.size()]);
1133   }
1134
1135   /**
1136    * Returns a readable description of all mappings for the given pdbfile to any
1137    * of the given sequences
1138    * 
1139    * @param pdbfile
1140    * @param seqs
1141    * @return
1142    */
1143   public String printMappings(String pdbfile, List<SequenceI> seqs)
1144   {
1145     if (pdbfile == null || seqs == null || seqs.isEmpty())
1146     {
1147       return "";
1148     }
1149
1150     StringBuilder sb = new StringBuilder(64);
1151     for (StructureMapping sm : mappings)
1152     {
1153       if (sm.pdbfile.equals(pdbfile) && seqs.contains(sm.sequence))
1154       {
1155         sb.append(sm.mappingDetails);
1156         sb.append(NEWLINE);
1157         // separator makes it easier to read multiple mappings
1158         sb.append("=====================");
1159         sb.append(NEWLINE);
1160       }
1161     }
1162     sb.append(NEWLINE);
1163
1164     return sb.toString();
1165   }
1166
1167   /**
1168    * Remove the given mapping
1169    * 
1170    * @param acf
1171    */
1172   public void deregisterMapping(AlignedCodonFrame acf)
1173   {
1174     if (acf != null)
1175     {
1176       boolean removed = seqmappings.remove(acf);
1177       if (removed && seqmappings.isEmpty())
1178       { // debug
1179         System.out.println("All mappings removed");
1180       }
1181     }
1182   }
1183
1184   /**
1185    * Add each of the given codonFrames to the stored set, if not aready present.
1186    * 
1187    * @param mappings
1188    */
1189   public void registerMappings(List<AlignedCodonFrame> mappings)
1190   {
1191     if (mappings != null)
1192     {
1193       for (AlignedCodonFrame acf : mappings)
1194       {
1195         registerMapping(acf);
1196       }
1197     }
1198   }
1199
1200   /**
1201    * Add the given mapping to the stored set, unless already stored.
1202    */
1203   public void registerMapping(AlignedCodonFrame acf)
1204   {
1205     if (acf != null)
1206     {
1207       if (!seqmappings.contains(acf))
1208       {
1209         seqmappings.add(acf);
1210       }
1211     }
1212   }
1213
1214   /**
1215    * Resets this object to its initial state by removing all registered
1216    * listeners, codon mappings, PDB file mappings
1217    */
1218   public void resetAll()
1219   {
1220     if (mappings != null)
1221     {
1222       mappings.clear();
1223     }
1224     if (seqmappings != null)
1225     {
1226       seqmappings.clear();
1227     }
1228     if (sel_listeners != null)
1229     {
1230       sel_listeners.clear();
1231     }
1232     if (listeners != null)
1233     {
1234       listeners.clear();
1235     }
1236     if (commandListeners != null)
1237     {
1238       commandListeners.clear();
1239     }
1240     if (view_listeners != null)
1241     {
1242       view_listeners.clear();
1243     }
1244     if (pdbFileNameId != null)
1245     {
1246       pdbFileNameId.clear();
1247     }
1248     if (pdbIdFileName != null)
1249     {
1250       pdbIdFileName.clear();
1251     }
1252   }
1253
1254   public void addSelectionListener(SelectionListener selecter)
1255   {
1256     if (!sel_listeners.contains(selecter))
1257     {
1258       sel_listeners.add(selecter);
1259     }
1260   }
1261
1262   public void removeSelectionListener(SelectionListener toremove)
1263   {
1264     if (sel_listeners.contains(toremove))
1265     {
1266       sel_listeners.remove(toremove);
1267     }
1268   }
1269
1270   public synchronized void sendSelection(
1271           jalview.datamodel.SequenceGroup selection,
1272           jalview.datamodel.ColumnSelection colsel, HiddenColumns hidden,
1273           SelectionSource source)
1274   {
1275     for (SelectionListener slis : sel_listeners)
1276     {
1277       if (slis != source)
1278       {
1279         slis.selection(selection, colsel, hidden, source);
1280       }
1281     }
1282   }
1283
1284   Vector<AlignmentViewPanelListener> view_listeners = new Vector<>();
1285
1286   public synchronized void sendViewPosition(
1287           jalview.api.AlignmentViewPanel source, int startRes, int endRes,
1288           int startSeq, int endSeq)
1289   {
1290
1291     if (view_listeners != null && view_listeners.size() > 0)
1292     {
1293       Enumeration<AlignmentViewPanelListener> listeners = view_listeners
1294               .elements();
1295       while (listeners.hasMoreElements())
1296       {
1297         AlignmentViewPanelListener slis = listeners.nextElement();
1298         if (slis != source)
1299         {
1300           slis.viewPosition(startRes, endRes, startSeq, endSeq, source);
1301         }
1302         ;
1303       }
1304     }
1305   }
1306
1307   /**
1308    * release all references associated with this manager provider
1309    * 
1310    * @param jalviewLite
1311    */
1312   public static void release(StructureSelectionManagerProvider jalviewLite)
1313   {
1314     // synchronized (instances)
1315     {
1316       if (instances == null)
1317       {
1318         return;
1319       }
1320       StructureSelectionManager mnger = (instances.get(jalviewLite));
1321       if (mnger != null)
1322       {
1323         instances.remove(jalviewLite);
1324         try
1325         {
1326           mnger.finalize();
1327         } catch (Throwable x)
1328         {
1329         }
1330       }
1331     }
1332   }
1333
1334   public void registerPDBEntry(PDBEntry pdbentry)
1335   {
1336     if (pdbentry.getFile() != null
1337             && pdbentry.getFile().trim().length() > 0)
1338     {
1339       registerPDBFile(pdbentry.getId(), pdbentry.getFile());
1340     }
1341   }
1342
1343   public void addCommandListener(CommandListener cl)
1344   {
1345     if (!commandListeners.contains(cl))
1346     {
1347       commandListeners.add(cl);
1348     }
1349   }
1350
1351   public boolean hasCommandListener(CommandListener cl)
1352   {
1353     return this.commandListeners.contains(cl);
1354   }
1355
1356   public boolean removeCommandListener(CommandListener l)
1357   {
1358     return commandListeners.remove(l);
1359   }
1360
1361   /**
1362    * Forward a command to any command listeners (except for the command's
1363    * source).
1364    * 
1365    * @param command
1366    *          the command to be broadcast (in its form after being performed)
1367    * @param undo
1368    *          if true, the command was being 'undone'
1369    * @param source
1370    */
1371   public void commandPerformed(CommandI command, boolean undo,
1372           VamsasSource source)
1373   {
1374     for (CommandListener listener : commandListeners)
1375     {
1376       listener.mirrorCommand(command, undo, this, source);
1377     }
1378   }
1379
1380   /**
1381    * Returns a new CommandI representing the given command as mapped to the
1382    * given sequences. If no mapping could be made, or the command is not of a
1383    * mappable kind, returns null.
1384    * 
1385    * @param command
1386    * @param undo
1387    * @param mapTo
1388    * @param gapChar
1389    * @return
1390    */
1391   public CommandI mapCommand(CommandI command, boolean undo,
1392           final AlignmentI mapTo, char gapChar)
1393   {
1394     if (command instanceof EditCommand)
1395     {
1396       return MappingUtils.mapEditCommand((EditCommand) command, undo, mapTo,
1397               gapChar, seqmappings);
1398     }
1399     else if (command instanceof OrderCommand)
1400     {
1401       return MappingUtils.mapOrderCommand((OrderCommand) command, undo,
1402               mapTo, seqmappings);
1403     }
1404     return null;
1405   }
1406
1407   public IProgressIndicator getProgressIndicator()
1408   {
1409     return progressIndicator;
1410   }
1411
1412   public void setProgressIndicator(IProgressIndicator progressIndicator)
1413   {
1414     this.progressIndicator = progressIndicator;
1415   }
1416
1417   public long getProgressSessionId()
1418   {
1419     return progressSessionId;
1420   }
1421
1422   public void setProgressSessionId(long progressSessionId)
1423   {
1424     this.progressSessionId = progressSessionId;
1425   }
1426
1427   public void setProgressBar(String message)
1428   {
1429     if (progressIndicator == null)
1430     {
1431       return;
1432     }
1433     progressIndicator.setProgressBar(message, progressSessionId);
1434   }
1435
1436   public List<AlignedCodonFrame> getSequenceMappings()
1437   {
1438     return seqmappings;
1439   }
1440
1441   /**
1442    * utilty method - transform array of chain characters
1443    * 
1444    * @param chains
1445    * @return array of lists with a chain in each
1446    */
1447   public static List<String>[] perChainList(String[] chains)
1448   {
1449     List<String>[] chainsL = new List[chains.length];
1450     int p = 0;
1451     for (String ch : chains)
1452     {
1453       chainsL[p] = new ArrayList();
1454       chainsL[p++].add(ch);
1455     }
1456     return chainsL;
1457   }
1458
1459 }