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