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