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