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