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