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