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