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