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