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