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