JAL-2349 JAL-3855 highlight residues associated with elements under mouse - Jmol...
[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    * hack to highlight a range of positions at once on any structure views
878    * 
879    * @param sequenceRef
880    * @param is
881    *          - series of int start-end ranges as positions on sequenceRef
882    * @param i
883    * @param object
884    */
885   public void highlightPositionsOn(SequenceI sequenceRef, int[][] is,
886           Object source)
887   {
888     boolean hasSequenceListeners = handlingVamsasMo
889             || !seqmappings.isEmpty();
890     SearchResultsI results = null;
891     ArrayList<Integer> listOfPositions = new ArrayList<Integer>();
892     for (int[] s_e : is)
893     {
894       for (int p = s_e[0]; p <= s_e[1]; listOfPositions.add(p++))
895         ;
896     }
897     int seqpos[] = new int[listOfPositions.size()];
898     int i = 0;
899     for (Integer p : listOfPositions)
900     {
901       seqpos[i++] = p;
902     }
903
904     for (i = 0; i < listeners.size(); i++)
905     {
906       Object listener = listeners.elementAt(i);
907       if (listener == source)
908       {
909         // TODO listener (e.g. SeqPanel) is never == source (AlignViewport)
910         // Temporary fudge with SequenceListener.getVamsasSource()
911         continue;
912       }
913       if (listener instanceof StructureListener)
914       {
915         highlightStructure((StructureListener) listener, sequenceRef,
916                 seqpos);
917       }
918
919     }
920   }
921
922   /**
923    * Propagate mouseover of a single position in a structure
924    * 
925    * @param pdbResNum
926    * @param chain
927    * @param pdbfile
928    * @return
929    */
930   public String mouseOverStructure(int pdbResNum, String chain,
931           String pdbfile)
932   {
933     AtomSpec atomSpec = new AtomSpec(pdbfile, chain, pdbResNum, 0);
934     List<AtomSpec> atoms = Collections.singletonList(atomSpec);
935     return mouseOverStructure(atoms);
936   }
937
938   /**
939    * Propagate mouseover or selection of multiple positions in a structure
940    * 
941    * @param atoms
942    */
943   public String mouseOverStructure(List<AtomSpec> atoms)
944   {
945     if (listeners == null)
946     {
947       // old or prematurely sent event
948       return null;
949     }
950     boolean hasSequenceListener = false;
951     for (int i = 0; i < listeners.size(); i++)
952     {
953       if (listeners.elementAt(i) instanceof SequenceListener)
954       {
955         hasSequenceListener = true;
956       }
957     }
958     if (!hasSequenceListener)
959     {
960       return null;
961     }
962
963     SearchResultsI results = findAlignmentPositionsForStructurePositions(
964             atoms);
965     String result = null;
966     for (Object li : listeners)
967     {
968       if (li instanceof SequenceListener)
969       {
970         String s = ((SequenceListener) li).highlightSequence(results);
971         if (s != null)
972         {
973           result = s;
974         }
975       }
976     }
977     return result;
978   }
979
980   /**
981    * Constructs a SearchResults object holding regions (if any) in the Jalview
982    * alignment which have a mapping to the structure viewer positions in the
983    * supplied list
984    * 
985    * @param atoms
986    * @return
987    */
988   public SearchResultsI findAlignmentPositionsForStructurePositions(
989           List<AtomSpec> atoms)
990   {
991     SearchResultsI results = new SearchResults();
992     for (AtomSpec atom : atoms)
993     {
994       SequenceI lastseq = null;
995       int lastipos = -1;
996       for (StructureMapping sm : mappings)
997       {
998         if (sm.pdbfile.equals(atom.getPdbFile())
999                 && sm.pdbchain.equals(atom.getChain()))
1000         {
1001           int indexpos = sm.getSeqPos(atom.getPdbResNum());
1002           if (lastipos != indexpos || lastseq != sm.sequence)
1003           {
1004             results.addResult(sm.sequence, indexpos, indexpos);
1005             lastipos = indexpos;
1006             lastseq = sm.sequence;
1007             // construct highlighted sequence list
1008             for (AlignedCodonFrame acf : seqmappings)
1009             {
1010               acf.markMappedRegion(sm.sequence, indexpos, results);
1011             }
1012           }
1013         }
1014       }
1015     }
1016     return results;
1017   }
1018
1019   /**
1020    * highlight regions associated with a position (indexpos) in seq
1021    * 
1022    * @param seq
1023    *          the sequence that the mouse over occurred on
1024    * @param indexpos
1025    *          the absolute position being mouseovered in seq (0 to seq.length())
1026    * @param seqPos
1027    *          the sequence position (if -1, seq.findPosition is called to
1028    *          resolve the residue number)
1029    */
1030   public void mouseOverSequence(SequenceI seq, int indexpos, int seqPos,
1031           VamsasSource source)
1032   {
1033     boolean hasSequenceListeners = handlingVamsasMo
1034             || !seqmappings.isEmpty();
1035     SearchResultsI results = null;
1036     if (seqPos == -1)
1037     {
1038       seqPos = seq.findPosition(indexpos);
1039     }
1040     for (int i = 0; i < listeners.size(); i++)
1041     {
1042       Object listener = listeners.elementAt(i);
1043       if (listener == source)
1044       {
1045         // TODO listener (e.g. SeqPanel) is never == source (AlignViewport)
1046         // Temporary fudge with SequenceListener.getVamsasSource()
1047         continue;
1048       }
1049       if (listener instanceof StructureListener)
1050       {
1051         highlightStructure((StructureListener) listener, seq, seqPos);
1052       }
1053       else
1054       {
1055         if (listener instanceof SequenceListener)
1056         {
1057           final SequenceListener seqListener = (SequenceListener) listener;
1058           if (hasSequenceListeners
1059                   && seqListener.getVamsasSource() != source)
1060           {
1061             if (relaySeqMappings)
1062             {
1063               if (results == null)
1064               {
1065                 results = MappingUtils.buildSearchResults(seq, seqPos,
1066                         seqmappings);
1067               }
1068               if (handlingVamsasMo)
1069               {
1070                 results.addResult(seq, seqPos, seqPos);
1071
1072               }
1073               if (!results.isEmpty())
1074               {
1075                 seqListener.highlightSequence(results);
1076               }
1077             }
1078           }
1079         }
1080         else if (listener instanceof VamsasListener && !handlingVamsasMo)
1081         {
1082           ((VamsasListener) listener).mouseOverSequence(seq, indexpos,
1083                   source);
1084         }
1085         else if (listener instanceof SecondaryStructureListener)
1086         {
1087           ((SecondaryStructureListener) listener).mouseOverSequence(seq,
1088                   indexpos, seqPos);
1089         }
1090       }
1091     }
1092   }
1093
1094   /**
1095    * Send suitable messages to a StructureListener to highlight atoms
1096    * corresponding to the given sequence position(s)
1097    * 
1098    * @param sl
1099    * @param seq
1100    * @param positions
1101    */
1102   public void highlightStructure(StructureListener sl, SequenceI seq,
1103           int... positions)
1104   {
1105     if (!sl.isListeningFor(seq))
1106     {
1107       return;
1108     }
1109     int atomNo;
1110     List<AtomSpec> atoms = new ArrayList<>();
1111     for (StructureMapping sm : mappings)
1112     {
1113       if (sm.sequence == seq || sm.sequence == seq.getDatasetSequence()
1114               || (sm.sequence.getDatasetSequence() != null && sm.sequence
1115                       .getDatasetSequence() == seq.getDatasetSequence()))
1116       {
1117         for (int index : positions)
1118         {
1119           atomNo = sm.getAtomNum(index);
1120
1121           if (atomNo > 0)
1122           {
1123             atoms.add(new AtomSpec(sm.pdbfile, sm.pdbchain,
1124                     sm.getPDBResNum(index), atomNo));
1125           }
1126         }
1127       }
1128     }
1129     sl.highlightAtoms(atoms);
1130   }
1131
1132   /**
1133    * true if a mouse over event from an external (ie Vamsas) source is being
1134    * handled
1135    */
1136   boolean handlingVamsasMo = false;
1137
1138   long lastmsg = 0;
1139
1140   /**
1141    * as mouseOverSequence but only route event to SequenceListeners
1142    * 
1143    * @param sequenceI
1144    * @param position
1145    *          in an alignment sequence
1146    */
1147   public void mouseOverVamsasSequence(SequenceI sequenceI, int position,
1148           VamsasSource source)
1149   {
1150     handlingVamsasMo = true;
1151     long msg = sequenceI.hashCode() * (1 + position);
1152     if (lastmsg != msg)
1153     {
1154       lastmsg = msg;
1155       mouseOverSequence(sequenceI, position, -1, source);
1156     }
1157     handlingVamsasMo = false;
1158   }
1159
1160   public Annotation[] colourSequenceFromStructure(SequenceI seq,
1161           String pdbid)
1162   {
1163     return null;
1164     // THIS WILL NOT BE AVAILABLE IN JALVIEW 2.3,
1165     // UNTIL THE COLOUR BY ANNOTATION IS REWORKED
1166     /*
1167      * Annotation [] annotations = new Annotation[seq.getLength()];
1168      * 
1169      * StructureListener sl; int atomNo = 0; for (int i = 0; i <
1170      * listeners.size(); i++) { if (listeners.elementAt(i) instanceof
1171      * StructureListener) { sl = (StructureListener) listeners.elementAt(i);
1172      * 
1173      * for (int j = 0; j < mappings.length; j++) {
1174      * 
1175      * if (mappings[j].sequence == seq && mappings[j].getPdbId().equals(pdbid)
1176      * && mappings[j].pdbfile.equals(sl.getPdbFile())) {
1177      * System.out.println(pdbid+" "+mappings[j].getPdbId() +"
1178      * "+mappings[j].pdbfile);
1179      * 
1180      * java.awt.Color col; for(int index=0; index<seq.getLength(); index++) {
1181      * if(jalview.util.Comparison.isGap(seq.getCharAt(index))) continue;
1182      * 
1183      * atomNo = mappings[j].getAtomNum(seq.findPosition(index)); col =
1184      * java.awt.Color.white; if (atomNo > 0) { col = sl.getColour(atomNo,
1185      * mappings[j].getPDBResNum(index), mappings[j].pdbchain,
1186      * mappings[j].pdbfile); }
1187      * 
1188      * annotations[index] = new Annotation("X",null,' ',0,col); } return
1189      * annotations; } } } }
1190      * 
1191      * return annotations;
1192      */
1193   }
1194
1195   public void structureSelectionChanged()
1196   {
1197   }
1198
1199   public void sequenceSelectionChanged()
1200   {
1201   }
1202
1203   public void sequenceColoursChanged(Object source)
1204   {
1205     StructureListener sl;
1206     for (int i = 0; i < listeners.size(); i++)
1207     {
1208       if (listeners.elementAt(i) instanceof StructureListener)
1209       {
1210         sl = (StructureListener) listeners.elementAt(i);
1211         sl.updateColours(source);
1212       }
1213     }
1214   }
1215
1216   public StructureMapping[] getMapping(String pdbfile)
1217   {
1218     List<StructureMapping> tmp = new ArrayList<>();
1219     for (StructureMapping sm : mappings)
1220     {
1221       if (sm.pdbfile.equals(pdbfile))
1222       {
1223         tmp.add(sm);
1224       }
1225     }
1226     return tmp.toArray(new StructureMapping[tmp.size()]);
1227   }
1228
1229   /**
1230    * Returns a readable description of all mappings for the given pdbfile to any
1231    * of the given sequences
1232    * 
1233    * @param pdbfile
1234    * @param seqs
1235    * @return
1236    */
1237   public String printMappings(String pdbfile, List<SequenceI> seqs)
1238   {
1239     if (pdbfile == null || seqs == null || seqs.isEmpty())
1240     {
1241       return "";
1242     }
1243
1244     StringBuilder sb = new StringBuilder(64);
1245     for (StructureMapping sm : mappings)
1246     {
1247       if (Platform.pathEquals(sm.pdbfile, pdbfile)
1248               && seqs.contains(sm.sequence))
1249       {
1250         sb.append(sm.mappingDetails);
1251         sb.append(NEWLINE);
1252         // separator makes it easier to read multiple mappings
1253         sb.append("=====================");
1254         sb.append(NEWLINE);
1255       }
1256     }
1257     sb.append(NEWLINE);
1258
1259     return sb.toString();
1260   }
1261
1262   /**
1263    * Remove the given mapping
1264    * 
1265    * @param acf
1266    */
1267   public void deregisterMapping(AlignedCodonFrame acf)
1268   {
1269     if (acf != null)
1270     {
1271       boolean removed = seqmappings.remove(acf);
1272       if (removed && seqmappings.isEmpty())
1273       { // debug
1274         System.out.println("All mappings removed");
1275       }
1276     }
1277   }
1278
1279   /**
1280    * Add each of the given codonFrames to the stored set, if not aready present.
1281    * 
1282    * @param mappings
1283    */
1284   public void registerMappings(List<AlignedCodonFrame> mappings)
1285   {
1286     if (mappings != null)
1287     {
1288       for (AlignedCodonFrame acf : mappings)
1289       {
1290         registerMapping(acf);
1291       }
1292     }
1293   }
1294
1295   /**
1296    * Add the given mapping to the stored set, unless already stored.
1297    */
1298   public void registerMapping(AlignedCodonFrame acf)
1299   {
1300     if (acf != null)
1301     {
1302       if (!seqmappings.contains(acf))
1303       {
1304         seqmappings.add(acf);
1305       }
1306     }
1307   }
1308
1309   /**
1310    * Resets this object to its initial state by removing all registered
1311    * listeners, codon mappings, PDB file mappings
1312    */
1313   public void resetAll()
1314   {
1315     if (mappings != null)
1316     {
1317       mappings.clear();
1318     }
1319     if (seqmappings != null)
1320     {
1321       seqmappings.clear();
1322     }
1323     if (sel_listeners != null)
1324     {
1325       sel_listeners.clear();
1326     }
1327     if (listeners != null)
1328     {
1329       listeners.clear();
1330     }
1331     if (commandListeners != null)
1332     {
1333       commandListeners.clear();
1334     }
1335     if (view_listeners != null)
1336     {
1337       view_listeners.clear();
1338     }
1339     if (pdbFileNameId != null)
1340     {
1341       pdbFileNameId.clear();
1342     }
1343     if (pdbIdFileName != null)
1344     {
1345       pdbIdFileName.clear();
1346     }
1347   }
1348
1349   public void addSelectionListener(SelectionListener selecter)
1350   {
1351     if (!sel_listeners.contains(selecter))
1352     {
1353       sel_listeners.add(selecter);
1354     }
1355   }
1356
1357   public void removeSelectionListener(SelectionListener toremove)
1358   {
1359     if (sel_listeners.contains(toremove))
1360     {
1361       sel_listeners.remove(toremove);
1362     }
1363   }
1364
1365   public synchronized void sendSelection(
1366           jalview.datamodel.SequenceGroup selection,
1367           jalview.datamodel.ColumnSelection colsel, HiddenColumns hidden,
1368           SelectionSource source)
1369   {
1370     for (SelectionListener slis : sel_listeners)
1371     {
1372       if (slis != source)
1373       {
1374         slis.selection(selection, colsel, hidden, source);
1375       }
1376     }
1377   }
1378
1379   Vector<AlignmentViewPanelListener> view_listeners = new Vector<>();
1380
1381   public synchronized void sendViewPosition(
1382           jalview.api.AlignmentViewPanel source, int startRes, int endRes,
1383           int startSeq, int endSeq)
1384   {
1385
1386     if (view_listeners != null && view_listeners.size() > 0)
1387     {
1388       Enumeration<AlignmentViewPanelListener> listeners = view_listeners
1389               .elements();
1390       while (listeners.hasMoreElements())
1391       {
1392         AlignmentViewPanelListener slis = listeners.nextElement();
1393         if (slis != source)
1394         {
1395           slis.viewPosition(startRes, endRes, startSeq, endSeq, source);
1396         }
1397         ;
1398       }
1399     }
1400   }
1401
1402   /**
1403    * release all references associated with this manager provider
1404    * 
1405    * @param jalviewLite
1406    */
1407   public static void release(StructureSelectionManagerProvider jalviewLite)
1408   {
1409     // synchronized (instances)
1410     {
1411       if (instances == null)
1412       {
1413         return;
1414       }
1415       StructureSelectionManager mnger = (instances.get(jalviewLite));
1416       if (mnger != null)
1417       {
1418         instances.remove(jalviewLite);
1419         try
1420         {
1421           /* bsoares 2019-03-20 finalize deprecated, no apparent external
1422            * resources to close
1423            */
1424           // mnger.finalize();
1425         } catch (Throwable x)
1426         {
1427         }
1428       }
1429     }
1430   }
1431
1432   public void registerPDBEntry(PDBEntry pdbentry)
1433   {
1434     if (pdbentry.getFile() != null
1435             && pdbentry.getFile().trim().length() > 0)
1436     {
1437       registerPDBFile(pdbentry.getId(), pdbentry.getFile());
1438     }
1439   }
1440
1441   public void addCommandListener(CommandListener cl)
1442   {
1443     if (!commandListeners.contains(cl))
1444     {
1445       commandListeners.add(cl);
1446     }
1447   }
1448
1449   public boolean hasCommandListener(CommandListener cl)
1450   {
1451     return this.commandListeners.contains(cl);
1452   }
1453
1454   public boolean removeCommandListener(CommandListener l)
1455   {
1456     return commandListeners.remove(l);
1457   }
1458
1459   /**
1460    * Forward a command to any command listeners (except for the command's
1461    * source).
1462    * 
1463    * @param command
1464    *          the command to be broadcast (in its form after being performed)
1465    * @param undo
1466    *          if true, the command was being 'undone'
1467    * @param source
1468    */
1469   public void commandPerformed(CommandI command, boolean undo,
1470           VamsasSource source)
1471   {
1472     for (CommandListener listener : commandListeners)
1473     {
1474       listener.mirrorCommand(command, undo, this, source);
1475     }
1476   }
1477
1478   /**
1479    * Returns a new CommandI representing the given command as mapped to the
1480    * given sequences. If no mapping could be made, or the command is not of a
1481    * mappable kind, returns null.
1482    * 
1483    * @param command
1484    * @param undo
1485    * @param mapTo
1486    * @param gapChar
1487    * @return
1488    */
1489   public CommandI mapCommand(CommandI command, boolean undo,
1490           final AlignmentI mapTo, char gapChar)
1491   {
1492     if (command instanceof EditCommand)
1493     {
1494       return MappingUtils.mapEditCommand((EditCommand) command, undo, mapTo,
1495               gapChar, seqmappings);
1496     }
1497     else if (command instanceof OrderCommand)
1498     {
1499       return MappingUtils.mapOrderCommand((OrderCommand) command, undo,
1500               mapTo, seqmappings);
1501     }
1502     return null;
1503   }
1504
1505   public List<AlignedCodonFrame> getSequenceMappings()
1506   {
1507     return seqmappings;
1508   }
1509
1510 }