JAL-1551 spotlessApply
[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       {
741         PAEContactMatrix.validateContactMatrixFile(fileName);
742       } catch (Exception thr)
743       {
744         JvOptionPane.showInternalMessageDialog(this, MessageManager
745                 .formatMessage("label.couldnt_load_file", new Object[]
746                 { fileName }) + "<br>" + thr.getLocalizedMessage(),
747                 MessageManager.getString("label.error_loading_file"),
748                 JvOptionPane.WARNING_MESSAGE);
749         Console.error("Couldn't import " + fileName + " as a PAE matrix",
750                 thr);
751         return;
752       }
753       localPdbPaeMatrixFileName = fileName;
754       Cache.setProperty("LAST_DIRECTORY", localPdbPaeMatrixFileName);
755     }
756     validateAssociationFromFile();
757   }
758
759   private String guessPAEFilename()
760   {
761     if (selectedPdbFileName.toLowerCase(Locale.ROOT).endsWith(".pdb")
762             || selectedPdbFileName.toLowerCase(Locale.ROOT)
763                     .endsWith(".cif"))
764     {
765       String jsonExt = selectedPdbFileName.substring(0,
766               selectedPdbFileName.length() - 4) + ".json";
767       // AlphaFold naming scheme
768       String guessFile1 = StringUtils.replaceLast(jsonExt, "model",
769               "predicted_aligned_error");
770       // nf-core mode naming scheme
771       String guessFile2 = StringUtils.replaceLast(jsonExt, ".json",
772               "_scores.json");
773       if (new File(guessFile1).exists())
774       {
775         return guessFile1;
776       }
777       else if (new File(jsonExt).exists())
778       {
779         return jsonExt;
780       }
781       else if (new File(guessFile2).exists())
782       {
783         return guessFile2;
784       }
785     }
786     return null;
787   }
788
789   /**
790    * Populates the filter combo-box options dynamically depending on discovered
791    * structures
792    */
793   protected void populateFilterComboBox(boolean haveData,
794           boolean cachedPDBExist)
795   {
796     populateFilterComboBox(haveData, cachedPDBExist, null);
797   }
798
799   /**
800    * Populates the filter combo-box options dynamically depending on discovered
801    * structures
802    */
803   protected void populateFilterComboBox(boolean haveData,
804           boolean cachedPDBExist, FilterOption lastSel)
805   {
806
807     /*
808      * temporarily suspend the change listener behaviour
809      */
810     cmb_filterOption.removeItemListener(this);
811     int selSet = -1;
812     cmb_filterOption.removeAllItems();
813     if (haveData)
814     {
815       List<FilterOption> filters = data
816               .getAvailableFilterOptions(VIEWS_FILTER);
817       data.updateAvailableFilterOptions(VIEWS_FILTER, filters,
818               lastDiscoveredStructuresSet);
819       int p = 0;
820       for (FilterOption filter : filters)
821       {
822         if (lastSel != null && filter.equals(lastSel))
823         {
824           selSet = p;
825         }
826         p++;
827         cmb_filterOption.addItem(filter);
828       }
829     }
830
831     cmb_filterOption.addItem(
832             new FilterOption(MessageManager.getString("label.enter_pdb_id"),
833                     "-", VIEWS_ENTER_ID, false, null));
834     cmb_filterOption.addItem(
835             new FilterOption(MessageManager.getString("label.from_file"),
836                     "-", VIEWS_FROM_FILE, false, null));
837     if (canQueryTDB && notQueriedTDBYet)
838     {
839       btn_queryTDB.setVisible(true);
840       pnl_queryTDB.setVisible(true);
841     }
842
843     if (cachedPDBExist)
844     {
845       FilterOption cachedOption = new FilterOption(
846               MessageManager.getString("label.cached_structures"), "-",
847               VIEWS_LOCAL_PDB, false, null);
848       cmb_filterOption.addItem(cachedOption);
849       if (selSet == -1)
850       {
851         cmb_filterOption.setSelectedItem(cachedOption);
852       }
853     }
854     if (selSet > -1)
855     {
856       cmb_filterOption.setSelectedIndex(selSet);
857     }
858     cmb_filterOption.addItemListener(this);
859   }
860
861   /**
862    * Updates the displayed view based on the selected filter option
863    */
864   protected void updateCurrentView()
865   {
866     FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
867             .getSelectedItem());
868
869     if (lastSelected == selectedFilterOpt)
870     {
871       // don't need to do anything, probably
872       return;
873     }
874     // otherwise, record selection
875     // and update the layout and dialog accordingly
876     lastSelected = selectedFilterOpt;
877
878     layout_switchableViews.show(pnl_switchableViews,
879             selectedFilterOpt.getView());
880     String filterTitle = mainFrame.getTitle();
881     mainFrame.setTitle(frameTitle);
882     chk_invertFilter.setVisible(false);
883
884     if (selectedFilterOpt.getView() == VIEWS_FILTER)
885     {
886       mainFrame.setTitle(filterTitle);
887       // TDB Query has no invert as yet
888       chk_invertFilter.setVisible(selectedFilterOpt
889               .getQuerySource() instanceof PDBStructureChooserQuerySource);
890
891       if (data != selectedFilterOpt.getQuerySource()
892               || data.needsRefetch(selectedFilterOpt))
893       {
894         data = selectedFilterOpt.getQuerySource();
895         // rebuild the views completely, since prefs will also change
896         tabRefresh();
897         return;
898       }
899       else
900       {
901         filterResultSet(selectedFilterOpt.getValue());
902       }
903     }
904     else if (selectedFilterOpt.getView() == VIEWS_ENTER_ID
905             || selectedFilterOpt.getView() == VIEWS_FROM_FILE)
906     {
907       mainFrame.setTitle(MessageManager
908               .getString("label.structure_chooser_manual_association"));
909       idInputAssSeqPanel.loadCmbAssSeq();
910       fileChooserAssSeqPanel.loadCmbAssSeq();
911     }
912     validateSelections();
913   }
914
915   /**
916    * Validates user selection and enables the 'Add' and 'New View' buttons if
917    * all parameters are correct (the Add button will only be visible if there is
918    * at least one existing structure viewer open). This basically means at least
919    * one structure selected and no error messages.
920    * <p>
921    * The 'Superpose Structures' option is enabled if either more than one
922    * structure is selected, or the 'Add' to existing view option is enabled, and
923    * disabled if the only option is to open a new view of a single structure.
924    */
925   @Override
926   protected void validateSelections()
927   {
928     FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
929             .getSelectedItem());
930     btn_add.setEnabled(false);
931     String currentView = selectedFilterOpt.getView();
932     int selectedCount = 0;
933     if (currentView == VIEWS_FILTER)
934     {
935       selectedCount = getResultTable().getSelectedRows().length;
936       if (selectedCount > 0)
937       {
938         btn_add.setEnabled(true);
939       }
940     }
941     else if (currentView == VIEWS_LOCAL_PDB)
942     {
943       selectedCount = tbl_local_pdb.getSelectedRows().length;
944       if (selectedCount > 0)
945       {
946         btn_add.setEnabled(true);
947       }
948     }
949     else if (currentView == VIEWS_ENTER_ID)
950     {
951       validateAssociationEnterPdb();
952     }
953     else if (currentView == VIEWS_FROM_FILE)
954     {
955       validateAssociationFromFile();
956     }
957
958     btn_newView.setEnabled(btn_add.isEnabled());
959
960     /*
961      * enable 'Superpose' option if more than one structure is selected,
962      * or there are view(s) available to add structure(s) to
963      */
964     chk_superpose
965             .setEnabled(selectedCount > 1 || targetView.getItemCount() > 0);
966   }
967
968   @Override
969   protected boolean showPopupFor(int selectedRow, int x, int y)
970   {
971     FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
972             .getSelectedItem());
973     String currentView = selectedFilterOpt.getView();
974
975     if (currentView == VIEWS_FILTER
976             && data instanceof ThreeDBStructureChooserQuerySource)
977     {
978
979       TDB_FTSData row = ((ThreeDBStructureChooserQuerySource) data)
980               .getFTSDataFor(getResultTable(), selectedRow,
981                       discoveredStructuresSet);
982       String pageUrl = row.getModelViewUrl();
983       JPopupMenu popup = new JPopupMenu("3D Beacons");
984       JMenuItem viewUrl = new JMenuItem("View model web page");
985       viewUrl.addActionListener(new ActionListener()
986       {
987         @Override
988         public void actionPerformed(ActionEvent e)
989         {
990           Desktop.showUrl(pageUrl);
991         }
992       });
993       popup.add(viewUrl);
994       SwingUtilities.invokeLater(new Runnable()
995       {
996         @Override
997         public void run()
998         {
999           popup.show(getResultTable(), x, y);
1000         }
1001       });
1002       return true;
1003     }
1004     // event not handled by us
1005     return false;
1006   }
1007
1008   /**
1009    * Validates inputs from the Manual PDB entry panel
1010    */
1011   protected void validateAssociationEnterPdb()
1012   {
1013     AssociateSeqOptions assSeqOpt = (AssociateSeqOptions) idInputAssSeqPanel
1014             .getCmb_assSeq().getSelectedItem();
1015     lbl_pdbManualFetchStatus.setIcon(errorImage);
1016     lbl_pdbManualFetchStatus.setToolTipText("");
1017     if (txt_search.getText().length() > 0)
1018     {
1019       lbl_pdbManualFetchStatus.setToolTipText(JvSwingUtils.wrapTooltip(true,
1020               MessageManager.formatMessage("info.no_pdb_entry_found_for",
1021                       txt_search.getText())));
1022     }
1023
1024     if (errorWarning.length() > 0)
1025     {
1026       lbl_pdbManualFetchStatus.setIcon(warningImage);
1027       lbl_pdbManualFetchStatus.setToolTipText(
1028               JvSwingUtils.wrapTooltip(true, errorWarning.toString()));
1029     }
1030
1031     if (selectedSequences.length == 1 || !assSeqOpt.getName()
1032             .equalsIgnoreCase("-Select Associated Seq-"))
1033     {
1034       txt_search.setEnabled(true);
1035       if (isValidPBDEntry)
1036       {
1037         btn_add.setEnabled(true);
1038         lbl_pdbManualFetchStatus.setToolTipText("");
1039         lbl_pdbManualFetchStatus.setIcon(goodImage);
1040       }
1041     }
1042     else
1043     {
1044       txt_search.setEnabled(false);
1045       lbl_pdbManualFetchStatus.setIcon(errorImage);
1046     }
1047   }
1048
1049   /**
1050    * Validates inputs for the manual PDB file selection options
1051    */
1052   protected void validateAssociationFromFile()
1053   {
1054     AssociateSeqOptions assSeqOpt = (AssociateSeqOptions) fileChooserAssSeqPanel
1055             .getCmb_assSeq().getSelectedItem();
1056     // lbl_fromFileStatus.setIcon(errorImage);
1057     String pdbFileString = "";
1058     String pdbFileTooltip = "";
1059     if (selectedSequences.length == 1 || (assSeqOpt != null && !assSeqOpt
1060             .getName().equalsIgnoreCase("-Select Associated Seq-")))
1061     {
1062       btn_pdbFromFile.setEnabled(true);
1063       if (selectedPdbFileName != null && selectedPdbFileName.length() > 0)
1064       {
1065         btn_add.setEnabled(true);
1066         // lbl_fromFileStatus.setIcon(goodImage);
1067         pdbFileString = new File(selectedPdbFileName).getName();
1068         pdbFileTooltip = new File(selectedPdbFileName).getAbsolutePath();
1069         setPdbOptionsEnabled(true);
1070       }
1071       else
1072       {
1073         pdbFileString = MessageManager.getString("label.none");
1074         pdbFileTooltip = MessageManager.getString("label.nothing_selected");
1075         setPdbOptionsEnabled(false);
1076       }
1077     }
1078     else
1079     {
1080       btn_pdbFromFile.setEnabled(false);
1081       setPdbOptionsEnabled(false);
1082       // lbl_fromFileStatus.setIcon(errorImage);
1083       pdbFileString = MessageManager.getString("label.none");
1084       pdbFileTooltip = MessageManager.getString("label.nothing_selected");
1085     }
1086     lbl_pdbFile.setText(pdbFileString);
1087     lbl_pdbFile.setToolTipText(pdbFileTooltip);
1088
1089     // PAE file choice
1090     String paeFileString = "";
1091     String paeFileTooltip = "";
1092     if (localPdbPaeMatrixFileName != null
1093             && localPdbPaeMatrixFileName.length() > 0)
1094     {
1095       paeFileString = new File(localPdbPaeMatrixFileName).getName();
1096       paeFileTooltip = new File(localPdbPaeMatrixFileName)
1097               .getAbsolutePath();
1098     }
1099     else
1100     {
1101       paeFileString = MessageManager.getString("label.none");
1102       paeFileTooltip = MessageManager.getString("label.nothing_selected");
1103     }
1104     lbl_paeFile.setText(paeFileString);
1105     lbl_paeFile.setToolTipText(paeFileTooltip);
1106   }
1107
1108   @Override
1109   protected void cmbAssSeqStateChanged()
1110   {
1111     validateSelections();
1112   }
1113
1114   private FilterOption lastSelected = null;
1115
1116   /**
1117    * Handles the state change event for the 'filter' combo-box and 'invert'
1118    * check-box
1119    */
1120   @Override
1121   protected void stateChanged(ItemEvent e)
1122   {
1123     if (e.getSource() instanceof JCheckBox)
1124     {
1125       updateCurrentView();
1126     }
1127     else
1128     {
1129       if (e.getStateChange() == ItemEvent.SELECTED)
1130       {
1131         updateCurrentView();
1132       }
1133     }
1134
1135   }
1136
1137   /**
1138    * select structures for viewing by their PDB IDs
1139    * 
1140    * @param pdbids
1141    * @return true if structures were found and marked as selected
1142    */
1143   public boolean selectStructure(String... pdbids)
1144   {
1145     boolean found = false;
1146
1147     FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
1148             .getSelectedItem());
1149     String currentView = selectedFilterOpt.getView();
1150     JTable restable = (currentView == VIEWS_FILTER) ? getResultTable()
1151             : (currentView == VIEWS_LOCAL_PDB) ? tbl_local_pdb : null;
1152
1153     if (restable == null)
1154     {
1155       // can't select (enter PDB ID, or load file - need to also select which
1156       // sequence to associate with)
1157       return false;
1158     }
1159
1160     int pdbIdColIndex = restable.getColumn("PDB Id").getModelIndex();
1161     for (int r = 0; r < restable.getRowCount(); r++)
1162     {
1163       for (int p = 0; p < pdbids.length; p++)
1164       {
1165         if (String.valueOf(restable.getValueAt(r, pdbIdColIndex))
1166                 .equalsIgnoreCase(pdbids[p]))
1167         {
1168           restable.setRowSelectionInterval(r, r);
1169           found = true;
1170         }
1171       }
1172     }
1173     return found;
1174   }
1175
1176   /**
1177    * Handles the 'New View' action
1178    */
1179   @Override
1180   protected void newView_ActionPerformed()
1181   {
1182     targetView.setSelectedItem(null);
1183     showStructures(false);
1184   }
1185
1186   /**
1187    * Handles the 'Add to existing viewer' action
1188    */
1189   @Override
1190   protected void add_ActionPerformed()
1191   {
1192     showStructures(false);
1193   }
1194
1195   /**
1196    * structure viewer opened by this dialog, or null
1197    */
1198   private StructureViewer sViewer = null;
1199
1200   public void showStructures(boolean waitUntilFinished)
1201   {
1202
1203     final StructureSelectionManager ssm = ap.getStructureSelectionManager();
1204
1205     final int preferredHeight = pnl_filter.getHeight();
1206     btn_add.setEnabled(false);
1207     btn_newView.setEnabled(false);
1208     btn_cancel.setEnabled(false);
1209     actionsPanel.setEnabled(false);
1210
1211     final String progress = MessageManager
1212             .getString("label.working_ellipsis");
1213     setProgressBar(progress, progress.hashCode());
1214     Runnable viewStruc = new Runnable()
1215     {
1216       @Override
1217       public void run()
1218       {
1219         FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
1220                 .getSelectedItem());
1221         String currentView = selectedFilterOpt.getView();
1222         JTable restable = (currentView == VIEWS_FILTER) ? getResultTable()
1223                 : tbl_local_pdb;
1224
1225         if (currentView == VIEWS_FILTER)
1226         {
1227           int[] selectedRows = restable.getSelectedRows();
1228           PDBEntry[] pdbEntriesToView = new PDBEntry[selectedRows.length];
1229           List<SequenceI> selectedSeqsToView = new ArrayList<>();
1230           pdbEntriesToView = data.collectSelectedRows(restable,
1231                   selectedRows, selectedSeqsToView);
1232
1233           SequenceI[] selectedSeqs = selectedSeqsToView
1234                   .toArray(new SequenceI[selectedSeqsToView.size()]);
1235           sViewer = launchStructureViewer(ssm, pdbEntriesToView, ap,
1236                   selectedSeqs);
1237         }
1238         else if (currentView == VIEWS_LOCAL_PDB)
1239         {
1240           int[] selectedRows = tbl_local_pdb.getSelectedRows();
1241           PDBEntry[] pdbEntriesToView = new PDBEntry[selectedRows.length];
1242           int count = 0;
1243           int pdbIdColIndex = tbl_local_pdb.getColumn("PDB Id")
1244                   .getModelIndex();
1245           int refSeqColIndex = tbl_local_pdb.getColumn("Ref Sequence")
1246                   .getModelIndex();
1247           List<SequenceI> selectedSeqsToView = new ArrayList<>();
1248           for (int row : selectedRows)
1249           {
1250             PDBEntry pdbEntry = ((PDBEntryTableModel) tbl_local_pdb
1251                     .getModel()).getPDBEntryAt(row).getPdbEntry();
1252
1253             pdbEntriesToView[count++] = pdbEntry;
1254             SequenceI selectedSeq = (SequenceI) tbl_local_pdb
1255                     .getValueAt(row, refSeqColIndex);
1256             selectedSeqsToView.add(selectedSeq);
1257           }
1258           SequenceI[] selectedSeqs = selectedSeqsToView
1259                   .toArray(new SequenceI[selectedSeqsToView.size()]);
1260           sViewer = launchStructureViewer(ssm, pdbEntriesToView, ap,
1261                   selectedSeqs);
1262         }
1263         else if (currentView == VIEWS_ENTER_ID)
1264         {
1265           SequenceI userSelectedSeq = ((AssociateSeqOptions) idInputAssSeqPanel
1266                   .getCmb_assSeq().getSelectedItem()).getSequence();
1267           if (userSelectedSeq != null)
1268           {
1269             selectedSequence = userSelectedSeq;
1270           }
1271           String pdbIdStr = txt_search.getText();
1272           PDBEntry pdbEntry = selectedSequence.getPDBEntry(pdbIdStr);
1273           if (pdbEntry == null)
1274           {
1275             pdbEntry = new PDBEntry();
1276             if (pdbIdStr.split(":").length > 1)
1277             {
1278               pdbEntry.setId(pdbIdStr.split(":")[0]);
1279               pdbEntry.setChainCode(
1280                       pdbIdStr.split(":")[1].toUpperCase(Locale.ROOT));
1281             }
1282             else
1283             {
1284               pdbEntry.setId(pdbIdStr);
1285             }
1286             pdbEntry.setType(PDBEntry.Type.PDB);
1287             selectedSequence.getDatasetSequence().addPDBId(pdbEntry);
1288           }
1289
1290           PDBEntry[] pdbEntriesToView = new PDBEntry[] { pdbEntry };
1291           sViewer = launchStructureViewer(ssm, pdbEntriesToView, ap,
1292                   new SequenceI[]
1293                   { selectedSequence });
1294         }
1295         else if (currentView == VIEWS_FROM_FILE)
1296         {
1297           StructureChooser sc = StructureChooser.this;
1298           TFType tft = (TFType) sc.combo_tempFacAs.getSelectedItem();
1299           String paeFilename = sc.localPdbPaeMatrixFileName;
1300           AssociateSeqOptions assSeqOpt = (AssociateSeqOptions) fileChooserAssSeqPanel
1301                   .getCmb_assSeq().getSelectedItem();
1302           SequenceI userSelectedSeq = assSeqOpt.getSequence();
1303           if (userSelectedSeq != null)
1304           {
1305             selectedSequence = userSelectedSeq;
1306           }
1307           String pdbFilename = selectedPdbFileName;
1308
1309           StructureChooser.openStructureFileForSequence(ssm, sc, ap,
1310                   selectedSequence, true, pdbFilename, tft, paeFilename);
1311         }
1312         SwingUtilities.invokeLater(new Runnable()
1313         {
1314           @Override
1315           public void run()
1316           {
1317             setProgressBar("Complete.", progress.hashCode());
1318             closeAction(preferredHeight);
1319             mainFrame.dispose();
1320           }
1321         });
1322       }
1323     };
1324     Thread runner = new Thread(viewStruc);
1325     runner.start();
1326     if (waitUntilFinished)
1327     {
1328       while (sViewer == null ? runner.isAlive()
1329               : (sViewer.sview == null ? true
1330                       : !sViewer.sview.hasMapping()))
1331       {
1332         try
1333         {
1334           Thread.sleep(300);
1335         } catch (InterruptedException ie)
1336         {
1337
1338         }
1339       }
1340     }
1341   }
1342
1343   /**
1344    * Answers a structure viewer (new or existing) configured to superimpose
1345    * added structures or not according to the user's choice
1346    * 
1347    * @param ssm
1348    * @return
1349    */
1350   StructureViewer getTargetedStructureViewer(StructureSelectionManager ssm)
1351   {
1352     Object sv = targetView.getSelectedItem();
1353
1354     return sv == null ? new StructureViewer(ssm) : (StructureViewer) sv;
1355   }
1356
1357   /**
1358    * Adds PDB structures to a new or existing structure viewer
1359    * 
1360    * @param ssm
1361    * @param pdbEntriesToView
1362    * @param alignPanel
1363    * @param sequences
1364    * @return
1365    */
1366   private StructureViewer launchStructureViewer(
1367           StructureSelectionManager ssm, final PDBEntry[] pdbEntriesToView,
1368           final AlignmentPanel alignPanel, SequenceI[] sequences)
1369   {
1370     long progressId = sequences.hashCode();
1371     setProgressBar(MessageManager
1372             .getString("status.launching_3d_structure_viewer"), progressId);
1373     final StructureViewer theViewer = getTargetedStructureViewer(ssm);
1374     boolean superimpose = chk_superpose.isSelected();
1375     theViewer.setSuperpose(superimpose);
1376
1377     /*
1378      * remember user's choice of superimpose or not
1379      */
1380     Cache.setProperty(AUTOSUPERIMPOSE,
1381             Boolean.valueOf(superimpose).toString());
1382
1383     setProgressBar(null, progressId);
1384     if (SiftsSettings.isMapWithSifts())
1385     {
1386       List<SequenceI> seqsWithoutSourceDBRef = new ArrayList<>();
1387       int p = 0;
1388       // TODO: skip PDBEntry:Sequence pairs where PDBEntry doesn't look like a
1389       // real PDB ID. For moment, we can also safely do this if there is already
1390       // a known mapping between the PDBEntry and the sequence.
1391       for (SequenceI seq : sequences)
1392       {
1393         PDBEntry pdbe = pdbEntriesToView[p++];
1394         if (pdbe != null && pdbe.getFile() != null)
1395         {
1396           StructureMapping[] smm = ssm.getMapping(pdbe.getFile());
1397           if (smm != null && smm.length > 0)
1398           {
1399             for (StructureMapping sm : smm)
1400             {
1401               if (sm.getSequence() == seq)
1402               {
1403                 continue;
1404               }
1405             }
1406           }
1407         }
1408         if (seq.getPrimaryDBRefs().isEmpty())
1409         {
1410           seqsWithoutSourceDBRef.add(seq);
1411           continue;
1412         }
1413       }
1414       if (!seqsWithoutSourceDBRef.isEmpty())
1415       {
1416         int y = seqsWithoutSourceDBRef.size();
1417         setProgressBar(MessageManager.formatMessage(
1418                 "status.fetching_dbrefs_for_sequences_without_valid_refs",
1419                 y), progressId);
1420         SequenceI[] seqWithoutSrcDBRef = seqsWithoutSourceDBRef
1421                 .toArray(new SequenceI[y]);
1422         DBRefFetcher dbRefFetcher = new DBRefFetcher(seqWithoutSrcDBRef);
1423         dbRefFetcher.fetchDBRefs(true);
1424
1425         setProgressBar("Fetch complete.", progressId); // todo i18n
1426       }
1427     }
1428     if (pdbEntriesToView.length > 1)
1429     {
1430       setProgressBar(
1431               MessageManager.getString(
1432                       "status.fetching_3d_structures_for_selected_entries"),
1433               progressId);
1434       theViewer.viewStructures(pdbEntriesToView, sequences, alignPanel);
1435     }
1436     else
1437     {
1438       setProgressBar(MessageManager.formatMessage(
1439               "status.fetching_3d_structures_for",
1440               pdbEntriesToView[0].getId()), progressId);
1441       // Can we pass a pre-computeMappinged pdbFile?
1442       theViewer.viewStructures(pdbEntriesToView[0], sequences, alignPanel);
1443     }
1444     setProgressBar(null, progressId);
1445     // remember the last viewer we used...
1446     lastTargetedView = theViewer;
1447     return theViewer;
1448   }
1449
1450   /**
1451    * Populates the combo-box used in associating manually fetched structures to
1452    * a unique sequence when more than one sequence selection is made.
1453    */
1454   @Override
1455   protected void populateCmbAssociateSeqOptions(
1456           JComboBox<AssociateSeqOptions> cmb_assSeq,
1457           JLabel lbl_associateSeq)
1458   {
1459     cmb_assSeq.removeAllItems();
1460     cmb_assSeq.addItem(
1461             new AssociateSeqOptions("-Select Associated Seq-", null));
1462     lbl_associateSeq.setVisible(false);
1463     if (selectedSequences.length > 1)
1464     {
1465       for (SequenceI seq : selectedSequences)
1466       {
1467         cmb_assSeq.addItem(new AssociateSeqOptions(seq));
1468       }
1469     }
1470     else
1471     {
1472       String seqName = selectedSequence.getDisplayId(false);
1473       seqName = seqName.length() <= 40 ? seqName : seqName.substring(0, 39);
1474       lbl_associateSeq.setText(seqName);
1475       lbl_associateSeq.setVisible(true);
1476       cmb_assSeq.setVisible(false);
1477     }
1478   }
1479
1480   protected boolean isStructuresDiscovered()
1481   {
1482     return discoveredStructuresSet != null
1483             && !discoveredStructuresSet.isEmpty();
1484   }
1485
1486   protected int PDB_ID_MIN = 3;// or: (Jalview.isJS() ? 3 : 1); // Bob proposes
1487                                // this.
1488   // Doing a search for "1" or "1c" is valuable?
1489   // Those work but are enormously slow.
1490
1491   @Override
1492   protected void txt_search_ActionPerformed()
1493   {
1494     String text = txt_search.getText().trim();
1495     if (text.length() >= PDB_ID_MIN)
1496       new Thread()
1497       {
1498
1499         @Override
1500         public void run()
1501         {
1502           errorWarning.setLength(0);
1503           isValidPBDEntry = false;
1504           if (text.length() > 0)
1505           {
1506             // TODO move this pdb id search into the PDB specific
1507             // FTSSearchEngine
1508             // for moment, it will work fine as is because it is self-contained
1509             String searchTerm = text.toLowerCase(Locale.ROOT);
1510             searchTerm = searchTerm.split(":")[0];
1511             // System.out.println(">>>>> search term : " + searchTerm);
1512             List<FTSDataColumnI> wantedFields = new ArrayList<>();
1513             FTSRestRequest pdbRequest = new FTSRestRequest();
1514             pdbRequest.setAllowEmptySeq(false);
1515             pdbRequest.setResponseSize(1);
1516             pdbRequest.setFieldToSearchBy("(pdb_id:");
1517             pdbRequest.setWantedFields(wantedFields);
1518             pdbRequest.setSearchTerm(searchTerm + ")");
1519             pdbRequest.setAssociatedSequence(selectedSequence);
1520             FTSRestClientI pdbRestClient = PDBFTSRestClient.getInstance();
1521             wantedFields.add(pdbRestClient.getPrimaryKeyColumn());
1522             FTSRestResponse resultList;
1523             try
1524             {
1525               resultList = pdbRestClient.executeRequest(pdbRequest);
1526             } catch (Exception e)
1527             {
1528               errorWarning.append(e.getMessage());
1529               return;
1530             } finally
1531             {
1532               validateSelections();
1533             }
1534             if (resultList.getSearchSummary() != null
1535                     && resultList.getSearchSummary().size() > 0)
1536             {
1537               isValidPBDEntry = true;
1538             }
1539           }
1540           validateSelections();
1541         }
1542       }.start();
1543   }
1544
1545   @Override
1546   protected void tabRefresh()
1547   {
1548     if (selectedSequences != null)
1549     {
1550       lbl_loading.setVisible(true);
1551       Thread refreshThread = new Thread(new Runnable()
1552       {
1553         @Override
1554         public void run()
1555         {
1556           fetchStructuresMetaData();
1557           // populateFilterComboBox(true, cachedPDBExists);
1558
1559           filterResultSet(
1560                   ((FilterOption) cmb_filterOption.getSelectedItem())
1561                           .getValue());
1562           lbl_loading.setVisible(false);
1563         }
1564       });
1565       refreshThread.start();
1566     }
1567   }
1568
1569   public class PDBEntryTableModel extends AbstractTableModel
1570   {
1571     String[] columns = { "Ref Sequence", "PDB Id", "Chain", "Type",
1572         "File" };
1573
1574     private List<CachedPDB> pdbEntries;
1575
1576     public PDBEntryTableModel(List<CachedPDB> pdbEntries)
1577     {
1578       this.pdbEntries = new ArrayList<>(pdbEntries);
1579     }
1580
1581     @Override
1582     public String getColumnName(int columnIndex)
1583     {
1584       return columns[columnIndex];
1585     }
1586
1587     @Override
1588     public int getRowCount()
1589     {
1590       return pdbEntries.size();
1591     }
1592
1593     @Override
1594     public int getColumnCount()
1595     {
1596       return columns.length;
1597     }
1598
1599     @Override
1600     public boolean isCellEditable(int row, int column)
1601     {
1602       return false;
1603     }
1604
1605     @Override
1606     public Object getValueAt(int rowIndex, int columnIndex)
1607     {
1608       Object value = "??";
1609       CachedPDB entry = pdbEntries.get(rowIndex);
1610       switch (columnIndex)
1611       {
1612       case 0:
1613         value = entry.getSequence();
1614         break;
1615       case 1:
1616         value = entry.getQualifiedId();
1617         break;
1618       case 2:
1619         value = entry.getPdbEntry().getChainCode() == null ? "_"
1620                 : entry.getPdbEntry().getChainCode();
1621         break;
1622       case 3:
1623         value = entry.getPdbEntry().getType();
1624         break;
1625       case 4:
1626         value = entry.getPdbEntry().getFile();
1627         break;
1628       }
1629       return value;
1630     }
1631
1632     @Override
1633     public Class<?> getColumnClass(int columnIndex)
1634     {
1635       return columnIndex == 0 ? SequenceI.class : PDBEntry.class;
1636     }
1637
1638     public CachedPDB getPDBEntryAt(int row)
1639     {
1640       return pdbEntries.get(row);
1641     }
1642
1643   }
1644
1645   private class CachedPDB
1646   {
1647     private SequenceI sequence;
1648
1649     private PDBEntry pdbEntry;
1650
1651     public CachedPDB(SequenceI sequence, PDBEntry pdbEntry)
1652     {
1653       this.sequence = sequence;
1654       this.pdbEntry = pdbEntry;
1655     }
1656
1657     public String getQualifiedId()
1658     {
1659       if (pdbEntry.hasProvider())
1660       {
1661         return pdbEntry.getProvider() + ":" + pdbEntry.getId();
1662       }
1663       return pdbEntry.toString();
1664     }
1665
1666     public SequenceI getSequence()
1667     {
1668       return sequence;
1669     }
1670
1671     public PDBEntry getPdbEntry()
1672     {
1673       return pdbEntry;
1674     }
1675
1676   }
1677
1678   private IProgressIndicator progressBar;
1679
1680   @Override
1681   public void setProgressBar(String message, long id)
1682   {
1683     if (!Platform.isHeadless())
1684       progressBar.setProgressBar(message, id);
1685   }
1686
1687   @Override
1688   public void registerHandler(long id, IProgressIndicatorHandler handler)
1689   {
1690     progressBar.registerHandler(id, handler);
1691   }
1692
1693   @Override
1694   public boolean operationInProgress()
1695   {
1696     return progressBar.operationInProgress();
1697   }
1698
1699   public JalviewStructureDisplayI getOpenedStructureViewer()
1700   {
1701     return sViewer == null ? null : sViewer.sview;
1702   }
1703
1704   @Override
1705   protected void setFTSDocFieldPrefs(FTSDataColumnPreferences newPrefs)
1706   {
1707     data.setDocFieldPrefs(newPrefs);
1708
1709   }
1710
1711   /**
1712    * 
1713    * @return true when all initialisation threads have finished and dialog is
1714    *         visible
1715    */
1716   public boolean isDialogVisible()
1717   {
1718     return mainFrame != null && data != null && cmb_filterOption != null
1719             && mainFrame.isVisible()
1720             && cmb_filterOption.getSelectedItem() != null;
1721   }
1722
1723   /**
1724    * 
1725    * @return true if the 3D-Beacons query button will/has been displayed
1726    */
1727   public boolean isCanQueryTDB()
1728   {
1729     return canQueryTDB;
1730   }
1731
1732   public boolean isNotQueriedTDBYet()
1733   {
1734     return notQueriedTDBYet;
1735   }
1736
1737   /**
1738    * Open a single structure file for a given sequence
1739    */
1740   public static void openStructureFileForSequence(
1741           StructureSelectionManager ssm, StructureChooser sc,
1742           AlignmentPanel ap, SequenceI seq, boolean prompt,
1743           String sFilename, TFType tft, String paeFilename)
1744   {
1745     boolean headless = false;
1746     if (sc == null)
1747     {
1748       headless = true;
1749       sc = new StructureChooser(new SequenceI[] { seq }, seq, ap, false);
1750     }
1751     if (ssm == null)
1752       ssm = ap.getStructureSelectionManager();
1753
1754     PDBEntry fileEntry = new AssociatePdbFileWithSeq().associatePdbWithSeq(
1755             sFilename, DataSourceType.FILE, seq, prompt, Desktop.instance,
1756             tft, paeFilename);
1757
1758     StructureViewer sViewer = sc.launchStructureViewer(ssm,
1759             new PDBEntry[]
1760             { fileEntry }, ap, new SequenceI[] { seq });
1761
1762     if (headless)
1763       sc.mainFrame.dispose();
1764   }
1765 }