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