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