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