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