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