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