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