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