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