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