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