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