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