JAL-4177 disable ‘Cancel’ as well - since we as yet do not support cancelling the...
[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     btn_cancel.setEnabled(false);
1207     actionsPanel.setEnabled(false);
1208     
1209     final String progress=MessageManager.getString("label.working_ellipsis");
1210     setProgressBar(progress, progress.hashCode());
1211     Runnable viewStruc = new Runnable()
1212     {
1213       @Override
1214       public void run()
1215       {
1216         FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
1217                 .getSelectedItem());
1218         String currentView = selectedFilterOpt.getView();
1219         JTable restable = (currentView == VIEWS_FILTER) ? getResultTable()
1220                 : tbl_local_pdb;
1221
1222         if (currentView == VIEWS_FILTER)
1223         {
1224           int[] selectedRows = restable.getSelectedRows();
1225           PDBEntry[] pdbEntriesToView = new PDBEntry[selectedRows.length];
1226           List<SequenceI> selectedSeqsToView = new ArrayList<>();
1227           pdbEntriesToView = data.collectSelectedRows(restable,
1228                   selectedRows, selectedSeqsToView);
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_LOCAL_PDB)
1236         {
1237           int[] selectedRows = tbl_local_pdb.getSelectedRows();
1238           PDBEntry[] pdbEntriesToView = new PDBEntry[selectedRows.length];
1239           int count = 0;
1240           int pdbIdColIndex = tbl_local_pdb.getColumn("PDB Id")
1241                   .getModelIndex();
1242           int refSeqColIndex = tbl_local_pdb.getColumn("Ref Sequence")
1243                   .getModelIndex();
1244           List<SequenceI> selectedSeqsToView = new ArrayList<>();
1245           for (int row : selectedRows)
1246           {
1247             PDBEntry pdbEntry = ((PDBEntryTableModel) tbl_local_pdb
1248                     .getModel()).getPDBEntryAt(row).getPdbEntry();
1249
1250             pdbEntriesToView[count++] = pdbEntry;
1251             SequenceI selectedSeq = (SequenceI) tbl_local_pdb
1252                     .getValueAt(row, refSeqColIndex);
1253             selectedSeqsToView.add(selectedSeq);
1254           }
1255           SequenceI[] selectedSeqs = selectedSeqsToView
1256                   .toArray(new SequenceI[selectedSeqsToView.size()]);
1257           sViewer = launchStructureViewer(ssm, pdbEntriesToView, ap,
1258                   selectedSeqs);
1259         }
1260         else if (currentView == VIEWS_ENTER_ID)
1261         {
1262           SequenceI userSelectedSeq = ((AssociateSeqOptions) idInputAssSeqPanel
1263                   .getCmb_assSeq().getSelectedItem()).getSequence();
1264           if (userSelectedSeq != null)
1265           {
1266             selectedSequence = userSelectedSeq;
1267           }
1268           String pdbIdStr = txt_search.getText();
1269           PDBEntry pdbEntry = selectedSequence.getPDBEntry(pdbIdStr);
1270           if (pdbEntry == null)
1271           {
1272             pdbEntry = new PDBEntry();
1273             if (pdbIdStr.split(":").length > 1)
1274             {
1275               pdbEntry.setId(pdbIdStr.split(":")[0]);
1276               pdbEntry.setChainCode(
1277                       pdbIdStr.split(":")[1].toUpperCase(Locale.ROOT));
1278             }
1279             else
1280             {
1281               pdbEntry.setId(pdbIdStr);
1282             }
1283             pdbEntry.setType(PDBEntry.Type.PDB);
1284             selectedSequence.getDatasetSequence().addPDBId(pdbEntry);
1285           }
1286
1287           PDBEntry[] pdbEntriesToView = new PDBEntry[] { pdbEntry };
1288           sViewer = launchStructureViewer(ssm, pdbEntriesToView, ap,
1289                   new SequenceI[]
1290                   { selectedSequence });
1291         }
1292         else if (currentView == VIEWS_FROM_FILE)
1293         {
1294           StructureChooser sc = StructureChooser.this;
1295           TFType tft = (TFType) sc.combo_tempFacAs.getSelectedItem();
1296           String paeFilename = sc.localPdbPaeMatrixFileName;
1297           AssociateSeqOptions assSeqOpt = (AssociateSeqOptions) fileChooserAssSeqPanel
1298                   .getCmb_assSeq().getSelectedItem();
1299           SequenceI userSelectedSeq = assSeqOpt.getSequence();
1300           if (userSelectedSeq != null)
1301           {
1302             selectedSequence = userSelectedSeq;
1303           }
1304           String pdbFilename = selectedPdbFileName;
1305
1306           StructureChooser.openStructureFileForSequence(ssm, sc, ap,
1307                   selectedSequence, true, pdbFilename, tft, paeFilename);
1308         }
1309         SwingUtilities.invokeLater(new Runnable()
1310         {
1311           @Override
1312           public void run()
1313           {
1314             setProgressBar("Complete.", progress.hashCode());
1315             closeAction(preferredHeight);
1316             mainFrame.dispose();
1317           }
1318         });
1319       }
1320     };
1321     Thread runner = new Thread(viewStruc);
1322     runner.start();
1323     if (waitUntilFinished)
1324     {
1325       while (sViewer == null ? runner.isAlive()
1326               : (sViewer.sview == null ? true
1327                       : !sViewer.sview.hasMapping()))
1328       {
1329         try
1330         {
1331           Thread.sleep(300);
1332         } catch (InterruptedException ie)
1333         {
1334
1335         }
1336       }
1337     }
1338   }
1339
1340   /**
1341    * Answers a structure viewer (new or existing) configured to superimpose
1342    * added structures or not according to the user's choice
1343    * 
1344    * @param ssm
1345    * @return
1346    */
1347   StructureViewer getTargetedStructureViewer(StructureSelectionManager ssm)
1348   {
1349     Object sv = targetView.getSelectedItem();
1350
1351     return sv == null ? new StructureViewer(ssm) : (StructureViewer) sv;
1352   }
1353
1354   /**
1355    * Adds PDB structures to a new or existing structure viewer
1356    * 
1357    * @param ssm
1358    * @param pdbEntriesToView
1359    * @param alignPanel
1360    * @param sequences
1361    * @return
1362    */
1363   private StructureViewer launchStructureViewer(
1364           StructureSelectionManager ssm, final PDBEntry[] pdbEntriesToView,
1365           final AlignmentPanel alignPanel, SequenceI[] sequences)
1366   {
1367     long progressId = sequences.hashCode();
1368     setProgressBar(MessageManager
1369             .getString("status.launching_3d_structure_viewer"), progressId);
1370     final StructureViewer theViewer = getTargetedStructureViewer(ssm);
1371     boolean superimpose = chk_superpose.isSelected();
1372     theViewer.setSuperpose(superimpose);
1373
1374     /*
1375      * remember user's choice of superimpose or not
1376      */
1377     Cache.setProperty(AUTOSUPERIMPOSE,
1378             Boolean.valueOf(superimpose).toString());
1379
1380     setProgressBar(null, progressId);
1381     if (SiftsSettings.isMapWithSifts())
1382     {
1383       List<SequenceI> seqsWithoutSourceDBRef = new ArrayList<>();
1384       int p = 0;
1385       // TODO: skip PDBEntry:Sequence pairs where PDBEntry doesn't look like a
1386       // real PDB ID. For moment, we can also safely do this if there is already
1387       // a known mapping between the PDBEntry and the sequence.
1388       for (SequenceI seq : sequences)
1389       {
1390         PDBEntry pdbe = pdbEntriesToView[p++];
1391         if (pdbe != null && pdbe.getFile() != null)
1392         {
1393           StructureMapping[] smm = ssm.getMapping(pdbe.getFile());
1394           if (smm != null && smm.length > 0)
1395           {
1396             for (StructureMapping sm : smm)
1397             {
1398               if (sm.getSequence() == seq)
1399               {
1400                 continue;
1401               }
1402             }
1403           }
1404         }
1405         if (seq.getPrimaryDBRefs().isEmpty())
1406         {
1407           seqsWithoutSourceDBRef.add(seq);
1408           continue;
1409         }
1410       }
1411       if (!seqsWithoutSourceDBRef.isEmpty())
1412       {
1413         int y = seqsWithoutSourceDBRef.size();
1414         setProgressBar(MessageManager.formatMessage(
1415                 "status.fetching_dbrefs_for_sequences_without_valid_refs",
1416                 y), progressId);
1417         SequenceI[] seqWithoutSrcDBRef = seqsWithoutSourceDBRef
1418                 .toArray(new SequenceI[y]);
1419         DBRefFetcher dbRefFetcher = new DBRefFetcher(seqWithoutSrcDBRef);
1420         dbRefFetcher.fetchDBRefs(true);
1421
1422         setProgressBar("Fetch complete.", progressId); // todo i18n
1423       }
1424     }
1425     if (pdbEntriesToView.length > 1)
1426     {
1427       setProgressBar(
1428               MessageManager.getString(
1429                       "status.fetching_3d_structures_for_selected_entries"),
1430               progressId);
1431       theViewer.viewStructures(pdbEntriesToView, sequences, alignPanel);
1432     }
1433     else
1434     {
1435       setProgressBar(MessageManager.formatMessage(
1436               "status.fetching_3d_structures_for",
1437               pdbEntriesToView[0].getId()), progressId);
1438       // Can we pass a pre-computeMappinged pdbFile?
1439       theViewer.viewStructures(pdbEntriesToView[0], sequences, alignPanel);
1440     }
1441     setProgressBar(null, progressId);
1442     // remember the last viewer we used...
1443     lastTargetedView = theViewer;
1444     return theViewer;
1445   }
1446
1447   /**
1448    * Populates the combo-box used in associating manually fetched structures to
1449    * a unique sequence when more than one sequence selection is made.
1450    */
1451   @Override
1452   protected void populateCmbAssociateSeqOptions(
1453           JComboBox<AssociateSeqOptions> cmb_assSeq,
1454           JLabel lbl_associateSeq)
1455   {
1456     cmb_assSeq.removeAllItems();
1457     cmb_assSeq.addItem(
1458             new AssociateSeqOptions("-Select Associated Seq-", null));
1459     lbl_associateSeq.setVisible(false);
1460     if (selectedSequences.length > 1)
1461     {
1462       for (SequenceI seq : selectedSequences)
1463       {
1464         cmb_assSeq.addItem(new AssociateSeqOptions(seq));
1465       }
1466     }
1467     else
1468     {
1469       String seqName = selectedSequence.getDisplayId(false);
1470       seqName = seqName.length() <= 40 ? seqName : seqName.substring(0, 39);
1471       lbl_associateSeq.setText(seqName);
1472       lbl_associateSeq.setVisible(true);
1473       cmb_assSeq.setVisible(false);
1474     }
1475   }
1476
1477   protected boolean isStructuresDiscovered()
1478   {
1479     return discoveredStructuresSet != null
1480             && !discoveredStructuresSet.isEmpty();
1481   }
1482
1483   protected int PDB_ID_MIN = 3;// or: (Jalview.isJS() ? 3 : 1); // Bob proposes
1484                                // this.
1485   // Doing a search for "1" or "1c" is valuable?
1486   // Those work but are enormously slow.
1487
1488   @Override
1489   protected void txt_search_ActionPerformed()
1490   {
1491     String text = txt_search.getText().trim();
1492     if (text.length() >= PDB_ID_MIN)
1493       new Thread()
1494       {
1495
1496         @Override
1497         public void run()
1498         {
1499           errorWarning.setLength(0);
1500           isValidPBDEntry = false;
1501           if (text.length() > 0)
1502           {
1503             // TODO move this pdb id search into the PDB specific
1504             // FTSSearchEngine
1505             // for moment, it will work fine as is because it is self-contained
1506             String searchTerm = text.toLowerCase(Locale.ROOT);
1507             searchTerm = searchTerm.split(":")[0];
1508             // System.out.println(">>>>> search term : " + searchTerm);
1509             List<FTSDataColumnI> wantedFields = new ArrayList<>();
1510             FTSRestRequest pdbRequest = new FTSRestRequest();
1511             pdbRequest.setAllowEmptySeq(false);
1512             pdbRequest.setResponseSize(1);
1513             pdbRequest.setFieldToSearchBy("(pdb_id:");
1514             pdbRequest.setWantedFields(wantedFields);
1515             pdbRequest.setSearchTerm(searchTerm + ")");
1516             pdbRequest.setAssociatedSequence(selectedSequence);
1517             FTSRestClientI pdbRestClient = PDBFTSRestClient.getInstance();
1518             wantedFields.add(pdbRestClient.getPrimaryKeyColumn());
1519             FTSRestResponse resultList;
1520             try
1521             {
1522               resultList = pdbRestClient.executeRequest(pdbRequest);
1523             } catch (Exception e)
1524             {
1525               errorWarning.append(e.getMessage());
1526               return;
1527             } finally
1528             {
1529               validateSelections();
1530             }
1531             if (resultList.getSearchSummary() != null
1532                     && resultList.getSearchSummary().size() > 0)
1533             {
1534               isValidPBDEntry = true;
1535             }
1536           }
1537           validateSelections();
1538         }
1539       }.start();
1540   }
1541
1542   @Override
1543   protected void tabRefresh()
1544   {
1545     if (selectedSequences != null)
1546     {
1547       lbl_loading.setVisible(true);
1548       Thread refreshThread = new Thread(new Runnable()
1549       {
1550         @Override
1551         public void run()
1552         {
1553           fetchStructuresMetaData();
1554           // populateFilterComboBox(true, cachedPDBExists);
1555
1556           filterResultSet(
1557                   ((FilterOption) cmb_filterOption.getSelectedItem())
1558                           .getValue());
1559           lbl_loading.setVisible(false);
1560         }
1561       });
1562       refreshThread.start();
1563     }
1564   }
1565
1566   public class PDBEntryTableModel extends AbstractTableModel
1567   {
1568     String[] columns = { "Ref Sequence", "PDB Id", "Chain", "Type",
1569         "File" };
1570
1571     private List<CachedPDB> pdbEntries;
1572
1573     public PDBEntryTableModel(List<CachedPDB> pdbEntries)
1574     {
1575       this.pdbEntries = new ArrayList<>(pdbEntries);
1576     }
1577
1578     @Override
1579     public String getColumnName(int columnIndex)
1580     {
1581       return columns[columnIndex];
1582     }
1583
1584     @Override
1585     public int getRowCount()
1586     {
1587       return pdbEntries.size();
1588     }
1589
1590     @Override
1591     public int getColumnCount()
1592     {
1593       return columns.length;
1594     }
1595
1596     @Override
1597     public boolean isCellEditable(int row, int column)
1598     {
1599       return false;
1600     }
1601
1602     @Override
1603     public Object getValueAt(int rowIndex, int columnIndex)
1604     {
1605       Object value = "??";
1606       CachedPDB entry = pdbEntries.get(rowIndex);
1607       switch (columnIndex)
1608       {
1609       case 0:
1610         value = entry.getSequence();
1611         break;
1612       case 1:
1613         value = entry.getQualifiedId();
1614         break;
1615       case 2:
1616         value = entry.getPdbEntry().getChainCode() == null ? "_"
1617                 : entry.getPdbEntry().getChainCode();
1618         break;
1619       case 3:
1620         value = entry.getPdbEntry().getType();
1621         break;
1622       case 4:
1623         value = entry.getPdbEntry().getFile();
1624         break;
1625       }
1626       return value;
1627     }
1628
1629     @Override
1630     public Class<?> getColumnClass(int columnIndex)
1631     {
1632       return columnIndex == 0 ? SequenceI.class : PDBEntry.class;
1633     }
1634
1635     public CachedPDB getPDBEntryAt(int row)
1636     {
1637       return pdbEntries.get(row);
1638     }
1639
1640   }
1641
1642   private class CachedPDB
1643   {
1644     private SequenceI sequence;
1645
1646     private PDBEntry pdbEntry;
1647
1648     public CachedPDB(SequenceI sequence, PDBEntry pdbEntry)
1649     {
1650       this.sequence = sequence;
1651       this.pdbEntry = pdbEntry;
1652     }
1653
1654     public String getQualifiedId()
1655     {
1656       if (pdbEntry.hasProvider())
1657       {
1658         return pdbEntry.getProvider() + ":" + pdbEntry.getId();
1659       }
1660       return pdbEntry.toString();
1661     }
1662
1663     public SequenceI getSequence()
1664     {
1665       return sequence;
1666     }
1667
1668     public PDBEntry getPdbEntry()
1669     {
1670       return pdbEntry;
1671     }
1672
1673   }
1674
1675   private IProgressIndicator progressBar;
1676
1677   @Override
1678   public void setProgressBar(String message, long id)
1679   {
1680     if (!Platform.isHeadless())
1681       progressBar.setProgressBar(message, id);
1682   }
1683
1684   @Override
1685   public void registerHandler(long id, IProgressIndicatorHandler handler)
1686   {
1687     progressBar.registerHandler(id, handler);
1688   }
1689
1690   @Override
1691   public boolean operationInProgress()
1692   {
1693     return progressBar.operationInProgress();
1694   }
1695
1696   public JalviewStructureDisplayI getOpenedStructureViewer()
1697   {
1698     return sViewer == null ? null : sViewer.sview;
1699   }
1700
1701   @Override
1702   protected void setFTSDocFieldPrefs(FTSDataColumnPreferences newPrefs)
1703   {
1704     data.setDocFieldPrefs(newPrefs);
1705
1706   }
1707
1708   /**
1709    * 
1710    * @return true when all initialisation threads have finished and dialog is
1711    *         visible
1712    */
1713   public boolean isDialogVisible()
1714   {
1715     return mainFrame != null && data != null && cmb_filterOption != null
1716             && mainFrame.isVisible()
1717             && cmb_filterOption.getSelectedItem() != null;
1718   }
1719
1720   /**
1721    * 
1722    * @return true if the 3D-Beacons query button will/has been displayed
1723    */
1724   public boolean isCanQueryTDB()
1725   {
1726     return canQueryTDB;
1727   }
1728
1729   public boolean isNotQueriedTDBYet()
1730   {
1731     return notQueriedTDBYet;
1732   }
1733
1734   /**
1735    * Open a single structure file for a given sequence
1736    */
1737   public static void openStructureFileForSequence(
1738           StructureSelectionManager ssm, StructureChooser sc,
1739           AlignmentPanel ap, SequenceI seq, boolean prompt,
1740           String sFilename, TFType tft, String paeFilename)
1741   {
1742     boolean headless = false;
1743     if (sc == null)
1744     {
1745       headless = true;
1746       sc = new StructureChooser(new SequenceI[] { seq }, seq, ap, false);
1747     }
1748     if (ssm == null)
1749       ssm = ap.getStructureSelectionManager();
1750
1751     PDBEntry fileEntry = new AssociatePdbFileWithSeq().associatePdbWithSeq(
1752             sFilename, DataSourceType.FILE, seq, prompt, Desktop.instance,
1753             tft, paeFilename);
1754
1755     StructureViewer sViewer = sc.launchStructureViewer(ssm,
1756             new PDBEntry[]
1757             { fileEntry }, ap, new SequenceI[] { seq });
1758
1759     if (headless)
1760       sc.mainFrame.dispose();
1761   }
1762 }