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