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