JAL-3949 Complete new abstracted logging framework in jalview.log. Updated log calls...
[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.Cache;
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(
382           boolean forStructureView, SequenceI[] sequenceArray,
383           String[] targetChainIds, 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         if (s.isProtein()) {
420           isProtein = true;
421           break;
422         }
423       }
424       isMapUsingSIFTs = isMapUsingSIFTs && pdb.isPPDBIdAvailable() && !pdb.getId().startsWith("AF-") && isProtein;
425
426     } catch (Exception ex)
427     {
428       ex.printStackTrace();
429       return null;
430     }
431     /*
432      * sifts client - non null if SIFTS mappings are to be used 
433      */
434     SiftsClient siftsClient = null;
435     try
436     {
437       if (isMapUsingSIFTs)
438       {
439         siftsClient = new SiftsClient(pdb);
440       }
441     } catch (SiftsException e)
442     {
443       isMapUsingSIFTs = false;
444       Cache.error("SIFTS mapping failed", e);
445       Cache.error("Falling back on Needleman & Wunsch alignment");
446       siftsClient = null;
447     }
448
449     String targetChainId;
450     for (int s = 0; s < sequenceArray.length; s++)
451     {
452       boolean infChain = true;
453       final SequenceI seq = sequenceArray[s];
454       SequenceI ds = seq;
455       while (ds.getDatasetSequence() != null)
456       {
457         ds = ds.getDatasetSequence();
458       }
459
460       if (targetChainIds != null && targetChainIds[s] != null)
461       {
462         infChain = false;
463         targetChainId = targetChainIds[s];
464       }
465       else if (seq.getName().indexOf("|") > -1)
466       {
467         targetChainId = seq.getName()
468                 .substring(seq.getName().lastIndexOf("|") + 1);
469         if (targetChainId.length() > 1)
470         {
471           if (targetChainId.trim().length() == 0)
472           {
473             targetChainId = " ";
474           }
475           else
476           {
477             // not a valid chain identifier
478             targetChainId = "";
479           }
480         }
481       }
482       else
483       {
484         targetChainId = "";
485       }
486
487       /*
488        * Attempt pairwise alignment of the sequence with each chain in the PDB,
489        * and remember the highest scoring chain
490        */
491       float max = -10;
492       AlignSeq maxAlignseq = null;
493       String maxChainId = " ";
494       PDBChain maxChain = null;
495       boolean first = true;
496       for (PDBChain chain : pdb.getChains())
497       {
498         if (targetChainId.length() > 0 && !targetChainId.equals(chain.id)
499                 && !infChain)
500         {
501           continue; // don't try to map chains don't match.
502         }
503         // TODO: correctly determine sequence type for mixed na/peptide
504         // structures
505         final String type = chain.isNa ? AlignSeq.DNA : AlignSeq.PEP;
506         AlignSeq as = AlignSeq.doGlobalNWAlignment(seq, chain.sequence,
507                 type);
508         // equivalent to:
509         // AlignSeq as = new AlignSeq(sequence[s], chain.sequence, type);
510         // as.calcScoreMatrix();
511         // as.traceAlignment();
512
513         if (first || as.maxscore > max
514                 || (as.maxscore == max && chain.id.equals(targetChainId)))
515         {
516           first = false;
517           maxChain = chain;
518           max = as.maxscore;
519           maxAlignseq = as;
520           maxChainId = chain.id;
521         }
522       }
523       if (maxChain == null)
524       {
525         continue;
526       }
527
528       if (sourceType == DataSourceType.PASTE)
529       {
530         pdbFile = "INLINE" + pdb.getId();
531       }
532
533       List<StructureMapping> seqToStrucMapping = new ArrayList<>();
534       if (isMapUsingSIFTs && seq.isProtein())
535       {
536         if (progress!=null) {
537           progress.setProgressBar(MessageManager
538                 .getString("status.obtaining_mapping_with_sifts"),
539                   progressSessionId);
540         }
541         jalview.datamodel.Mapping sqmpping = maxAlignseq
542                 .getMappingFromS1(false);
543         if (targetChainId != null && !targetChainId.trim().isEmpty())
544         {
545           StructureMapping siftsMapping;
546           try
547           {
548             siftsMapping = getStructureMapping(seq, pdbFile, targetChainId,
549                     pdb, maxChain, sqmpping, maxAlignseq, siftsClient);
550             seqToStrucMapping.add(siftsMapping);
551             maxChain.makeExactMapping(siftsMapping, seq);
552             maxChain.transferRESNUMFeatures(seq, "IEA: SIFTS",pdb.getId().toLowerCase(Locale.ROOT));
553             maxChain.transferResidueAnnotation(siftsMapping, null);
554             ds.addPDBId(maxChain.sequence.getAllPDBEntries().get(0));
555
556           } catch (SiftsException e)
557           {
558             // fall back to NW alignment
559             Cache.error(e.getMessage());
560             StructureMapping nwMapping = getNWMappings(seq, pdbFile,
561                     targetChainId, maxChain, pdb, maxAlignseq);
562             seqToStrucMapping.add(nwMapping);
563             maxChain.makeExactMapping(maxAlignseq, seq);
564             maxChain.transferRESNUMFeatures(seq, "IEA:Jalview",pdb.getId().toLowerCase(Locale.ROOT)); // FIXME: is
565                                                                  // this
566                                                         // "IEA:Jalview" ?
567             maxChain.transferResidueAnnotation(nwMapping, sqmpping);
568             ds.addPDBId(maxChain.sequence.getAllPDBEntries().get(0));
569           }
570         }
571         else
572         {
573           List<StructureMapping> foundSiftsMappings = new ArrayList<>();
574           for (PDBChain chain : pdb.getChains())
575           {
576             StructureMapping siftsMapping = null;
577             try
578             {
579               siftsMapping = getStructureMapping(seq,
580                       pdbFile, chain.id, pdb, chain, sqmpping, maxAlignseq,
581                       siftsClient);
582               foundSiftsMappings.add(siftsMapping);
583               chain.makeExactMapping(siftsMapping, seq);
584               chain.transferRESNUMFeatures(seq, "IEA: SIFTS",pdb.getId().toLowerCase(Locale.ROOT));// FIXME: is this
585               // "IEA:SIFTS" ?
586               chain.transferResidueAnnotation(siftsMapping, null);
587             } catch (SiftsException e)
588             {
589               System.err.println(e.getMessage());
590             }
591             catch (Exception e)
592             {
593               System.err
594                       .println(
595                               "Unexpected exception during SIFTS mapping - falling back to NW for this sequence/structure pair");
596               System.err.println(e.getMessage());
597             }
598           }
599           if (!foundSiftsMappings.isEmpty())
600           {
601             seqToStrucMapping.addAll(foundSiftsMappings);
602             ds.addPDBId(sqmpping.getTo().getAllPDBEntries().get(0));
603           }
604           else
605           {
606             StructureMapping nwMapping = getNWMappings(seq, pdbFile,
607                     maxChainId, maxChain, pdb, maxAlignseq);
608             seqToStrucMapping.add(nwMapping);
609             maxChain.transferRESNUMFeatures(seq, null,pdb.getId().toLowerCase(Locale.ROOT)); // FIXME: is this
610                                                         // "IEA:Jalview" ?
611             maxChain.transferResidueAnnotation(nwMapping, sqmpping);
612             ds.addPDBId(maxChain.sequence.getAllPDBEntries().get(0));
613           }
614         }
615       }
616       else
617       {
618         if (progress != null)
619         {
620           progress.setProgressBar(MessageManager
621                                   .getString("status.obtaining_mapping_with_nw_alignment"),
622                   progressSessionId);
623         }
624         StructureMapping nwMapping = getNWMappings(seq, pdbFile, maxChainId,
625                 maxChain, pdb, maxAlignseq);
626         seqToStrucMapping.add(nwMapping);
627         ds.addPDBId(maxChain.sequence.getAllPDBEntries().get(0));
628       }
629       if (forStructureView)
630       {
631         for (StructureMapping sm : seqToStrucMapping)
632         {
633           addStructureMapping(sm); // not addAll!
634         }
635       }
636       if (progress != null)
637       {
638         progress.setProgressBar(null, progressSessionId);
639       }
640     }
641     return pdb;
642   }
643
644   /**
645    * check if we need to extract secondary structure from given pdbFile and
646    * transfer to sequences
647    * 
648    * @param pdbFile
649    * @param sequenceArray
650    * @return
651    */
652   private boolean isStructureFileProcessed(String pdbFile,
653           SequenceI[] sequenceArray)
654   {
655     boolean parseSecStr = true;
656     if (isPDBFileRegistered(pdbFile))
657     {
658       for (SequenceI sq : sequenceArray)
659       {
660         SequenceI ds = sq;
661         while (ds.getDatasetSequence() != null)
662         {
663           ds = ds.getDatasetSequence();
664         }
665         ;
666         if (ds.getAnnotation() != null)
667         {
668           for (AlignmentAnnotation ala : ds.getAnnotation())
669           {
670             // false if any annotation present from this structure
671             // JBPNote this fails for jmol/chimera view because the *file* is
672             // passed, not the structure data ID -
673             if (PDBfile.isCalcIdForFile(ala, findIdForPDBFile(pdbFile)))
674             {
675               parseSecStr = false;
676             }
677           }
678         }
679       }
680     }
681     return parseSecStr;
682   }
683
684   public void addStructureMapping(StructureMapping sm)
685   {
686     if (!mappings.contains(sm))
687     {
688       mappings.add(sm);
689     }
690   }
691
692   /**
693    * retrieve a mapping for seq from SIFTs using associated DBRefEntry for
694    * uniprot or PDB
695    * 
696    * @param seq
697    * @param pdbFile
698    * @param targetChainId
699    * @param pdb
700    * @param maxChain
701    * @param sqmpping
702    * @param maxAlignseq
703    * @param siftsClient
704    *          client for retrieval of SIFTS mappings for this structure
705    * @return
706    * @throws SiftsException
707    */
708   private StructureMapping getStructureMapping(SequenceI seq,
709           String pdbFile, String targetChainId, StructureFile pdb,
710           PDBChain maxChain, jalview.datamodel.Mapping sqmpping,
711           AlignSeq maxAlignseq, SiftsClient siftsClient) throws SiftsException
712   {
713     StructureMapping curChainMapping = siftsClient
714             .getSiftsStructureMapping(seq, pdbFile, targetChainId);
715     try
716     {
717       PDBChain chain = pdb.findChain(targetChainId);
718       if (chain != null)
719       {
720         chain.transferResidueAnnotation(curChainMapping, null);
721       }
722     } catch (Exception e)
723     {
724       e.printStackTrace();
725     }
726     return curChainMapping;
727   }
728
729   private StructureMapping getNWMappings(SequenceI seq, String pdbFile,
730           String maxChainId, PDBChain maxChain, StructureFile pdb,
731           AlignSeq maxAlignseq)
732   {
733     final StringBuilder mappingDetails = new StringBuilder(128);
734     mappingDetails.append(NEWLINE)
735             .append("Sequence \u27f7 Structure mapping details");
736     mappingDetails.append(NEWLINE);
737     mappingDetails
738             .append("Method: inferred with Needleman & Wunsch alignment");
739     mappingDetails.append(NEWLINE).append("PDB Sequence is :")
740             .append(NEWLINE).append("Sequence = ")
741             .append(maxChain.sequence.getSequenceAsString());
742     mappingDetails.append(NEWLINE).append("No of residues = ")
743             .append(maxChain.residues.size()).append(NEWLINE)
744             .append(NEWLINE);
745     PrintStream ps = new PrintStream(System.out)
746     {
747       @Override
748       public void print(String x)
749       {
750         mappingDetails.append(x);
751       }
752
753       @Override
754       public void println()
755       {
756         mappingDetails.append(NEWLINE);
757       }
758     };
759
760     maxAlignseq.printAlignment(ps);
761
762     mappingDetails.append(NEWLINE).append("PDB start/end ");
763     mappingDetails.append(String.valueOf(maxAlignseq.seq2start))
764             .append(" ");
765     mappingDetails.append(String.valueOf(maxAlignseq.seq2end));
766     mappingDetails.append(NEWLINE).append("SEQ start/end ");
767     mappingDetails
768             .append(String
769                     .valueOf(maxAlignseq.seq1start + (seq.getStart() - 1)))
770             .append(" ");
771     mappingDetails.append(
772             String.valueOf(maxAlignseq.seq1end + (seq.getStart() - 1)));
773     mappingDetails.append(NEWLINE);
774     maxChain.makeExactMapping(maxAlignseq, seq);
775     jalview.datamodel.Mapping sqmpping = maxAlignseq
776             .getMappingFromS1(false);
777     maxChain.transferRESNUMFeatures(seq, null, pdb.getId().toLowerCase(Locale.ROOT));
778
779     HashMap<Integer, int[]> mapping = new HashMap<>();
780     int resNum = -10000;
781     int index = 0;
782     char insCode = ' ';
783
784     do
785     {
786       Atom tmp = maxChain.atoms.elementAt(index);
787       if ((resNum != tmp.resNumber || insCode != tmp.insCode)
788               && tmp.alignmentMapping != -1)
789       {
790         resNum = tmp.resNumber;
791         insCode = tmp.insCode;
792         if (tmp.alignmentMapping >= -1)
793         {
794           mapping.put(tmp.alignmentMapping + 1,
795                   new int[]
796                   { tmp.resNumber, tmp.atomIndex });
797         }
798       }
799
800       index++;
801     } while (index < maxChain.atoms.size());
802
803     StructureMapping nwMapping = new StructureMapping(seq, pdbFile,
804             pdb.getId(), maxChainId, mapping, mappingDetails.toString());
805     maxChain.transferResidueAnnotation(nwMapping, sqmpping);
806     return nwMapping;
807   }
808
809   public void removeStructureViewerListener(Object svl, String[] pdbfiles)
810   {
811     listeners.removeElement(svl);
812     if (svl instanceof SequenceListener)
813     {
814       for (int i = 0; i < listeners.size(); i++)
815       {
816         if (listeners.elementAt(i) instanceof StructureListener)
817         {
818           ((StructureListener) listeners.elementAt(i))
819                   .releaseReferences(svl);
820         }
821       }
822     }
823
824     if (pdbfiles == null)
825     {
826       return;
827     }
828
829     /*
830      * Remove mappings to the closed listener's PDB files, but first check if
831      * another listener is still interested
832      */
833     List<String> pdbs = new ArrayList<>(Arrays.asList(pdbfiles));
834
835     StructureListener sl;
836     for (int i = 0; i < listeners.size(); i++)
837     {
838       if (listeners.elementAt(i) instanceof StructureListener)
839       {
840         sl = (StructureListener) listeners.elementAt(i);
841         for (String pdbfile : sl.getStructureFiles())
842         {
843           pdbs.remove(pdbfile);
844         }
845       }
846     }
847
848     /*
849      * Rebuild the mappings set, retaining only those which are for 'other' PDB
850      * files
851      */
852     if (pdbs.size() > 0)
853     {
854       List<StructureMapping> tmp = new ArrayList<>();
855       for (StructureMapping sm : mappings)
856       {
857         if (!pdbs.contains(sm.pdbfile))
858         {
859           tmp.add(sm);
860         }
861       }
862
863       mappings = tmp;
864     }
865   }
866
867   /**
868    * Propagate mouseover of a single position in a structure
869    * 
870    * @param pdbResNum
871    * @param chain
872    * @param pdbfile
873    * @return
874    */
875   public String mouseOverStructure(int pdbResNum, String chain,
876           String pdbfile)
877   {
878     AtomSpec atomSpec = new AtomSpec(pdbfile, chain, pdbResNum, 0);
879     List<AtomSpec> atoms = Collections.singletonList(atomSpec);
880     return mouseOverStructure(atoms);
881   }
882
883   /**
884    * Propagate mouseover or selection of multiple positions in a structure
885    * 
886    * @param atoms
887    */
888   public String mouseOverStructure(List<AtomSpec> atoms)
889   {
890     if (listeners == null)
891     {
892       // old or prematurely sent event
893       return null;
894     }
895     boolean hasSequenceListener = false;
896     for (int i = 0; i < listeners.size(); i++)
897     {
898       if (listeners.elementAt(i) instanceof SequenceListener)
899       {
900         hasSequenceListener = true;
901       }
902     }
903     if (!hasSequenceListener)
904     {
905       return null;
906     }
907
908     SearchResultsI results = findAlignmentPositionsForStructurePositions(
909             atoms);
910     String result = null;
911     for (Object li : listeners)
912     {
913       if (li instanceof SequenceListener)
914       {
915         String s = ((SequenceListener) li).highlightSequence(results);
916         if (s != null)
917         {
918           result = s;
919         }
920       }
921     }
922     return result;
923   }
924
925   /**
926    * Constructs a SearchResults object holding regions (if any) in the Jalview
927    * alignment which have a mapping to the structure viewer positions in the
928    * supplied list
929    * 
930    * @param atoms
931    * @return
932    */
933   public SearchResultsI findAlignmentPositionsForStructurePositions(
934           List<AtomSpec> atoms)
935   {
936     SearchResultsI results = new SearchResults();
937     for (AtomSpec atom : atoms)
938     {
939       SequenceI lastseq = null;
940       int lastipos = -1;
941       for (StructureMapping sm : mappings)
942       {
943         if (sm.pdbfile.equals(atom.getPdbFile())
944                 && sm.pdbchain.equals(atom.getChain()))
945         {
946           int indexpos = sm.getSeqPos(atom.getPdbResNum());
947           if (lastipos != indexpos || lastseq != sm.sequence)
948           {
949             results.addResult(sm.sequence, indexpos, indexpos);
950             lastipos = indexpos;
951             lastseq = sm.sequence;
952             // construct highlighted sequence list
953             for (AlignedCodonFrame acf : seqmappings)
954             {
955               acf.markMappedRegion(sm.sequence, indexpos, results);
956             }
957           }
958         }
959       }
960     }
961     return results;
962   }
963
964   /**
965    * highlight regions associated with a position (indexpos) in seq
966    * 
967    * @param seq
968    *          the sequence that the mouse over occurred on
969    * @param indexpos
970    *          the absolute position being mouseovered in seq (0 to seq.length())
971    * @param seqPos
972    *          the sequence position (if -1, seq.findPosition is called to
973    *          resolve the residue number)
974    */
975   public void mouseOverSequence(SequenceI seq, int indexpos, int seqPos,
976           VamsasSource source)
977   {
978     boolean hasSequenceListeners = handlingVamsasMo
979             || !seqmappings.isEmpty();
980     SearchResultsI results = null;
981     if (seqPos == -1)
982     {
983       seqPos = seq.findPosition(indexpos);
984     }
985     for (int i = 0; i < listeners.size(); i++)
986     {
987       Object listener = listeners.elementAt(i);
988       if (listener == source)
989       {
990         // TODO listener (e.g. SeqPanel) is never == source (AlignViewport)
991         // Temporary fudge with SequenceListener.getVamsasSource()
992         continue;
993       }
994       if (listener instanceof StructureListener)
995       {
996         highlightStructure((StructureListener) listener, seq, seqPos);
997       }
998       else
999       {
1000         if (listener instanceof SequenceListener)
1001         {
1002           final SequenceListener seqListener = (SequenceListener) listener;
1003           if (hasSequenceListeners
1004                   && seqListener.getVamsasSource() != source)
1005           {
1006             if (relaySeqMappings)
1007             {
1008               if (results == null)
1009               {
1010                 results = MappingUtils.buildSearchResults(seq, seqPos,
1011                         seqmappings);
1012               }
1013               if (handlingVamsasMo)
1014               {
1015                 results.addResult(seq, seqPos, seqPos);
1016
1017               }
1018               if (!results.isEmpty())
1019               {
1020                 seqListener.highlightSequence(results);
1021               }
1022             }
1023           }
1024         }
1025         else if (listener instanceof VamsasListener && !handlingVamsasMo)
1026         {
1027           ((VamsasListener) listener).mouseOverSequence(seq, indexpos,
1028                   source);
1029         }
1030         else if (listener instanceof SecondaryStructureListener)
1031         {
1032           ((SecondaryStructureListener) listener).mouseOverSequence(seq,
1033                   indexpos, seqPos);
1034         }
1035       }
1036     }
1037   }
1038
1039   /**
1040    * Send suitable messages to a StructureListener to highlight atoms
1041    * corresponding to the given sequence position(s)
1042    * 
1043    * @param sl
1044    * @param seq
1045    * @param positions
1046    */
1047   public void highlightStructure(StructureListener sl, SequenceI seq,
1048           int... positions)
1049   {
1050     if (!sl.isListeningFor(seq))
1051     {
1052       return;
1053     }
1054     int atomNo;
1055     List<AtomSpec> atoms = new ArrayList<>();
1056     for (StructureMapping sm : mappings)
1057     {
1058       if (sm.sequence == seq || sm.sequence == seq.getDatasetSequence()
1059               || (sm.sequence.getDatasetSequence() != null && sm.sequence
1060                       .getDatasetSequence() == seq.getDatasetSequence()))
1061       {
1062         for (int index : positions)
1063         {
1064           atomNo = sm.getAtomNum(index);
1065
1066           if (atomNo > 0)
1067           {
1068             atoms.add(new AtomSpec(sm.pdbfile, sm.pdbchain,
1069                     sm.getPDBResNum(index), atomNo));
1070           }
1071         }
1072       }
1073     }
1074     sl.highlightAtoms(atoms);
1075   }
1076
1077   /**
1078    * true if a mouse over event from an external (ie Vamsas) source is being
1079    * handled
1080    */
1081   boolean handlingVamsasMo = false;
1082
1083   long lastmsg = 0;
1084
1085   /**
1086    * as mouseOverSequence but only route event to SequenceListeners
1087    * 
1088    * @param sequenceI
1089    * @param position
1090    *          in an alignment sequence
1091    */
1092   public void mouseOverVamsasSequence(SequenceI sequenceI, int position,
1093           VamsasSource source)
1094   {
1095     handlingVamsasMo = true;
1096     long msg = sequenceI.hashCode() * (1 + position);
1097     if (lastmsg != msg)
1098     {
1099       lastmsg = msg;
1100       mouseOverSequence(sequenceI, position, -1, source);
1101     }
1102     handlingVamsasMo = false;
1103   }
1104
1105   public Annotation[] colourSequenceFromStructure(SequenceI seq,
1106           String pdbid)
1107   {
1108     return null;
1109     // THIS WILL NOT BE AVAILABLE IN JALVIEW 2.3,
1110     // UNTIL THE COLOUR BY ANNOTATION IS REWORKED
1111     /*
1112      * Annotation [] annotations = new Annotation[seq.getLength()];
1113      * 
1114      * StructureListener sl; int atomNo = 0; for (int i = 0; i <
1115      * listeners.size(); i++) { if (listeners.elementAt(i) instanceof
1116      * StructureListener) { sl = (StructureListener) listeners.elementAt(i);
1117      * 
1118      * for (int j = 0; j < mappings.length; j++) {
1119      * 
1120      * if (mappings[j].sequence == seq && mappings[j].getPdbId().equals(pdbid)
1121      * && mappings[j].pdbfile.equals(sl.getPdbFile())) {
1122      * System.out.println(pdbid+" "+mappings[j].getPdbId() +"
1123      * "+mappings[j].pdbfile);
1124      * 
1125      * java.awt.Color col; for(int index=0; index<seq.getLength(); index++) {
1126      * if(jalview.util.Comparison.isGap(seq.getCharAt(index))) continue;
1127      * 
1128      * atomNo = mappings[j].getAtomNum(seq.findPosition(index)); col =
1129      * java.awt.Color.white; if (atomNo > 0) { col = sl.getColour(atomNo,
1130      * mappings[j].getPDBResNum(index), mappings[j].pdbchain,
1131      * mappings[j].pdbfile); }
1132      * 
1133      * annotations[index] = new Annotation("X",null,' ',0,col); } return
1134      * annotations; } } } }
1135      * 
1136      * return annotations;
1137      */
1138   }
1139
1140   public void structureSelectionChanged()
1141   {
1142   }
1143
1144   public void sequenceSelectionChanged()
1145   {
1146   }
1147
1148   public void sequenceColoursChanged(Object source)
1149   {
1150     StructureListener sl;
1151     for (int i = 0; i < listeners.size(); i++)
1152     {
1153       if (listeners.elementAt(i) instanceof StructureListener)
1154       {
1155         sl = (StructureListener) listeners.elementAt(i);
1156         sl.updateColours(source);
1157       }
1158     }
1159   }
1160
1161   public StructureMapping[] getMapping(String pdbfile)
1162   {
1163     List<StructureMapping> tmp = new ArrayList<>();
1164     for (StructureMapping sm : mappings)
1165     {
1166       if (sm.pdbfile.equals(pdbfile))
1167       {
1168         tmp.add(sm);
1169       }
1170     }
1171     return tmp.toArray(new StructureMapping[tmp.size()]);
1172   }
1173
1174   /**
1175    * Returns a readable description of all mappings for the given pdbfile to any
1176    * of the given sequences
1177    * 
1178    * @param pdbfile
1179    * @param seqs
1180    * @return
1181    */
1182   public String printMappings(String pdbfile, List<SequenceI> seqs)
1183   {
1184     if (pdbfile == null || seqs == null || seqs.isEmpty())
1185     {
1186       return "";
1187     }
1188
1189     StringBuilder sb = new StringBuilder(64);
1190     for (StructureMapping sm : mappings)
1191     {
1192       if (Platform.pathEquals(sm.pdbfile, pdbfile)
1193               && seqs.contains(sm.sequence))
1194       {
1195         sb.append(sm.mappingDetails);
1196         sb.append(NEWLINE);
1197         // separator makes it easier to read multiple mappings
1198         sb.append("=====================");
1199         sb.append(NEWLINE);
1200       }
1201     }
1202     sb.append(NEWLINE);
1203
1204     return sb.toString();
1205   }
1206
1207   /**
1208    * Remove the given mapping
1209    * 
1210    * @param acf
1211    */
1212   public void deregisterMapping(AlignedCodonFrame acf)
1213   {
1214     if (acf != null)
1215     {
1216       boolean removed = seqmappings.remove(acf);
1217       if (removed && seqmappings.isEmpty())
1218       { // debug
1219         System.out.println("All mappings removed");
1220       }
1221     }
1222   }
1223
1224   /**
1225    * Add each of the given codonFrames to the stored set, if not aready present.
1226    * 
1227    * @param mappings
1228    */
1229   public void registerMappings(List<AlignedCodonFrame> mappings)
1230   {
1231     if (mappings != null)
1232     {
1233       for (AlignedCodonFrame acf : mappings)
1234       {
1235         registerMapping(acf);
1236       }
1237     }
1238   }
1239
1240   /**
1241    * Add the given mapping to the stored set, unless already stored.
1242    */
1243   public void registerMapping(AlignedCodonFrame acf)
1244   {
1245     if (acf != null)
1246     {
1247       if (!seqmappings.contains(acf))
1248       {
1249         seqmappings.add(acf);
1250       }
1251     }
1252   }
1253
1254   /**
1255    * Resets this object to its initial state by removing all registered
1256    * listeners, codon mappings, PDB file mappings
1257    */
1258   public void resetAll()
1259   {
1260     if (mappings != null)
1261     {
1262       mappings.clear();
1263     }
1264     if (seqmappings != null)
1265     {
1266       seqmappings.clear();
1267     }
1268     if (sel_listeners != null)
1269     {
1270       sel_listeners.clear();
1271     }
1272     if (listeners != null)
1273     {
1274       listeners.clear();
1275     }
1276     if (commandListeners != null)
1277     {
1278       commandListeners.clear();
1279     }
1280     if (view_listeners != null)
1281     {
1282       view_listeners.clear();
1283     }
1284     if (pdbFileNameId != null)
1285     {
1286       pdbFileNameId.clear();
1287     }
1288     if (pdbIdFileName != null)
1289     {
1290       pdbIdFileName.clear();
1291     }
1292   }
1293
1294   public void addSelectionListener(SelectionListener selecter)
1295   {
1296     if (!sel_listeners.contains(selecter))
1297     {
1298       sel_listeners.add(selecter);
1299     }
1300   }
1301
1302   public void removeSelectionListener(SelectionListener toremove)
1303   {
1304     if (sel_listeners.contains(toremove))
1305     {
1306       sel_listeners.remove(toremove);
1307     }
1308   }
1309
1310   public synchronized void sendSelection(
1311           jalview.datamodel.SequenceGroup selection,
1312           jalview.datamodel.ColumnSelection colsel, HiddenColumns hidden,
1313           SelectionSource source)
1314   {
1315     for (SelectionListener slis : sel_listeners)
1316     {
1317       if (slis != source)
1318       {
1319         slis.selection(selection, colsel, hidden, source);
1320       }
1321     }
1322   }
1323
1324   Vector<AlignmentViewPanelListener> view_listeners = new Vector<>();
1325
1326   public synchronized void sendViewPosition(
1327           jalview.api.AlignmentViewPanel source, int startRes, int endRes,
1328           int startSeq, int endSeq)
1329   {
1330
1331     if (view_listeners != null && view_listeners.size() > 0)
1332     {
1333       Enumeration<AlignmentViewPanelListener> listeners = view_listeners
1334               .elements();
1335       while (listeners.hasMoreElements())
1336       {
1337         AlignmentViewPanelListener slis = listeners.nextElement();
1338         if (slis != source)
1339         {
1340           slis.viewPosition(startRes, endRes, startSeq, endSeq, source);
1341         }
1342         ;
1343       }
1344     }
1345   }
1346
1347   /**
1348    * release all references associated with this manager provider
1349    * 
1350    * @param jalviewLite
1351    */
1352   public static void release(StructureSelectionManagerProvider jalviewLite)
1353   {
1354     // synchronized (instances)
1355     {
1356       if (instances == null)
1357       {
1358         return;
1359       }
1360       StructureSelectionManager mnger = (instances.get(jalviewLite));
1361       if (mnger != null)
1362       {
1363         instances.remove(jalviewLite);
1364         try
1365         {
1366           /* bsoares 2019-03-20 finalize deprecated, no apparent external
1367            * resources to close
1368            */
1369           // mnger.finalize();
1370         } catch (Throwable x)
1371         {
1372         }
1373       }
1374     }
1375   }
1376
1377   public void registerPDBEntry(PDBEntry pdbentry)
1378   {
1379     if (pdbentry.getFile() != null
1380             && pdbentry.getFile().trim().length() > 0)
1381     {
1382       registerPDBFile(pdbentry.getId(), pdbentry.getFile());
1383     }
1384   }
1385
1386   public void addCommandListener(CommandListener cl)
1387   {
1388     if (!commandListeners.contains(cl))
1389     {
1390       commandListeners.add(cl);
1391     }
1392   }
1393
1394   public boolean hasCommandListener(CommandListener cl)
1395   {
1396     return this.commandListeners.contains(cl);
1397   }
1398
1399   public boolean removeCommandListener(CommandListener l)
1400   {
1401     return commandListeners.remove(l);
1402   }
1403
1404   /**
1405    * Forward a command to any command listeners (except for the command's
1406    * source).
1407    * 
1408    * @param command
1409    *          the command to be broadcast (in its form after being performed)
1410    * @param undo
1411    *          if true, the command was being 'undone'
1412    * @param source
1413    */
1414   public void commandPerformed(CommandI command, boolean undo,
1415           VamsasSource source)
1416   {
1417     for (CommandListener listener : commandListeners)
1418     {
1419       listener.mirrorCommand(command, undo, this, source);
1420     }
1421   }
1422
1423   /**
1424    * Returns a new CommandI representing the given command as mapped to the
1425    * given sequences. If no mapping could be made, or the command is not of a
1426    * mappable kind, returns null.
1427    * 
1428    * @param command
1429    * @param undo
1430    * @param mapTo
1431    * @param gapChar
1432    * @return
1433    */
1434   public CommandI mapCommand(CommandI command, boolean undo,
1435           final AlignmentI mapTo, char gapChar)
1436   {
1437     if (command instanceof EditCommand)
1438     {
1439       return MappingUtils.mapEditCommand((EditCommand) command, undo, mapTo,
1440               gapChar, seqmappings);
1441     }
1442     else if (command instanceof OrderCommand)
1443     {
1444       return MappingUtils.mapOrderCommand((OrderCommand) command, undo,
1445               mapTo, seqmappings);
1446     }
1447     return null;
1448   }
1449
1450   public List<AlignedCodonFrame> getSequenceMappings()
1451   {
1452     return seqmappings;
1453   }
1454
1455 }