6fce9847e02af1843fdfa933bb9e0f93a38610b3
[jalview.git] / src / jalview / gui / StructureChooser.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
22 package jalview.gui;
23
24 import java.awt.event.ActionEvent;
25 import java.awt.event.ActionListener;
26 import java.awt.event.ItemEvent;
27 import java.io.File;
28 import java.util.ArrayList;
29 import java.util.Collection;
30 import java.util.HashSet;
31 import java.util.LinkedHashMap;
32 import java.util.LinkedHashSet;
33 import java.util.List;
34 import java.util.Locale;
35 import java.util.Map;
36 import java.util.concurrent.Executors;
37
38 import javax.swing.JCheckBox;
39 import javax.swing.JComboBox;
40 import javax.swing.JLabel;
41 import javax.swing.JMenuItem;
42 import javax.swing.JPopupMenu;
43 import javax.swing.JTable;
44 import javax.swing.SwingUtilities;
45 import javax.swing.table.AbstractTableModel;
46
47 import com.stevesoft.pat.Regex;
48
49 import jalview.analysis.AlignmentUtils;
50 import jalview.api.AlignmentViewPanel;
51 import jalview.api.structures.JalviewStructureDisplayI;
52 import jalview.bin.Cache;
53 import jalview.bin.Console;
54 import jalview.bin.Jalview;
55 import jalview.datamodel.AlignmentAnnotation;
56 import jalview.datamodel.AlignmentI;
57 import jalview.datamodel.PDBEntry;
58 import jalview.datamodel.SequenceGroup;
59 import jalview.datamodel.SequenceI;
60 import jalview.ext.jmol.JmolParser;
61 import jalview.fts.api.FTSData;
62 import jalview.fts.api.FTSDataColumnI;
63 import jalview.fts.api.FTSRestClientI;
64 import jalview.fts.core.FTSDataColumnPreferences;
65 import jalview.fts.core.FTSRestRequest;
66 import jalview.fts.core.FTSRestResponse;
67 import jalview.fts.service.pdb.PDBFTSRestClient;
68 import jalview.fts.service.threedbeacons.TDB_FTSData;
69 import jalview.gui.StructureViewer.ViewerType;
70 import jalview.gui.structurechooser.PDBStructureChooserQuerySource;
71 import jalview.gui.structurechooser.StructureChooserQuerySource;
72 import jalview.gui.structurechooser.ThreeDBStructureChooserQuerySource;
73 import jalview.io.DataSourceType;
74 import jalview.io.FileFormatException;
75 import jalview.io.JalviewFileChooser;
76 import jalview.io.JalviewFileView;
77 import jalview.jbgui.FilterOption;
78 import jalview.jbgui.GStructureChooser;
79 import jalview.structure.StructureImportSettings.TFType;
80 import jalview.structure.StructureMapping;
81 import jalview.structure.StructureSelectionManager;
82 import jalview.util.MessageManager;
83 import jalview.util.Platform;
84 import jalview.util.StringUtils;
85 import jalview.ws.DBRefFetcher;
86 import jalview.ws.DBRefFetcher.FetchFinishedListenerI;
87 import jalview.ws.datamodel.alphafold.PAEContactMatrix;
88 import jalview.ws.dbsources.EBIAlfaFold;
89 import jalview.ws.seqfetcher.DbSourceProxy;
90 import jalview.ws.sifts.SiftsSettings;
91
92 /**
93  * Provides the behaviors for the Structure chooser Panel
94  * 
95  * @author tcnofoegbu
96  *
97  */
98 @SuppressWarnings("serial")
99 public class StructureChooser extends GStructureChooser
100         implements IProgressIndicator
101 {
102   private static final String AUTOSUPERIMPOSE = "AUTOSUPERIMPOSE";
103
104   /**
105    * warn user if need to fetch more than this many uniprot records at once
106    */
107   private static final int THRESHOLD_WARN_UNIPROT_FETCH_NEEDED = 20;
108
109   private SequenceI selectedSequence;
110
111   private SequenceI[] selectedSequences;
112
113   private IProgressIndicator progressIndicator;
114
115   private Collection<FTSData> discoveredStructuresSet;
116
117   private StructureChooserQuerySource data;
118
119   @Override
120   protected FTSDataColumnPreferences getFTSDocFieldPrefs()
121   {
122     return data.getDocFieldPrefs();
123   }
124
125   private String selectedPdbFileName;
126
127   private TFType localPdbTempfacType;
128
129   private String localPdbPaeMatrixFileName;
130
131   private boolean isValidPBDEntry;
132
133   private boolean cachedPDBExists;
134
135   private Collection<FTSData> lastDiscoveredStructuresSet;
136
137   private boolean canQueryTDB = false;
138
139   private boolean notQueriedTDBYet = true;
140
141   List<SequenceI> seqsWithoutSourceDBRef = null;
142
143   private boolean showChooserGUI = true;
144
145   private static StructureViewer lastTargetedView = null;
146
147   public StructureChooser(SequenceI[] selectedSeqs, SequenceI selectedSeq,
148           AlignmentPanel ap)
149   {
150     this(selectedSeqs, selectedSeq, ap, true);
151   }
152
153   public StructureChooser(SequenceI[] selectedSeqs, SequenceI selectedSeq,
154           AlignmentPanel ap, boolean showGUI)
155   {
156     // which FTS engine to use
157     data = StructureChooserQuerySource.getQuerySourceFor(selectedSeqs);
158     initDialog();
159
160     this.ap = ap;
161     this.selectedSequence = selectedSeq;
162     this.selectedSequences = selectedSeqs;
163     this.progressIndicator = (ap == null) ? null : ap.alignFrame;
164     this.showChooserGUI = showGUI;
165     init();
166
167   }
168
169   /**
170    * sets canQueryTDB if protein sequences without a canonical uniprot ref or at
171    * least one structure are discovered.
172    */
173   private void populateSeqsWithoutSourceDBRef()
174   {
175     seqsWithoutSourceDBRef = new ArrayList<SequenceI>();
176     boolean needCanonical = false;
177     for (SequenceI seq : selectedSequences)
178     {
179       if (seq.isProtein())
180       {
181         int dbRef = ThreeDBStructureChooserQuerySource
182                 .checkUniprotRefs(seq.getDBRefs());
183         if (dbRef < 0)
184         {
185           if (dbRef == -1)
186           {
187             // need to retrieve canonicals
188             needCanonical = true;
189             seqsWithoutSourceDBRef.add(seq);
190           }
191           else
192           {
193             // could be a sequence with pdb ref
194             if (seq.getAllPDBEntries() == null
195                     || seq.getAllPDBEntries().size() == 0)
196             {
197               seqsWithoutSourceDBRef.add(seq);
198             }
199           }
200         }
201       }
202     }
203     // retrieve database refs for protein sequences
204     if (!seqsWithoutSourceDBRef.isEmpty())
205     {
206       canQueryTDB = true;
207       if (needCanonical)
208       {
209         // triggers display of the 'Query TDB' button
210         notQueriedTDBYet = true;
211       }
212     }
213   };
214
215   /**
216    * Initializes parameters used by the Structure Chooser Panel
217    */
218   protected void init()
219   {
220     if (!Jalview.isHeadlessMode())
221     {
222       progressBar = new ProgressBar(this.statusPanel, this.statusBar);
223     }
224
225     chk_superpose.setSelected(Cache.getDefault(AUTOSUPERIMPOSE, true));
226     btn_queryTDB.addActionListener(new ActionListener()
227     {
228
229       @Override
230       public void actionPerformed(ActionEvent e)
231       {
232         promptForTDBFetch(false);
233       }
234     });
235
236     Executors.defaultThreadFactory().newThread(new Runnable()
237     {
238       @Override
239       public void run()
240       {
241         populateSeqsWithoutSourceDBRef();
242         initialStructureDiscovery();
243       }
244
245     }).start();
246
247   }
248
249   // called by init
250   private void initialStructureDiscovery()
251   {
252     // check which FTS engine to use
253     data = StructureChooserQuerySource.getQuerySourceFor(selectedSequences);
254
255     // ensure a filter option is in force for search
256     populateFilterComboBox(true, cachedPDBExists);
257
258     // looks for any existing structures already loaded
259     // for the sequences (the cached ones)
260     // then queries the StructureChooserQuerySource to
261     // discover more structures.
262     //
263     // Possible optimisation is to only begin querying
264     // the structure chooser if there are no cached structures.
265
266     long startTime = System.currentTimeMillis();
267     updateProgressIndicator(
268             MessageManager.getString("status.loading_cached_pdb_entries"),
269             startTime);
270     loadLocalCachedPDBEntries();
271     updateProgressIndicator(null, startTime);
272     updateProgressIndicator(
273             MessageManager.getString("status.searching_for_pdb_structures"),
274             startTime);
275     fetchStructuresMetaData();
276     // revise filter options if no results were found
277     populateFilterComboBox(isStructuresDiscovered(), cachedPDBExists);
278     discoverStructureViews();
279     updateProgressIndicator(null, startTime);
280     mainFrame.setVisible(showChooserGUI);
281     updateCurrentView();
282   }
283
284   /**
285    * raises dialog for Uniprot fetch followed by 3D beacons search
286    * 
287    * @param ignoreGui
288    *          - when true, don't ask, just fetch
289    */
290   public void promptForTDBFetch(boolean ignoreGui)
291   {
292     final long progressId = System.currentTimeMillis();
293
294     // final action after prompting and discovering db refs
295     final Runnable strucDiscovery = new Runnable()
296     {
297       @Override
298       public void run()
299       {
300         mainFrame.setEnabled(false);
301         cmb_filterOption.setEnabled(false);
302         progressBar.setProgressBar(
303                 MessageManager.getString("status.searching_3d_beacons"),
304                 progressId);
305         btn_queryTDB.setEnabled(false);
306         // TODO: warn if no accessions discovered
307         populateSeqsWithoutSourceDBRef();
308         // redo initial discovery - this time with 3d beacons
309         // Executors.
310         previousWantedFields = null;
311         lastSelected = (FilterOption) cmb_filterOption.getSelectedItem();
312         cmb_filterOption.setSelectedItem(null);
313         cachedPDBExists = false; // reset to initial
314         initialStructureDiscovery();
315         if (!isStructuresDiscovered())
316         {
317           progressBar.setProgressBar(MessageManager.getString(
318                   "status.no_structures_discovered_from_3d_beacons"),
319                   progressId);
320           btn_queryTDB.setToolTipText(MessageManager.getString(
321                   "status.no_structures_discovered_from_3d_beacons"));
322           btn_queryTDB.setEnabled(false);
323           pnl_queryTDB.setVisible(false);
324         }
325         else
326         {
327           cmb_filterOption.setSelectedIndex(0); // select 'best'
328           btn_queryTDB.setVisible(false);
329           pnl_queryTDB.setVisible(false);
330           progressBar.setProgressBar(null, progressId);
331         }
332         mainFrame.setEnabled(true);
333         cmb_filterOption.setEnabled(true);
334       }
335     };
336
337     final FetchFinishedListenerI afterDbRefFetch = new FetchFinishedListenerI()
338     {
339
340       @Override
341       public void finished()
342       {
343         // filter has been selected, so we set flag to remove ourselves
344         notQueriedTDBYet = false;
345         // new thread to discover structures - via 3d beacons
346         Executors.defaultThreadFactory().newThread(strucDiscovery).start();
347
348       }
349     };
350
351     // fetch db refs if OK pressed
352     final Runnable discoverCanonicalDBrefs = () -> {
353       btn_queryTDB.setEnabled(false);
354       populateSeqsWithoutSourceDBRef();
355
356       final int y = seqsWithoutSourceDBRef.size();
357       if (y > 0)
358       {
359         final SequenceI[] seqWithoutSrcDBRef = seqsWithoutSourceDBRef
360                 .toArray(new SequenceI[y]);
361         DBRefFetcher dbRefFetcher = new DBRefFetcher(seqWithoutSrcDBRef,
362                 progressBar, new DbSourceProxy[]
363                 { new jalview.ws.dbsources.Uniprot() }, null, false);
364         dbRefFetcher.addListener(afterDbRefFetch);
365         // ideally this would also gracefully run with callbacks
366
367         dbRefFetcher.fetchDBRefs(true);
368       }
369       else
370       {
371         // call finished action directly
372         afterDbRefFetch.finished();
373       }
374     };
375     final Runnable revertview = () -> {
376       if (lastSelected != null)
377       {
378         cmb_filterOption.setSelectedItem(lastSelected);
379       }
380     };
381     int threshold = Cache.getDefault("UNIPROT_AUTOFETCH_THRESHOLD",
382             THRESHOLD_WARN_UNIPROT_FETCH_NEEDED);
383     Console.debug("Using Uniprot fetch threshold of " + threshold);
384     if (ignoreGui || seqsWithoutSourceDBRef.size() < threshold)
385     {
386       Executors.newSingleThreadExecutor().submit(discoverCanonicalDBrefs);
387       return;
388     }
389     // need cancel and no to result in the discoverPDB action - mocked is
390     // 'cancel' TODO: mock should be OK
391
392     StructureChooser thisSC = this;
393     JvOptionPane.newOptionDialog(thisSC.getFrame())
394             .setResponseHandler(JvOptionPane.OK_OPTION,
395                     discoverCanonicalDBrefs)
396             .setResponseHandler(JvOptionPane.CANCEL_OPTION, revertview)
397             .setResponseHandler(JvOptionPane.NO_OPTION, revertview)
398             .showDialog(
399                     MessageManager.formatMessage(
400                             "label.fetch_references_for_3dbeacons",
401                             seqsWithoutSourceDBRef.size()),
402                     MessageManager.getString("label.3dbeacons"),
403                     JvOptionPane.YES_NO_OPTION, JvOptionPane.PLAIN_MESSAGE,
404                     null, new Object[]
405                     { MessageManager.getString("action.ok"),
406                         MessageManager.getString("action.cancel") },
407                     MessageManager.getString("action.ok"), false);
408   }
409
410   /**
411    * Builds a drop-down choice list of existing structure viewers to which new
412    * structures may be added. If this list is empty then it, and the 'Add'
413    * button, are hidden.
414    */
415   private void discoverStructureViews()
416   {
417     if (Desktop.instance != null)
418     {
419       targetView.removeAllItems();
420       if (lastTargetedView != null && !lastTargetedView.isVisible())
421       {
422         lastTargetedView = null;
423       }
424       int linkedViewsAt = 0;
425       for (StructureViewerBase view : Desktop.instance
426               .getStructureViewers(null, null))
427       {
428         StructureViewer viewHandler = (lastTargetedView != null
429                 && lastTargetedView.sview == view) ? lastTargetedView
430                         : StructureViewer.reconfigure(view);
431
432         if (view.isLinkedWith(ap))
433         {
434           targetView.insertItemAt(viewHandler, linkedViewsAt++);
435         }
436         else
437         {
438           targetView.addItem(viewHandler);
439         }
440       }
441
442       /*
443        * show option to Add to viewer if at least 1 viewer found
444        */
445       targetView.setVisible(false);
446       if (targetView.getItemCount() > 0)
447       {
448         targetView.setVisible(true);
449         if (lastTargetedView != null)
450         {
451           targetView.setSelectedItem(lastTargetedView);
452         }
453         else
454         {
455           targetView.setSelectedIndex(0);
456         }
457       }
458       btn_add.setVisible(targetView.isVisible());
459     }
460   }
461
462   /**
463    * Updates the progress indicator with the specified message
464    * 
465    * @param message
466    *          displayed message for the operation
467    * @param id
468    *          unique handle for this indicator
469    */
470   protected void updateProgressIndicator(String message, long id)
471   {
472     if (progressIndicator != null)
473     {
474       progressIndicator.setProgressBar(message, id);
475     }
476   }
477
478   /**
479    * Retrieve meta-data for all the structure(s) for a given sequence(s) in a
480    * selection group
481    */
482   void fetchStructuresMetaData()
483   {
484     long startTime = System.currentTimeMillis();
485     Collection<FTSDataColumnI> wantedFields = data.getDocFieldPrefs()
486             .getStructureSummaryFields();
487
488     discoveredStructuresSet = new LinkedHashSet<>();
489     HashSet<String> errors = new HashSet<>();
490
491     FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
492             .getSelectedItem());
493
494     for (SequenceI seq : selectedSequences)
495     {
496
497       FTSRestResponse resultList;
498       try
499       {
500         resultList = data.fetchStructuresMetaData(seq, wantedFields,
501                 selectedFilterOpt, !chk_invertFilter.isSelected());
502         // null response means the FTSengine didn't yield a query for this
503         // consider designing a special exception if we really wanted to be
504         // OOCrazy
505         if (resultList == null)
506         {
507           continue;
508         }
509       } catch (Exception e)
510       {
511         e.printStackTrace();
512         errors.add(e.getMessage());
513         continue;
514       }
515       if (resultList.getSearchSummary() != null
516               && !resultList.getSearchSummary().isEmpty())
517       {
518         discoveredStructuresSet.addAll(resultList.getSearchSummary());
519       }
520     }
521
522     int noOfStructuresFound = 0;
523     String totalTime = (System.currentTimeMillis() - startTime)
524             + " milli secs";
525     if (discoveredStructuresSet != null
526             && !discoveredStructuresSet.isEmpty())
527     {
528       getResultTable()
529               .setModel(data.getTableModel(discoveredStructuresSet));
530
531       noOfStructuresFound = discoveredStructuresSet.size();
532       lastDiscoveredStructuresSet = discoveredStructuresSet;
533       mainFrame.setTitle(MessageManager.formatMessage(
534               "label.structure_chooser_no_of_structures",
535               noOfStructuresFound, totalTime));
536     }
537     else
538     {
539       mainFrame.setTitle(MessageManager
540               .getString("label.structure_chooser_manual_association"));
541       if (errors.size() > 0)
542       {
543         StringBuilder errorMsg = new StringBuilder();
544         for (String error : errors)
545         {
546           errorMsg.append(error).append("\n");
547         }
548         JvOptionPane.showMessageDialog(this, errorMsg.toString(),
549                 MessageManager.getString("label.pdb_web-service_error"),
550                 JvOptionPane.ERROR_MESSAGE);
551       }
552     }
553   }
554
555   protected void loadLocalCachedPDBEntries()
556   {
557     ArrayList<CachedPDB> entries = new ArrayList<>();
558     for (SequenceI seq : selectedSequences)
559     {
560       if (seq.getDatasetSequence() != null
561               && seq.getDatasetSequence().getAllPDBEntries() != null)
562       {
563         for (PDBEntry pdbEntry : seq.getDatasetSequence()
564                 .getAllPDBEntries())
565         {
566           if (pdbEntry.getFile() != null)
567           {
568             entries.add(new CachedPDB(seq, pdbEntry));
569           }
570         }
571       }
572     }
573     cachedPDBExists = !entries.isEmpty();
574     PDBEntryTableModel tableModelx = new PDBEntryTableModel(entries);
575     tbl_local_pdb.setModel(tableModelx);
576   }
577
578   /**
579    * Filters a given list of discovered structures based on supplied argument
580    * 
581    * @param fieldToFilterBy
582    *          the field to filter by
583    */
584   void filterResultSet(final String fieldToFilterBy)
585   {
586     Thread filterThread = new Thread(new Runnable()
587     {
588
589       @Override
590       public void run()
591       {
592         long startTime = System.currentTimeMillis();
593         lbl_loading.setVisible(true);
594         Collection<FTSDataColumnI> wantedFields = data.getDocFieldPrefs()
595                 .getStructureSummaryFields();
596         Collection<FTSData> filteredResponse = new HashSet<>();
597         HashSet<String> errors = new HashSet<>();
598
599         for (SequenceI seq : selectedSequences)
600         {
601
602           FTSRestResponse resultList;
603           try
604           {
605             resultList = data.selectFirstRankedQuery(seq,
606                     discoveredStructuresSet, wantedFields, fieldToFilterBy,
607                     !chk_invertFilter.isSelected());
608
609           } catch (Exception e)
610           {
611             e.printStackTrace();
612             errors.add(e.getMessage());
613             continue;
614           }
615           if (resultList.getSearchSummary() != null
616                   && !resultList.getSearchSummary().isEmpty())
617           {
618             filteredResponse.addAll(resultList.getSearchSummary());
619           }
620         }
621
622         String totalTime = (System.currentTimeMillis() - startTime)
623                 + " milli secs";
624         if (!filteredResponse.isEmpty())
625         {
626           final int filterResponseCount = filteredResponse.size();
627           Collection<FTSData> reorderedStructuresSet = new LinkedHashSet<>();
628           reorderedStructuresSet.addAll(filteredResponse);
629           reorderedStructuresSet.addAll(discoveredStructuresSet);
630           getResultTable()
631                   .setModel(data.getTableModel(reorderedStructuresSet));
632
633           FTSRestResponse.configureTableColumn(getResultTable(),
634                   wantedFields, tempUserPrefs);
635           getResultTable().getColumn("Ref Sequence").setPreferredWidth(120);
636           getResultTable().getColumn("Ref Sequence").setMinWidth(100);
637           getResultTable().getColumn("Ref Sequence").setMaxWidth(200);
638           // Update table selection model here
639           getResultTable().addRowSelectionInterval(0,
640                   filterResponseCount - 1);
641           mainFrame.setTitle(MessageManager.formatMessage(
642                   "label.structure_chooser_filter_time", totalTime));
643         }
644         else
645         {
646           mainFrame.setTitle(MessageManager.formatMessage(
647                   "label.structure_chooser_filter_time", totalTime));
648           if (errors.size() > 0)
649           {
650             StringBuilder errorMsg = new StringBuilder();
651             for (String error : errors)
652             {
653               errorMsg.append(error).append("\n");
654             }
655             JvOptionPane.showMessageDialog(null, errorMsg.toString(),
656                     MessageManager.getString("label.pdb_web-service_error"),
657                     JvOptionPane.ERROR_MESSAGE);
658           }
659         }
660
661         lbl_loading.setVisible(false);
662
663         validateSelections();
664       }
665     });
666     filterThread.start();
667   }
668
669   /**
670    * Handles action event for btn_pdbFromFile
671    */
672   @Override
673   protected void pdbFromFile_actionPerformed()
674   {
675     // TODO: JAL-3048 not needed for Jalview-JS until JSmol dep and
676     // StructureChooser
677     // works
678     JalviewFileChooser chooser = new JalviewFileChooser(
679             Cache.getProperty("LAST_DIRECTORY"));
680     chooser.setFileView(new JalviewFileView());
681     chooser.setDialogTitle(
682             MessageManager.formatMessage("label.select_pdb_file_for",
683                     selectedSequence.getDisplayId(false)));
684     chooser.setToolTipText(MessageManager.formatMessage(
685             "label.load_pdb_file_associate_with_sequence",
686             selectedSequence.getDisplayId(false)));
687
688     int value = chooser.showOpenDialog(null);
689     if (value == JalviewFileChooser.APPROVE_OPTION)
690     {
691       selectedPdbFileName = chooser.getSelectedFile().getPath();
692       Cache.setProperty("LAST_DIRECTORY", selectedPdbFileName);
693       boolean guessTFType = localPdbPaeMatrixFileName == null;
694       localPdbPaeMatrixFileName = guessPAEFilename();
695       guessTFType |= localPdbPaeMatrixFileName != null;
696       Regex alphaFold = JmolParser.getNewAlphafoldValidator();
697       if (guessTFType
698               && alphaFold.search(new File(selectedPdbFileName).getName())
699               && !tempFacAsChanged)
700       {
701         // localPdbPaeMatrixFileName was null and now isn't and filename could
702         // well be AlphaFold and user hasn't adjusted the tempFacType
703         combo_tempFacAs.setSelectedItem(TFType.PLDDT);
704       }
705       validateSelections();
706     }
707   }
708
709   /**
710    * Handles action event for btn_paeMatrixFile
711    */
712   @Override
713   protected void paeMatrixFile_actionPerformed()
714   {
715     File pdbFile = new File(selectedPdbFileName);
716     String setFile = Cache.getProperty("LAST_DIRECTORY");
717     if (localPdbPaeMatrixFileName != null)
718     {
719       File paeFile = new File(localPdbPaeMatrixFileName);
720       if (paeFile.exists())
721         setFile = paeFile.getAbsolutePath();
722       else if (paeFile.getParentFile().exists())
723         setFile = paeFile.getParentFile().getAbsolutePath();
724     }
725     else
726     {
727       String guess = guessPAEFilename();
728       if (guess != null)
729         setFile = guess;
730     }
731     JalviewFileChooser chooser = new JalviewFileChooser(setFile);
732     chooser.setFileView(new JalviewFileView());
733     chooser.setDialogTitle(MessageManager.formatMessage(
734             "label.select_pae_matrix_file_for", pdbFile.getName()));
735     chooser.setToolTipText(MessageManager.formatMessage(
736             "label.load_pae_matrix_file_associate_with_structure",
737             pdbFile.getName()));
738
739     // TODO convert to Callable/Promise
740     int value = chooser.showOpenDialog(null);
741     if (value == JalviewFileChooser.APPROVE_OPTION)
742     {
743       String fileName = chooser.getSelectedFile().getPath();
744       try
745       {
746         PAEContactMatrix.validateContactMatrixFile(fileName);
747       } catch (Exception thr)
748       {
749         JvOptionPane.showInternalMessageDialog(this, MessageManager
750                 .formatMessage("label.couldnt_load_file", new Object[]
751                 { fileName }) + "<br>" + thr.getLocalizedMessage(),
752                 MessageManager.getString("label.error_loading_file"),
753                 JvOptionPane.WARNING_MESSAGE);
754         Console.error("Couldn't import " + fileName + " as a PAE matrix",
755                 thr);
756         return;
757       }
758       localPdbPaeMatrixFileName = fileName;
759       Cache.setProperty("LAST_DIRECTORY", localPdbPaeMatrixFileName);
760     }
761     validateAssociationFromFile();
762   }
763
764   private String guessPAEFilename()
765   {
766     if (selectedPdbFileName.toLowerCase(Locale.ROOT).endsWith(".pdb")
767             || selectedPdbFileName.toLowerCase(Locale.ROOT)
768                     .endsWith(".cif"))
769     {
770       String jsonExt = selectedPdbFileName.substring(0,
771               selectedPdbFileName.length() - 4) + ".json";
772       // AlphaFold naming scheme
773       String guessFile1 = StringUtils.replaceLast(jsonExt, "model",
774               "predicted_aligned_error");
775       // nf-core mode naming scheme
776       String guessFile2 = StringUtils.replaceLast(jsonExt, ".json",
777               "_scores.json");
778       if (new File(guessFile1).exists())
779       {
780         return guessFile1;
781       }
782       else if (new File(jsonExt).exists())
783       {
784         return jsonExt;
785       }
786       else if (new File(guessFile2).exists())
787       {
788         return guessFile2;
789       }
790     }
791     return null;
792   }
793
794   /**
795    * Populates the filter combo-box options dynamically depending on discovered
796    * structures
797    */
798   protected void populateFilterComboBox(boolean haveData,
799           boolean cachedPDBExist)
800   {
801     populateFilterComboBox(haveData, cachedPDBExist, null);
802   }
803
804   /**
805    * Populates the filter combo-box options dynamically depending on discovered
806    * structures
807    */
808   protected void populateFilterComboBox(boolean haveData,
809           boolean cachedPDBExist, FilterOption lastSel)
810   {
811
812     /*
813      * temporarily suspend the change listener behaviour
814      */
815     cmb_filterOption.removeItemListener(this);
816     int selSet = -1;
817     cmb_filterOption.removeAllItems();
818     if (haveData)
819     {
820       List<FilterOption> filters = data
821               .getAvailableFilterOptions(VIEWS_FILTER);
822       data.updateAvailableFilterOptions(VIEWS_FILTER, filters,
823               lastDiscoveredStructuresSet);
824       int p = 0;
825       for (FilterOption filter : filters)
826       {
827         if (lastSel != null && filter.equals(lastSel))
828         {
829           selSet = p;
830         }
831         p++;
832         cmb_filterOption.addItem(filter);
833       }
834     }
835
836     cmb_filterOption.addItem(
837             new FilterOption(MessageManager.getString("label.enter_pdb_id"),
838                     "-", VIEWS_ENTER_ID, false, null));
839     cmb_filterOption.addItem(
840             new FilterOption(MessageManager.getString("label.from_file"),
841                     "-", VIEWS_FROM_FILE, false, null));
842     if (canQueryTDB && notQueriedTDBYet)
843     {
844       btn_queryTDB.setVisible(true);
845       pnl_queryTDB.setVisible(true);
846     }
847
848     if (cachedPDBExist)
849     {
850       FilterOption cachedOption = new FilterOption(
851               MessageManager.getString("label.cached_structures"), "-",
852               VIEWS_LOCAL_PDB, false, null);
853       cmb_filterOption.addItem(cachedOption);
854       if (selSet == -1)
855       {
856         cmb_filterOption.setSelectedItem(cachedOption);
857       }
858     }
859     if (selSet > -1)
860     {
861       cmb_filterOption.setSelectedIndex(selSet);
862     }
863     cmb_filterOption.addItemListener(this);
864   }
865
866   /**
867    * Updates the displayed view based on the selected filter option
868    */
869   protected void updateCurrentView()
870   {
871     FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
872             .getSelectedItem());
873
874     if (lastSelected == selectedFilterOpt)
875     {
876       // don't need to do anything, probably
877       return;
878     }
879     // otherwise, record selection
880     // and update the layout and dialog accordingly
881     lastSelected = selectedFilterOpt;
882
883     layout_switchableViews.show(pnl_switchableViews,
884             selectedFilterOpt.getView());
885     String filterTitle = mainFrame.getTitle();
886     mainFrame.setTitle(frameTitle);
887     chk_invertFilter.setVisible(false);
888
889     if (selectedFilterOpt.getView() == VIEWS_FILTER)
890     {
891       mainFrame.setTitle(filterTitle);
892       // TDB Query has no invert as yet
893       chk_invertFilter.setVisible(selectedFilterOpt
894               .getQuerySource() instanceof PDBStructureChooserQuerySource);
895
896       if (data != selectedFilterOpt.getQuerySource()
897               || data.needsRefetch(selectedFilterOpt))
898       {
899         data = selectedFilterOpt.getQuerySource();
900         // rebuild the views completely, since prefs will also change
901         tabRefresh();
902         return;
903       }
904       else
905       {
906         filterResultSet(selectedFilterOpt.getValue());
907       }
908     }
909     else if (selectedFilterOpt.getView() == VIEWS_ENTER_ID
910             || selectedFilterOpt.getView() == VIEWS_FROM_FILE)
911     {
912       mainFrame.setTitle(MessageManager
913               .getString("label.structure_chooser_manual_association"));
914       idInputAssSeqPanel.loadCmbAssSeq();
915       fileChooserAssSeqPanel.loadCmbAssSeq();
916     }
917     validateSelections();
918   }
919
920   /**
921    * Validates user selection and enables the 'Add' and 'New View' buttons if
922    * all parameters are correct (the Add button will only be visible if there is
923    * at least one existing structure viewer open). This basically means at least
924    * one structure selected and no error messages.
925    * <p>
926    * The 'Superpose Structures' option is enabled if either more than one
927    * structure is selected, or the 'Add' to existing view option is enabled, and
928    * disabled if the only option is to open a new view of a single structure.
929    */
930   @Override
931   protected void validateSelections()
932   {
933     FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
934             .getSelectedItem());
935     btn_add.setEnabled(false);
936     String currentView = selectedFilterOpt.getView();
937     int selectedCount = 0;
938     if (currentView == VIEWS_FILTER)
939     {
940       selectedCount = getResultTable().getSelectedRows().length;
941       if (selectedCount > 0)
942       {
943         btn_add.setEnabled(true);
944       }
945     }
946     else if (currentView == VIEWS_LOCAL_PDB)
947     {
948       selectedCount = tbl_local_pdb.getSelectedRows().length;
949       if (selectedCount > 0)
950       {
951         btn_add.setEnabled(true);
952       }
953     }
954     else if (currentView == VIEWS_ENTER_ID)
955     {
956       validateAssociationEnterPdb();
957     }
958     else if (currentView == VIEWS_FROM_FILE)
959     {
960       validateAssociationFromFile();
961     }
962
963     btn_newView.setEnabled(btn_add.isEnabled());
964
965     /*
966      * enable 'Superpose' option if more than one structure is selected,
967      * or there are view(s) available to add structure(s) to
968      */
969     chk_superpose
970             .setEnabled(selectedCount > 1 || targetView.getItemCount() > 0);
971   }
972
973   @Override
974   protected boolean showPopupFor(int selectedRow, int x, int y)
975   {
976     FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
977             .getSelectedItem());
978     String currentView = selectedFilterOpt.getView();
979
980     if (currentView == VIEWS_FILTER
981             && data instanceof ThreeDBStructureChooserQuerySource)
982     {
983
984       TDB_FTSData row = ((ThreeDBStructureChooserQuerySource) data)
985               .getFTSDataFor(getResultTable(), selectedRow,
986                       discoveredStructuresSet);
987       String pageUrl = row.getModelViewUrl();
988       JPopupMenu popup = new JPopupMenu("3D Beacons");
989       JMenuItem viewUrl = new JMenuItem("View model web page");
990       viewUrl.addActionListener(new ActionListener()
991       {
992         @Override
993         public void actionPerformed(ActionEvent e)
994         {
995           Desktop.showUrl(pageUrl);
996         }
997       });
998       popup.add(viewUrl);
999       SwingUtilities.invokeLater(new Runnable()
1000       {
1001         @Override
1002         public void run()
1003         {
1004           popup.show(getResultTable(), x, y);
1005         }
1006       });
1007       return true;
1008     }
1009     // event not handled by us
1010     return false;
1011   }
1012
1013   /**
1014    * Validates inputs from the Manual PDB entry panel
1015    */
1016   protected void validateAssociationEnterPdb()
1017   {
1018     AssociateSeqOptions assSeqOpt = (AssociateSeqOptions) idInputAssSeqPanel
1019             .getCmb_assSeq().getSelectedItem();
1020     lbl_pdbManualFetchStatus.setIcon(errorImage);
1021     lbl_pdbManualFetchStatus.setToolTipText("");
1022     if (txt_search.getText().length() > 0)
1023     {
1024       lbl_pdbManualFetchStatus.setToolTipText(JvSwingUtils.wrapTooltip(true,
1025               MessageManager.formatMessage("info.no_pdb_entry_found_for",
1026                       txt_search.getText())));
1027     }
1028
1029     if (errorWarning.length() > 0)
1030     {
1031       lbl_pdbManualFetchStatus.setIcon(warningImage);
1032       lbl_pdbManualFetchStatus.setToolTipText(
1033               JvSwingUtils.wrapTooltip(true, errorWarning.toString()));
1034     }
1035
1036     if (selectedSequences.length == 1 || !assSeqOpt.getName()
1037             .equalsIgnoreCase("-Select Associated Seq-"))
1038     {
1039       txt_search.setEnabled(true);
1040       if (isValidPBDEntry)
1041       {
1042         btn_add.setEnabled(true);
1043         lbl_pdbManualFetchStatus.setToolTipText("");
1044         lbl_pdbManualFetchStatus.setIcon(goodImage);
1045       }
1046     }
1047     else
1048     {
1049       txt_search.setEnabled(false);
1050       lbl_pdbManualFetchStatus.setIcon(errorImage);
1051     }
1052   }
1053
1054   /**
1055    * Validates inputs for the manual PDB file selection options
1056    */
1057   protected void validateAssociationFromFile()
1058   {
1059     AssociateSeqOptions assSeqOpt = (AssociateSeqOptions) fileChooserAssSeqPanel
1060             .getCmb_assSeq().getSelectedItem();
1061     // lbl_fromFileStatus.setIcon(errorImage);
1062     String pdbFileString = "";
1063     String pdbFileTooltip = "";
1064     if (selectedSequences.length == 1 || (assSeqOpt != null && !assSeqOpt
1065             .getName().equalsIgnoreCase("-Select Associated Seq-")))
1066     {
1067       btn_pdbFromFile.setEnabled(true);
1068       if (selectedPdbFileName != null && selectedPdbFileName.length() > 0)
1069       {
1070         btn_add.setEnabled(true);
1071         // lbl_fromFileStatus.setIcon(goodImage);
1072         pdbFileString = new File(selectedPdbFileName).getName();
1073         pdbFileTooltip = new File(selectedPdbFileName).getAbsolutePath();
1074         setPdbOptionsEnabled(true);
1075       }
1076       else
1077       {
1078         pdbFileString = MessageManager.getString("label.none");
1079         pdbFileTooltip = MessageManager.getString("label.nothing_selected");
1080         setPdbOptionsEnabled(false);
1081       }
1082     }
1083     else
1084     {
1085       btn_pdbFromFile.setEnabled(false);
1086       setPdbOptionsEnabled(false);
1087       // lbl_fromFileStatus.setIcon(errorImage);
1088       pdbFileString = MessageManager.getString("label.none");
1089       pdbFileTooltip = MessageManager.getString("label.nothing_selected");
1090     }
1091     lbl_pdbFile.setText(pdbFileString);
1092     lbl_pdbFile.setToolTipText(pdbFileTooltip);
1093
1094     // PAE file choice
1095     String paeFileString = "";
1096     String paeFileTooltip = "";
1097     if (localPdbPaeMatrixFileName != null
1098             && localPdbPaeMatrixFileName.length() > 0)
1099     {
1100       paeFileString = new File(localPdbPaeMatrixFileName).getName();
1101       paeFileTooltip = new File(localPdbPaeMatrixFileName)
1102               .getAbsolutePath();
1103     }
1104     else
1105     {
1106       paeFileString = MessageManager.getString("label.none");
1107       paeFileTooltip = MessageManager.getString("label.nothing_selected");
1108     }
1109     lbl_paeFile.setText(paeFileString);
1110     lbl_paeFile.setToolTipText(paeFileTooltip);
1111   }
1112
1113   @Override
1114   protected void cmbAssSeqStateChanged()
1115   {
1116     validateSelections();
1117   }
1118
1119   private FilterOption lastSelected = null;
1120
1121   /**
1122    * Handles the state change event for the 'filter' combo-box and 'invert'
1123    * check-box
1124    */
1125   @Override
1126   protected void stateChanged(ItemEvent e)
1127   {
1128     if (e.getSource() instanceof JCheckBox)
1129     {
1130       updateCurrentView();
1131     }
1132     else
1133     {
1134       if (e.getStateChange() == ItemEvent.SELECTED)
1135       {
1136         updateCurrentView();
1137       }
1138     }
1139
1140   }
1141
1142   /**
1143    * select structures for viewing by their PDB IDs
1144    * 
1145    * @param pdbids
1146    * @return true if structures were found and marked as selected
1147    */
1148   public boolean selectStructure(String... pdbids)
1149   {
1150     boolean found = false;
1151
1152     FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
1153             .getSelectedItem());
1154     String currentView = selectedFilterOpt.getView();
1155     JTable restable = (currentView == VIEWS_FILTER) ? getResultTable()
1156             : (currentView == VIEWS_LOCAL_PDB) ? tbl_local_pdb : null;
1157
1158     if (restable == null)
1159     {
1160       // can't select (enter PDB ID, or load file - need to also select which
1161       // sequence to associate with)
1162       return false;
1163     }
1164
1165     int pdbIdColIndex = restable.getColumn("PDB Id").getModelIndex();
1166     for (int r = 0; r < restable.getRowCount(); r++)
1167     {
1168       for (int p = 0; p < pdbids.length; p++)
1169       {
1170         if (String.valueOf(restable.getValueAt(r, pdbIdColIndex))
1171                 .equalsIgnoreCase(pdbids[p]))
1172         {
1173           restable.setRowSelectionInterval(r, r);
1174           found = true;
1175         }
1176       }
1177     }
1178     return found;
1179   }
1180
1181   /**
1182    * Handles the 'New View' action
1183    */
1184   @Override
1185   protected void newView_ActionPerformed()
1186   {
1187     targetView.setSelectedItem(null);
1188     showStructures(false);
1189   }
1190
1191   /**
1192    * Handles the 'Add to existing viewer' action
1193    */
1194   @Override
1195   protected void add_ActionPerformed()
1196   {
1197     showStructures(false);
1198   }
1199
1200   /**
1201    * structure viewer opened by this dialog, or null
1202    */
1203   private StructureViewer sViewer = null;
1204
1205   public void showStructures(boolean waitUntilFinished)
1206   {
1207
1208     final StructureSelectionManager ssm = ap.getStructureSelectionManager();
1209
1210     final int preferredHeight = pnl_filter.getHeight();
1211     btn_add.setEnabled(false);
1212     btn_newView.setEnabled(false);
1213     btn_cancel.setEnabled(false);
1214     actionsPanel.setEnabled(false);
1215
1216     final String progress = MessageManager
1217             .getString("label.working_ellipsis");
1218     setProgressBar(progress, progress.hashCode());
1219     Runnable viewStruc = new Runnable()
1220     {
1221       @Override
1222       public void run()
1223       {
1224         FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
1225                 .getSelectedItem());
1226         String currentView = selectedFilterOpt.getView();
1227         JTable restable = (currentView == VIEWS_FILTER) ? getResultTable()
1228                 : tbl_local_pdb;
1229
1230         if (currentView == VIEWS_FILTER)
1231         {
1232           int[] selectedRows = restable.getSelectedRows();
1233           PDBEntry[] pdbEntriesToView = new PDBEntry[selectedRows.length];
1234           List<SequenceI> selectedSeqsToView = new ArrayList<>();
1235           pdbEntriesToView = data.collectSelectedRows(restable,
1236                   selectedRows, selectedSeqsToView);
1237
1238           SequenceI[] selectedSeqs = selectedSeqsToView
1239                   .toArray(new SequenceI[selectedSeqsToView.size()]);
1240           sViewer = launchStructureViewer(ssm, pdbEntriesToView, ap,
1241                   selectedSeqs);
1242         }
1243         else if (currentView == VIEWS_LOCAL_PDB)
1244         {
1245           int[] selectedRows = tbl_local_pdb.getSelectedRows();
1246           PDBEntry[] pdbEntriesToView = new PDBEntry[selectedRows.length];
1247           int count = 0;
1248           int pdbIdColIndex = tbl_local_pdb.getColumn("PDB Id")
1249                   .getModelIndex();
1250           int refSeqColIndex = tbl_local_pdb.getColumn("Ref Sequence")
1251                   .getModelIndex();
1252           List<SequenceI> selectedSeqsToView = new ArrayList<>();
1253           for (int row : selectedRows)
1254           {
1255             PDBEntry pdbEntry = ((PDBEntryTableModel) tbl_local_pdb
1256                     .getModel()).getPDBEntryAt(row).getPdbEntry();
1257
1258             pdbEntriesToView[count++] = pdbEntry;
1259             SequenceI selectedSeq = (SequenceI) tbl_local_pdb
1260                     .getValueAt(row, refSeqColIndex);
1261             selectedSeqsToView.add(selectedSeq);
1262           }
1263           SequenceI[] selectedSeqs = selectedSeqsToView
1264                   .toArray(new SequenceI[selectedSeqsToView.size()]);
1265           sViewer = launchStructureViewer(ssm, pdbEntriesToView, ap,
1266                   selectedSeqs);
1267         }
1268         else if (currentView == VIEWS_ENTER_ID)
1269         {
1270           SequenceI userSelectedSeq = ((AssociateSeqOptions) idInputAssSeqPanel
1271                   .getCmb_assSeq().getSelectedItem()).getSequence();
1272           if (userSelectedSeq != null)
1273           {
1274             selectedSequence = userSelectedSeq;
1275           }
1276           String pdbIdStr = txt_search.getText();
1277           PDBEntry pdbEntry = selectedSequence.getPDBEntry(pdbIdStr);
1278           if (pdbEntry == null)
1279           {
1280             pdbEntry = new PDBEntry();
1281             if (pdbIdStr.split(":").length > 1)
1282             {
1283               pdbEntry.setId(pdbIdStr.split(":")[0]);
1284               pdbEntry.setChainCode(
1285                       pdbIdStr.split(":")[1].toUpperCase(Locale.ROOT));
1286             }
1287             else
1288             {
1289               pdbEntry.setId(pdbIdStr);
1290             }
1291             pdbEntry.setType(PDBEntry.Type.PDB);
1292             selectedSequence.getDatasetSequence().addPDBId(pdbEntry);
1293           }
1294
1295           PDBEntry[] pdbEntriesToView = new PDBEntry[] { pdbEntry };
1296           sViewer = launchStructureViewer(ssm, pdbEntriesToView, ap,
1297                   new SequenceI[]
1298                   { selectedSequence });
1299         }
1300         else if (currentView == VIEWS_FROM_FILE)
1301         {
1302           StructureChooser sc = StructureChooser.this;
1303           TFType tft = (TFType) sc.combo_tempFacAs.getSelectedItem();
1304           String paeFilename = sc.localPdbPaeMatrixFileName;
1305           AssociateSeqOptions assSeqOpt = (AssociateSeqOptions) fileChooserAssSeqPanel
1306                   .getCmb_assSeq().getSelectedItem();
1307           SequenceI userSelectedSeq = assSeqOpt.getSequence();
1308           if (userSelectedSeq != null)
1309           {
1310             selectedSequence = userSelectedSeq;
1311           }
1312           String pdbFilename = selectedPdbFileName;
1313
1314           StructureChooser.openStructureFileForSequence(ssm, sc, ap,
1315                   selectedSequence, true, pdbFilename, tft, paeFilename,
1316                   true);
1317         }
1318         SwingUtilities.invokeLater(new Runnable()
1319         {
1320           @Override
1321           public void run()
1322           {
1323             setProgressBar("Complete.", progress.hashCode());
1324             closeAction(preferredHeight);
1325             mainFrame.dispose();
1326           }
1327         });
1328       }
1329     };
1330     Thread runner = new Thread(viewStruc);
1331     runner.start();
1332     if (waitUntilFinished)
1333     {
1334       while (sViewer == null ? runner.isAlive()
1335               : (sViewer.sview == null ? true
1336                       : !sViewer.sview.hasMapping()))
1337       {
1338         try
1339         {
1340           Thread.sleep(300);
1341         } catch (InterruptedException ie)
1342         {
1343
1344         }
1345       }
1346     }
1347   }
1348
1349   /**
1350    * Answers a structure viewer (new or existing) configured to superimpose
1351    * added structures or not according to the user's choice
1352    * 
1353    * @param ssm
1354    * @return
1355    */
1356   StructureViewer getTargetedStructureViewer(StructureSelectionManager ssm)
1357   {
1358     Object sv = targetView.getSelectedItem();
1359
1360     return sv == null ? new StructureViewer(ssm) : (StructureViewer) sv;
1361   }
1362
1363   /**
1364    * Adds PDB structures to a new or existing structure viewer
1365    * 
1366    * @param ssm
1367    * @param pdbEntriesToView
1368    * @param alignPanel
1369    * @param sequences
1370    * @return
1371    */
1372   private StructureViewer launchStructureViewer(
1373           StructureSelectionManager ssm, final PDBEntry[] pdbEntriesToView,
1374           final AlignmentPanel alignPanel, SequenceI[] sequences)
1375   {
1376     return launchStructureViewer(ssm, pdbEntriesToView, alignPanel,
1377             sequences, null);
1378   }
1379
1380   private StructureViewer launchStructureViewer(
1381           StructureSelectionManager ssm, final PDBEntry[] pdbEntriesToView,
1382           final AlignmentPanel alignPanel, SequenceI[] sequences,
1383           ViewerType viewerType)
1384   {
1385     long progressId = sequences.hashCode();
1386     setProgressBar(MessageManager
1387             .getString("status.launching_3d_structure_viewer"), progressId);
1388     final StructureViewer theViewer = getTargetedStructureViewer(ssm);
1389     boolean superimpose = chk_superpose.isSelected();
1390     theViewer.setSuperpose(superimpose);
1391
1392     /*
1393      * remember user's choice of superimpose or not
1394      */
1395     Cache.setProperty(AUTOSUPERIMPOSE,
1396             Boolean.valueOf(superimpose).toString());
1397
1398     setProgressBar(null, progressId);
1399     if (SiftsSettings.isMapWithSifts())
1400     {
1401       List<SequenceI> seqsWithoutSourceDBRef = new ArrayList<>();
1402       int p = 0;
1403       // TODO: skip PDBEntry:Sequence pairs where PDBEntry doesn't look like a
1404       // real PDB ID. For moment, we can also safely do this if there is already
1405       // a known mapping between the PDBEntry and the sequence.
1406       for (SequenceI seq : sequences)
1407       {
1408         PDBEntry pdbe = pdbEntriesToView[p++];
1409         if (pdbe != null && pdbe.getFile() != null)
1410         {
1411           StructureMapping[] smm = ssm.getMapping(pdbe.getFile());
1412           if (smm != null && smm.length > 0)
1413           {
1414             for (StructureMapping sm : smm)
1415             {
1416               if (sm.getSequence() == seq)
1417               {
1418                 continue;
1419               }
1420             }
1421           }
1422         }
1423         if (seq.getPrimaryDBRefs().isEmpty())
1424         {
1425           seqsWithoutSourceDBRef.add(seq);
1426           continue;
1427         }
1428       }
1429       if (!seqsWithoutSourceDBRef.isEmpty())
1430       {
1431         int y = seqsWithoutSourceDBRef.size();
1432         setProgressBar(MessageManager.formatMessage(
1433                 "status.fetching_dbrefs_for_sequences_without_valid_refs",
1434                 y), progressId);
1435         SequenceI[] seqWithoutSrcDBRef = seqsWithoutSourceDBRef
1436                 .toArray(new SequenceI[y]);
1437         DBRefFetcher dbRefFetcher = new DBRefFetcher(seqWithoutSrcDBRef);
1438         dbRefFetcher.fetchDBRefs(true);
1439
1440         setProgressBar("Fetch complete.", progressId); // todo i18n
1441       }
1442     }
1443     if (pdbEntriesToView.length > 1)
1444     {
1445       setProgressBar(
1446               MessageManager.getString(
1447                       "status.fetching_3d_structures_for_selected_entries"),
1448               progressId);
1449       theViewer.viewStructures(pdbEntriesToView, sequences, alignPanel,
1450               viewerType);
1451     }
1452     else
1453     {
1454       setProgressBar(MessageManager.formatMessage(
1455               "status.fetching_3d_structures_for",
1456               pdbEntriesToView[0].getId()), progressId);
1457       // Can we pass a pre-computeMappinged pdbFile?
1458       theViewer.viewStructures(pdbEntriesToView[0], sequences, alignPanel,
1459               viewerType);
1460     }
1461     setProgressBar(null, progressId);
1462     // remember the last viewer we used...
1463     lastTargetedView = theViewer;
1464     return theViewer;
1465   }
1466
1467   /**
1468    * Populates the combo-box used in associating manually fetched structures to
1469    * a unique sequence when more than one sequence selection is made.
1470    */
1471   @Override
1472   protected void populateCmbAssociateSeqOptions(
1473           JComboBox<AssociateSeqOptions> cmb_assSeq,
1474           JLabel lbl_associateSeq)
1475   {
1476     cmb_assSeq.removeAllItems();
1477     cmb_assSeq.addItem(
1478             new AssociateSeqOptions("-Select Associated Seq-", null));
1479     lbl_associateSeq.setVisible(false);
1480     if (selectedSequences.length > 1)
1481     {
1482       for (SequenceI seq : selectedSequences)
1483       {
1484         cmb_assSeq.addItem(new AssociateSeqOptions(seq));
1485       }
1486     }
1487     else
1488     {
1489       String seqName = selectedSequence.getDisplayId(false);
1490       seqName = seqName.length() <= 40 ? seqName : seqName.substring(0, 39);
1491       lbl_associateSeq.setText(seqName);
1492       lbl_associateSeq.setVisible(true);
1493       cmb_assSeq.setVisible(false);
1494     }
1495   }
1496
1497   protected boolean isStructuresDiscovered()
1498   {
1499     return discoveredStructuresSet != null
1500             && !discoveredStructuresSet.isEmpty();
1501   }
1502
1503   protected int PDB_ID_MIN = 3;// or: (Jalview.isJS() ? 3 : 1); // Bob proposes
1504                                // this.
1505   // Doing a search for "1" or "1c" is valuable?
1506   // Those work but are enormously slow.
1507
1508   @Override
1509   protected void txt_search_ActionPerformed()
1510   {
1511     String text = txt_search.getText().trim();
1512     if (text.length() >= PDB_ID_MIN)
1513       new Thread()
1514       {
1515
1516         @Override
1517         public void run()
1518         {
1519           errorWarning.setLength(0);
1520           isValidPBDEntry = false;
1521           if (text.length() > 0)
1522           {
1523             // TODO move this pdb id search into the PDB specific
1524             // FTSSearchEngine
1525             // for moment, it will work fine as is because it is self-contained
1526             String searchTerm = text.toLowerCase(Locale.ROOT);
1527             searchTerm = searchTerm.split(":")[0];
1528             // System.out.println(">>>>> search term : " + searchTerm);
1529             List<FTSDataColumnI> wantedFields = new ArrayList<>();
1530             FTSRestRequest pdbRequest = new FTSRestRequest();
1531             pdbRequest.setAllowEmptySeq(false);
1532             pdbRequest.setResponseSize(1);
1533             pdbRequest.setFieldToSearchBy("(pdb_id:");
1534             pdbRequest.setWantedFields(wantedFields);
1535             pdbRequest.setSearchTerm(searchTerm + ")");
1536             pdbRequest.setAssociatedSequence(selectedSequence);
1537             FTSRestClientI pdbRestClient = PDBFTSRestClient.getInstance();
1538             wantedFields.add(pdbRestClient.getPrimaryKeyColumn());
1539             FTSRestResponse resultList;
1540             try
1541             {
1542               resultList = pdbRestClient.executeRequest(pdbRequest);
1543             } catch (Exception e)
1544             {
1545               errorWarning.append(e.getMessage());
1546               return;
1547             } finally
1548             {
1549               validateSelections();
1550             }
1551             if (resultList.getSearchSummary() != null
1552                     && resultList.getSearchSummary().size() > 0)
1553             {
1554               isValidPBDEntry = true;
1555             }
1556           }
1557           validateSelections();
1558         }
1559       }.start();
1560   }
1561
1562   @Override
1563   protected void tabRefresh()
1564   {
1565     if (selectedSequences != null)
1566     {
1567       lbl_loading.setVisible(true);
1568       Thread refreshThread = new Thread(new Runnable()
1569       {
1570         @Override
1571         public void run()
1572         {
1573           fetchStructuresMetaData();
1574           // populateFilterComboBox(true, cachedPDBExists);
1575
1576           filterResultSet(
1577                   ((FilterOption) cmb_filterOption.getSelectedItem())
1578                           .getValue());
1579           lbl_loading.setVisible(false);
1580         }
1581       });
1582       refreshThread.start();
1583     }
1584   }
1585
1586   public class PDBEntryTableModel extends AbstractTableModel
1587   {
1588     String[] columns = { "Ref Sequence", "PDB Id", "Chain", "Type",
1589         "File" };
1590
1591     private List<CachedPDB> pdbEntries;
1592
1593     public PDBEntryTableModel(List<CachedPDB> pdbEntries)
1594     {
1595       this.pdbEntries = new ArrayList<>(pdbEntries);
1596     }
1597
1598     @Override
1599     public String getColumnName(int columnIndex)
1600     {
1601       return columns[columnIndex];
1602     }
1603
1604     @Override
1605     public int getRowCount()
1606     {
1607       return pdbEntries.size();
1608     }
1609
1610     @Override
1611     public int getColumnCount()
1612     {
1613       return columns.length;
1614     }
1615
1616     @Override
1617     public boolean isCellEditable(int row, int column)
1618     {
1619       return false;
1620     }
1621
1622     @Override
1623     public Object getValueAt(int rowIndex, int columnIndex)
1624     {
1625       Object value = "??";
1626       CachedPDB entry = pdbEntries.get(rowIndex);
1627       switch (columnIndex)
1628       {
1629       case 0:
1630         value = entry.getSequence();
1631         break;
1632       case 1:
1633         value = entry.getQualifiedId();
1634         break;
1635       case 2:
1636         value = entry.getPdbEntry().getChainCode() == null ? "_"
1637                 : entry.getPdbEntry().getChainCode();
1638         break;
1639       case 3:
1640         value = entry.getPdbEntry().getType();
1641         break;
1642       case 4:
1643         value = entry.getPdbEntry().getFile();
1644         break;
1645       }
1646       return value;
1647     }
1648
1649     @Override
1650     public Class<?> getColumnClass(int columnIndex)
1651     {
1652       return columnIndex == 0 ? SequenceI.class : PDBEntry.class;
1653     }
1654
1655     public CachedPDB getPDBEntryAt(int row)
1656     {
1657       return pdbEntries.get(row);
1658     }
1659
1660   }
1661
1662   private class CachedPDB
1663   {
1664     private SequenceI sequence;
1665
1666     private PDBEntry pdbEntry;
1667
1668     public CachedPDB(SequenceI sequence, PDBEntry pdbEntry)
1669     {
1670       this.sequence = sequence;
1671       this.pdbEntry = pdbEntry;
1672     }
1673
1674     public String getQualifiedId()
1675     {
1676       if (pdbEntry.hasProvider())
1677       {
1678         return pdbEntry.getProvider() + ":" + pdbEntry.getId();
1679       }
1680       return pdbEntry.toString();
1681     }
1682
1683     public SequenceI getSequence()
1684     {
1685       return sequence;
1686     }
1687
1688     public PDBEntry getPdbEntry()
1689     {
1690       return pdbEntry;
1691     }
1692
1693   }
1694
1695   private IProgressIndicator progressBar;
1696
1697   @Override
1698   public void setProgressBar(String message, long id)
1699   {
1700     if (!Platform.isHeadless() && progressBar != null)
1701       progressBar.setProgressBar(message, id);
1702   }
1703
1704   @Override
1705   public void registerHandler(long id, IProgressIndicatorHandler handler)
1706   {
1707     if (progressBar != null)
1708       progressBar.registerHandler(id, handler);
1709   }
1710
1711   @Override
1712   public boolean operationInProgress()
1713   {
1714     return progressBar == null ? false : progressBar.operationInProgress();
1715   }
1716
1717   public JalviewStructureDisplayI getOpenedStructureViewer()
1718   {
1719     return sViewer == null ? null : sViewer.sview;
1720   }
1721
1722   @Override
1723   protected void setFTSDocFieldPrefs(FTSDataColumnPreferences newPrefs)
1724   {
1725     data.setDocFieldPrefs(newPrefs);
1726
1727   }
1728
1729   /**
1730    * 
1731    * @return true when all initialisation threads have finished and dialog is
1732    *         visible
1733    */
1734   public boolean isDialogVisible()
1735   {
1736     return mainFrame != null && data != null && cmb_filterOption != null
1737             && mainFrame.isVisible()
1738             && cmb_filterOption.getSelectedItem() != null;
1739   }
1740
1741   /**
1742    * 
1743    * @return true if the 3D-Beacons query button will/has been displayed
1744    */
1745   public boolean isCanQueryTDB()
1746   {
1747     return canQueryTDB;
1748   }
1749
1750   public boolean isNotQueriedTDBYet()
1751   {
1752     return notQueriedTDBYet;
1753   }
1754
1755   /**
1756    * Open a single structure file for a given sequence
1757    */
1758   public static void openStructureFileForSequence(
1759           StructureSelectionManager ssm, StructureChooser sc,
1760           AlignmentPanel ap, SequenceI seq, boolean prompt,
1761           String sFilename, TFType tft, String paeFilename,
1762           boolean doXferSettings)
1763   {
1764     openStructureFileForSequence(ssm, sc, ap, seq, prompt, sFilename, tft,
1765             paeFilename, false, true, doXferSettings, null);
1766   }
1767
1768   public static StructureViewer openStructureFileForSequence(
1769           StructureSelectionManager ssm, StructureChooser sc,
1770           AlignmentPanel ap, SequenceI seq, boolean prompt,
1771           String sFilename, TFType tft, String paeFilename,
1772           boolean forceHeadless, boolean showRefAnnotations,
1773           boolean doXferSettings, ViewerType viewerType)
1774   {
1775     StructureViewer sv = null;
1776     boolean headless = forceHeadless;
1777     if (sc == null)
1778     {
1779       // headless = true;
1780       prompt = false;
1781       sc = new StructureChooser(new SequenceI[] { seq }, seq, ap, false);
1782     }
1783     if (ssm == null)
1784     {
1785       ssm = ap.getStructureSelectionManager();
1786     }
1787
1788     PDBEntry fileEntry = new AssociatePdbFileWithSeq().associatePdbWithSeq(
1789             sFilename, DataSourceType.FILE, seq, prompt, Desktop.instance,
1790             tft, paeFilename, doXferSettings);
1791
1792     // if headless, "false" in the sc constructor above will avoid GUI behaviour
1793     // in sc.launchStructureViewer()
1794     if (!headless && !(viewerType == null))
1795     {
1796       sv = sc.launchStructureViewer(ssm, new PDBEntry[] { fileEntry }, ap,
1797               new SequenceI[]
1798               { seq }, viewerType);
1799     }
1800
1801     sc.mainFrame.dispose();
1802
1803     if (showRefAnnotations)
1804       showReferenceAnnotationsForSequence(ap.alignFrame, seq);
1805
1806     return sv;
1807   }
1808
1809   public static void showReferenceAnnotationsForSequence(AlignFrame af,
1810           SequenceI sequence)
1811   {
1812     AlignViewport av = af.getCurrentView();
1813     AlignmentI al = av.getAlignment();
1814
1815     List<SequenceI> forSequences = new ArrayList<>();
1816     forSequences.add(sequence);
1817     final Map<SequenceI, List<AlignmentAnnotation>> candidates = new LinkedHashMap<>();
1818     AlignmentUtils.findAddableReferenceAnnotations(forSequences, null,
1819             candidates, al);
1820     final SequenceGroup selectionGroup = av.getSelectionGroup();
1821     AlignmentUtils.addReferenceAnnotations(candidates, al, selectionGroup);
1822     for (AlignmentViewPanel ap : af.getAlignPanels())
1823     {
1824       // required to readjust the height and position of the PAE
1825       // annotation
1826       ap.adjustAnnotationHeight();
1827     }
1828
1829   }
1830 }