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