JAL-4310 let the user know when model pages are not available for a 3d-beacons struct...
[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       
1012       JPopupMenu popup = new JPopupMenu("3D Beacons");
1013       JMenuItem viewUrl = new JMenuItem("View model web page");
1014       if (pageUrl == null || "".equals(pageUrl.trim())) {
1015         viewUrl.setEnabled(false);
1016         viewUrl.setText("No model page available.");
1017       }
1018       viewUrl.addActionListener(new ActionListener()
1019       {
1020         @Override
1021         public void actionPerformed(ActionEvent e)
1022         {
1023           Desktop.showUrl(pageUrl);
1024         }
1025       });
1026       popup.add(viewUrl);
1027       SwingUtilities.invokeLater(new Runnable()
1028       {
1029         @Override
1030         public void run()
1031         {
1032           popup.show(getResultTable(), x, y);
1033         }
1034       });
1035       return true;
1036     }
1037     // event not handled by us
1038     return false;
1039   }
1040
1041   /**
1042    * Validates inputs from the Manual PDB entry panel
1043    */
1044   protected void validateAssociationEnterPdb()
1045   {
1046     AssociateSeqOptions assSeqOpt = (AssociateSeqOptions) idInputAssSeqPanel
1047             .getCmb_assSeq().getSelectedItem();
1048     lbl_pdbManualFetchStatus.setIcon(errorImage);
1049     lbl_pdbManualFetchStatus.setToolTipText("");
1050     if (txt_search.getText().length() > 0)
1051     {
1052       lbl_pdbManualFetchStatus.setToolTipText(JvSwingUtils.wrapTooltip(true,
1053               MessageManager.formatMessage("info.no_pdb_entry_found_for",
1054                       txt_search.getText())));
1055     }
1056
1057     if (errorWarning.length() > 0)
1058     {
1059       lbl_pdbManualFetchStatus.setIcon(warningImage);
1060       lbl_pdbManualFetchStatus.setToolTipText(
1061               JvSwingUtils.wrapTooltip(true, errorWarning.toString()));
1062     }
1063
1064     if (selectedSequences.length == 1 || !assSeqOpt.getName()
1065             .equalsIgnoreCase("-Select Associated Seq-"))
1066     {
1067       txt_search.setEnabled(true);
1068       if (isValidPBDEntry)
1069       {
1070         btn_add.setEnabled(true);
1071         lbl_pdbManualFetchStatus.setToolTipText("");
1072         lbl_pdbManualFetchStatus.setIcon(goodImage);
1073       }
1074     }
1075     else
1076     {
1077       txt_search.setEnabled(false);
1078       lbl_pdbManualFetchStatus.setIcon(errorImage);
1079     }
1080   }
1081
1082   /**
1083    * Validates inputs for the manual PDB file selection options
1084    */
1085   protected void validateAssociationFromFile()
1086   {
1087     AssociateSeqOptions assSeqOpt = (AssociateSeqOptions) fileChooserAssSeqPanel
1088             .getCmb_assSeq().getSelectedItem();
1089     // lbl_fromFileStatus.setIcon(errorImage);
1090     String pdbFileString = "";
1091     String pdbFileTooltip = "";
1092     if (selectedSequences.length == 1 || (assSeqOpt != null && !assSeqOpt
1093             .getName().equalsIgnoreCase("-Select Associated Seq-")))
1094     {
1095       btn_pdbFromFile.setEnabled(true);
1096       if (selectedPdbFileName != null && selectedPdbFileName.length() > 0)
1097       {
1098         btn_add.setEnabled(true);
1099         // lbl_fromFileStatus.setIcon(goodImage);
1100         pdbFileString = new File(selectedPdbFileName).getName();
1101         pdbFileTooltip = new File(selectedPdbFileName).getAbsolutePath();
1102         setPdbOptionsEnabled(true);
1103       }
1104       else
1105       {
1106         pdbFileString = MessageManager.getString("label.none");
1107         pdbFileTooltip = MessageManager.getString("label.nothing_selected");
1108         setPdbOptionsEnabled(false);
1109       }
1110     }
1111     else
1112     {
1113       btn_pdbFromFile.setEnabled(false);
1114       setPdbOptionsEnabled(false);
1115       // lbl_fromFileStatus.setIcon(errorImage);
1116       pdbFileString = MessageManager.getString("label.none");
1117       pdbFileTooltip = MessageManager.getString("label.nothing_selected");
1118     }
1119     lbl_pdbFile.setText(pdbFileString);
1120     lbl_pdbFile.setToolTipText(pdbFileTooltip);
1121
1122     // PAE file choice
1123     String paeFileString = "";
1124     String paeFileTooltip = "";
1125     if (localPdbPaeMatrixFileName != null
1126             && localPdbPaeMatrixFileName.length() > 0)
1127     {
1128       paeFileString = new File(localPdbPaeMatrixFileName).getName();
1129       paeFileTooltip = new File(localPdbPaeMatrixFileName)
1130               .getAbsolutePath();
1131     }
1132     else
1133     {
1134       paeFileString = MessageManager.getString("label.none");
1135       paeFileTooltip = MessageManager.getString("label.nothing_selected");
1136     }
1137     lbl_paeFile.setText(paeFileString);
1138     lbl_paeFile.setToolTipText(paeFileTooltip);
1139   }
1140
1141   @Override
1142   protected void cmbAssSeqStateChanged()
1143   {
1144     validateSelections();
1145   }
1146
1147   private FilterOption lastSelected = null;
1148
1149   /**
1150    * Handles the state change event for the 'filter' combo-box and 'invert'
1151    * check-box
1152    */
1153   @Override
1154   protected void stateChanged(ItemEvent e)
1155   {
1156     if (e.getSource() instanceof JCheckBox)
1157     {
1158       updateCurrentView();
1159     }
1160     else
1161     {
1162       if (e.getStateChange() == ItemEvent.SELECTED)
1163       {
1164         updateCurrentView();
1165       }
1166     }
1167
1168   }
1169
1170   /**
1171    * select structures for viewing by their PDB IDs
1172    * 
1173    * @param pdbids
1174    * @return true if structures were found and marked as selected
1175    */
1176   public boolean selectStructure(String... pdbids)
1177   {
1178     boolean found = false;
1179
1180     FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
1181             .getSelectedItem());
1182     String currentView = selectedFilterOpt.getView();
1183     JTable restable = (currentView == VIEWS_FILTER) ? getResultTable()
1184             : (currentView == VIEWS_LOCAL_PDB) ? tbl_local_pdb : null;
1185
1186     if (restable == null)
1187     {
1188       // can't select (enter PDB ID, or load file - need to also select which
1189       // sequence to associate with)
1190       return false;
1191     }
1192
1193     int pdbIdColIndex = restable.getColumn("PDB Id").getModelIndex();
1194     for (int r = 0; r < restable.getRowCount(); r++)
1195     {
1196       for (int p = 0; p < pdbids.length; p++)
1197       {
1198         if (String.valueOf(restable.getValueAt(r, pdbIdColIndex))
1199                 .equalsIgnoreCase(pdbids[p]))
1200         {
1201           restable.setRowSelectionInterval(r, r);
1202           found = true;
1203         }
1204       }
1205     }
1206     return found;
1207   }
1208
1209   /**
1210    * Handles the 'New View' action
1211    */
1212   @Override
1213   protected void newView_ActionPerformed()
1214   {
1215     targetView.setSelectedItem(null);
1216     showStructures(false);
1217   }
1218
1219   /**
1220    * Handles the 'Add to existing viewer' action
1221    */
1222   @Override
1223   protected void add_ActionPerformed()
1224   {
1225     showStructures(false);
1226   }
1227
1228   /**
1229    * structure viewer opened by this dialog, or null
1230    */
1231   private StructureViewer sViewer = null;
1232
1233   public void showStructures(boolean waitUntilFinished)
1234   {
1235
1236     final StructureSelectionManager ssm = ap.getStructureSelectionManager();
1237
1238     final int preferredHeight = pnl_filter.getHeight();
1239     btn_add.setEnabled(false);
1240     btn_newView.setEnabled(false);
1241     btn_cancel.setEnabled(false);
1242     actionsPanel.setEnabled(false);
1243
1244     final String progress = MessageManager
1245             .getString("label.working_ellipsis");
1246     setProgressBar(progress, progress.hashCode());
1247     Runnable viewStruc = new Runnable()
1248     {
1249       @Override
1250       public void run()
1251       {
1252         FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
1253                 .getSelectedItem());
1254         String currentView = selectedFilterOpt.getView();
1255         JTable restable = (currentView == VIEWS_FILTER) ? getResultTable()
1256                 : tbl_local_pdb;
1257
1258         if (currentView == VIEWS_FILTER)
1259         {
1260           int[] selectedRows = restable.getSelectedRows();
1261           PDBEntry[] pdbEntriesToView = new PDBEntry[selectedRows.length];
1262           List<SequenceI> selectedSeqsToView = new ArrayList<>();
1263           pdbEntriesToView = data.collectSelectedRows(restable,
1264                   selectedRows, selectedSeqsToView);
1265
1266           SequenceI[] selectedSeqs = selectedSeqsToView
1267                   .toArray(new SequenceI[selectedSeqsToView.size()]);
1268           sViewer = launchStructureViewer(ssm, pdbEntriesToView, ap,
1269                   selectedSeqs);
1270         }
1271         else if (currentView == VIEWS_LOCAL_PDB)
1272         {
1273           int[] selectedRows = tbl_local_pdb.getSelectedRows();
1274           PDBEntry[] pdbEntriesToView = new PDBEntry[selectedRows.length];
1275           int count = 0;
1276           int pdbIdColIndex = tbl_local_pdb.getColumn("PDB Id")
1277                   .getModelIndex();
1278           int refSeqColIndex = tbl_local_pdb.getColumn("Ref Sequence")
1279                   .getModelIndex();
1280           List<SequenceI> selectedSeqsToView = new ArrayList<>();
1281           for (int row : selectedRows)
1282           {
1283             PDBEntry pdbEntry = ((PDBEntryTableModel) tbl_local_pdb
1284                     .getModel()).getPDBEntryAt(row).getPdbEntry();
1285
1286             pdbEntriesToView[count++] = pdbEntry;
1287             SequenceI selectedSeq = (SequenceI) tbl_local_pdb
1288                     .getValueAt(row, refSeqColIndex);
1289             selectedSeqsToView.add(selectedSeq);
1290           }
1291           SequenceI[] selectedSeqs = selectedSeqsToView
1292                   .toArray(new SequenceI[selectedSeqsToView.size()]);
1293           sViewer = launchStructureViewer(ssm, pdbEntriesToView, ap,
1294                   selectedSeqs);
1295         }
1296         else if (currentView == VIEWS_ENTER_ID)
1297         {
1298           SequenceI userSelectedSeq = ((AssociateSeqOptions) idInputAssSeqPanel
1299                   .getCmb_assSeq().getSelectedItem()).getSequence();
1300           if (userSelectedSeq != null)
1301           {
1302             selectedSequence = userSelectedSeq;
1303           }
1304           String pdbIdStr = txt_search.getText();
1305           PDBEntry pdbEntry = selectedSequence.getPDBEntry(pdbIdStr);
1306           if (pdbEntry == null)
1307           {
1308             pdbEntry = new PDBEntry();
1309             if (pdbIdStr.split(":").length > 1)
1310             {
1311               pdbEntry.setId(pdbIdStr.split(":")[0]);
1312               pdbEntry.setChainCode(
1313                       pdbIdStr.split(":")[1].toUpperCase(Locale.ROOT));
1314             }
1315             else
1316             {
1317               pdbEntry.setId(pdbIdStr);
1318             }
1319             pdbEntry.setType(PDBEntry.Type.PDB);
1320             selectedSequence.getDatasetSequence().addPDBId(pdbEntry);
1321           }
1322
1323           PDBEntry[] pdbEntriesToView = new PDBEntry[] { pdbEntry };
1324           sViewer = launchStructureViewer(ssm, pdbEntriesToView, ap,
1325                   new SequenceI[]
1326                   { selectedSequence });
1327         }
1328         else if (currentView == VIEWS_FROM_FILE)
1329         {
1330           StructureChooser sc = StructureChooser.this;
1331           TFType tft = (TFType) sc.combo_tempFacAs.getSelectedItem();
1332           String paeFilename = sc.localPdbPaeMatrixFileName;
1333           AssociateSeqOptions assSeqOpt = (AssociateSeqOptions) fileChooserAssSeqPanel
1334                   .getCmb_assSeq().getSelectedItem();
1335           SequenceI userSelectedSeq = assSeqOpt.getSequence();
1336           if (userSelectedSeq != null)
1337           {
1338             selectedSequence = userSelectedSeq;
1339           }
1340           String pdbFilename = selectedPdbFileName;
1341
1342           StructureChooser.openStructureFileForSequence(ssm, sc, ap,
1343                   selectedSequence, true, pdbFilename, tft, paeFilename,
1344                   true);
1345         }
1346         SwingUtilities.invokeLater(new Runnable()
1347         {
1348           @Override
1349           public void run()
1350           {
1351             setProgressBar("Complete.", progress.hashCode());
1352             closeAction(preferredHeight);
1353             mainFrame.dispose();
1354           }
1355         });
1356       }
1357     };
1358     Thread runner = new Thread(viewStruc);
1359     runner.start();
1360     if (waitUntilFinished)
1361     {
1362       while (sViewer == null ? runner.isAlive()
1363               : (sViewer.sview == null ? true
1364                       : !sViewer.sview.hasMapping()))
1365       {
1366         try
1367         {
1368           Thread.sleep(300);
1369         } catch (InterruptedException ie)
1370         {
1371
1372         }
1373       }
1374     }
1375   }
1376
1377   /**
1378    * Answers a structure viewer (new or existing) configured to superimpose
1379    * added structures or not according to the user's choice
1380    * 
1381    * @param ssm
1382    * @return
1383    */
1384   StructureViewer getTargetedStructureViewer(StructureSelectionManager ssm)
1385   {
1386     Object sv = targetView.getSelectedItem();
1387
1388     return sv == null ? new StructureViewer(ssm) : (StructureViewer) sv;
1389   }
1390
1391   /**
1392    * Adds PDB structures to a new or existing structure viewer
1393    * 
1394    * @param ssm
1395    * @param pdbEntriesToView
1396    * @param alignPanel
1397    * @param sequences
1398    * @return
1399    */
1400   private StructureViewer launchStructureViewer(
1401           StructureSelectionManager ssm, final PDBEntry[] pdbEntriesToView,
1402           final AlignmentPanel alignPanel, SequenceI[] sequences)
1403   {
1404     return launchStructureViewer(ssm, pdbEntriesToView, alignPanel,
1405             sequences, null);
1406   }
1407
1408   private StructureViewer launchStructureViewer(
1409           StructureSelectionManager ssm, final PDBEntry[] pdbEntriesToView,
1410           final AlignmentPanel alignPanel, SequenceI[] sequences,
1411           ViewerType viewerType)
1412   {
1413     long progressId = sequences.hashCode();
1414     setProgressBar(MessageManager
1415             .getString("status.launching_3d_structure_viewer"), progressId);
1416     final StructureViewer theViewer = getTargetedStructureViewer(ssm);
1417     boolean superimpose = chk_superpose.isSelected();
1418     theViewer.setSuperpose(superimpose);
1419
1420     // if we're running in --headless mode make this viewer synchronous
1421     if (Jalview.isHeadlessMode())
1422     {
1423       theViewer.setAsync(false);
1424     }
1425
1426     /*
1427      * remember user's choice of superimpose or not
1428      */
1429     Cache.setProperty(AUTOSUPERIMPOSE,
1430             Boolean.valueOf(superimpose).toString());
1431
1432     setProgressBar(null, progressId);
1433     if (SiftsSettings.isMapWithSifts())
1434     {
1435       List<SequenceI> seqsWithoutSourceDBRef = new ArrayList<>();
1436       int p = 0;
1437       // TODO: skip PDBEntry:Sequence pairs where PDBEntry doesn't look like a
1438       // real PDB ID. For moment, we can also safely do this if there is already
1439       // a known mapping between the PDBEntry and the sequence.
1440       for (SequenceI seq : sequences)
1441       {
1442         PDBEntry pdbe = pdbEntriesToView[p++];
1443         if (pdbe != null && pdbe.getFile() != null)
1444         {
1445           StructureMapping[] smm = ssm.getMapping(pdbe.getFile());
1446           if (smm != null && smm.length > 0)
1447           {
1448             for (StructureMapping sm : smm)
1449             {
1450               if (sm.getSequence() == seq)
1451               {
1452                 continue;
1453               }
1454             }
1455           }
1456         }
1457         if (seq.getPrimaryDBRefs().isEmpty())
1458         {
1459           seqsWithoutSourceDBRef.add(seq);
1460           continue;
1461         }
1462       }
1463       if (!seqsWithoutSourceDBRef.isEmpty())
1464       {
1465         int y = seqsWithoutSourceDBRef.size();
1466         setProgressBar(MessageManager.formatMessage(
1467                 "status.fetching_dbrefs_for_sequences_without_valid_refs",
1468                 y), progressId);
1469         SequenceI[] seqWithoutSrcDBRef = seqsWithoutSourceDBRef
1470                 .toArray(new SequenceI[y]);
1471         DBRefFetcher dbRefFetcher = new DBRefFetcher(seqWithoutSrcDBRef);
1472         dbRefFetcher.fetchDBRefs(true);
1473
1474         setProgressBar("Fetch complete.", progressId); // todo i18n
1475       }
1476     }
1477     if (pdbEntriesToView.length > 1)
1478     {
1479       setProgressBar(
1480               MessageManager.getString(
1481                       "status.fetching_3d_structures_for_selected_entries"),
1482               progressId);
1483       theViewer.viewStructures(pdbEntriesToView, sequences, alignPanel,
1484               viewerType);
1485     }
1486     else
1487     {
1488       setProgressBar(MessageManager.formatMessage(
1489               "status.fetching_3d_structures_for",
1490               pdbEntriesToView[0].getId()), progressId);
1491       // Can we pass a pre-computeMappinged pdbFile?
1492       theViewer.viewStructures(pdbEntriesToView[0], sequences, alignPanel,
1493               viewerType);
1494     }
1495     setProgressBar(null, progressId);
1496     // remember the last viewer we used...
1497     lastTargetedView = theViewer;
1498     return theViewer;
1499   }
1500
1501   /**
1502    * Populates the combo-box used in associating manually fetched structures to
1503    * a unique sequence when more than one sequence selection is made.
1504    */
1505   @Override
1506   protected void populateCmbAssociateSeqOptions(
1507           JComboBox<AssociateSeqOptions> cmb_assSeq,
1508           JLabel lbl_associateSeq)
1509   {
1510     cmb_assSeq.removeAllItems();
1511     cmb_assSeq.addItem(
1512             new AssociateSeqOptions("-Select Associated Seq-", null));
1513     lbl_associateSeq.setVisible(false);
1514     if (selectedSequences.length > 1)
1515     {
1516       for (SequenceI seq : selectedSequences)
1517       {
1518         cmb_assSeq.addItem(new AssociateSeqOptions(seq));
1519       }
1520     }
1521     else
1522     {
1523       String seqName = selectedSequence.getDisplayId(false);
1524       seqName = seqName.length() <= 40 ? seqName : seqName.substring(0, 39);
1525       lbl_associateSeq.setText(seqName);
1526       lbl_associateSeq.setVisible(true);
1527       cmb_assSeq.setVisible(false);
1528     }
1529   }
1530
1531   protected boolean isStructuresDiscovered()
1532   {
1533     return discoveredStructuresSet != null
1534             && !discoveredStructuresSet.isEmpty();
1535   }
1536
1537   protected int PDB_ID_MIN = 3;// or: (Jalview.isJS() ? 3 : 1); // Bob proposes
1538                                // this.
1539   // Doing a search for "1" or "1c" is valuable?
1540   // Those work but are enormously slow.
1541
1542   @Override
1543   protected void txt_search_ActionPerformed()
1544   {
1545     String text = txt_search.getText().trim();
1546     if (text.length() >= PDB_ID_MIN)
1547       new Thread()
1548       {
1549
1550         @Override
1551         public void run()
1552         {
1553           errorWarning.setLength(0);
1554           isValidPBDEntry = false;
1555           if (text.length() > 0)
1556           {
1557             // TODO move this pdb id search into the PDB specific
1558             // FTSSearchEngine
1559             // for moment, it will work fine as is because it is self-contained
1560             String searchTerm = text.toLowerCase(Locale.ROOT);
1561             searchTerm = searchTerm.split(":")[0];
1562             // jalview.bin.Console.outPrintln(">>>>> search term : " +
1563             // searchTerm);
1564             List<FTSDataColumnI> wantedFields = new ArrayList<>();
1565             FTSRestRequest pdbRequest = new FTSRestRequest();
1566             pdbRequest.setAllowEmptySeq(false);
1567             pdbRequest.setResponseSize(1);
1568             pdbRequest.setFieldToSearchBy("(pdb_id:");
1569             pdbRequest.setWantedFields(wantedFields);
1570             pdbRequest.setSearchTerm(searchTerm + ")");
1571             pdbRequest.setAssociatedSequence(selectedSequence);
1572             FTSRestClientI pdbRestClient = PDBFTSRestClient.getInstance();
1573             wantedFields.add(pdbRestClient.getPrimaryKeyColumn());
1574             FTSRestResponse resultList;
1575             try
1576             {
1577               resultList = pdbRestClient.executeRequest(pdbRequest);
1578             } catch (Exception e)
1579             {
1580               errorWarning.append(e.getMessage());
1581               return;
1582             } finally
1583             {
1584               validateSelections();
1585             }
1586             if (resultList.getSearchSummary() != null
1587                     && resultList.getSearchSummary().size() > 0)
1588             {
1589               isValidPBDEntry = true;
1590             }
1591           }
1592           validateSelections();
1593         }
1594       }.start();
1595   }
1596
1597   @Override
1598   protected void tabRefresh()
1599   {
1600     if (selectedSequences != null)
1601     {
1602       lbl_loading.setVisible(true);
1603       Thread refreshThread = new Thread(new Runnable()
1604       {
1605         @Override
1606         public void run()
1607         {
1608           fetchStructuresMetaData();
1609           // populateFilterComboBox(true, cachedPDBExists);
1610
1611           filterResultSet(
1612                   ((FilterOption) cmb_filterOption.getSelectedItem())
1613                           .getValue());
1614           lbl_loading.setVisible(false);
1615         }
1616       });
1617       refreshThread.start();
1618     }
1619   }
1620
1621   public class PDBEntryTableModel extends AbstractTableModel
1622   {
1623     String[] columns = { "Ref Sequence", "PDB Id", "Chain", "Type",
1624         "File" };
1625
1626     private List<CachedPDB> pdbEntries;
1627
1628     public PDBEntryTableModel(List<CachedPDB> pdbEntries)
1629     {
1630       this.pdbEntries = new ArrayList<>(pdbEntries);
1631     }
1632
1633     @Override
1634     public String getColumnName(int columnIndex)
1635     {
1636       return columns[columnIndex];
1637     }
1638
1639     @Override
1640     public int getRowCount()
1641     {
1642       return pdbEntries.size();
1643     }
1644
1645     @Override
1646     public int getColumnCount()
1647     {
1648       return columns.length;
1649     }
1650
1651     @Override
1652     public boolean isCellEditable(int row, int column)
1653     {
1654       return false;
1655     }
1656
1657     @Override
1658     public Object getValueAt(int rowIndex, int columnIndex)
1659     {
1660       Object value = "??";
1661       CachedPDB entry = pdbEntries.get(rowIndex);
1662       switch (columnIndex)
1663       {
1664       case 0:
1665         value = entry.getSequence();
1666         break;
1667       case 1:
1668         value = entry.getQualifiedId();
1669         break;
1670       case 2:
1671         value = entry.getPdbEntry().getChainCode() == null ? "_"
1672                 : entry.getPdbEntry().getChainCode();
1673         break;
1674       case 3:
1675         value = entry.getPdbEntry().getType();
1676         break;
1677       case 4:
1678         value = entry.getPdbEntry().getFile();
1679         break;
1680       }
1681       return value;
1682     }
1683
1684     @Override
1685     public Class<?> getColumnClass(int columnIndex)
1686     {
1687       return columnIndex == 0 ? SequenceI.class : PDBEntry.class;
1688     }
1689
1690     public CachedPDB getPDBEntryAt(int row)
1691     {
1692       return pdbEntries.get(row);
1693     }
1694
1695   }
1696
1697   private class CachedPDB
1698   {
1699     private SequenceI sequence;
1700
1701     private PDBEntry pdbEntry;
1702
1703     public CachedPDB(SequenceI sequence, PDBEntry pdbEntry)
1704     {
1705       this.sequence = sequence;
1706       this.pdbEntry = pdbEntry;
1707     }
1708
1709     public String getQualifiedId()
1710     {
1711       if (pdbEntry.hasProvider())
1712       {
1713         return pdbEntry.getProvider() + ":" + pdbEntry.getId();
1714       }
1715       return pdbEntry.toString();
1716     }
1717
1718     public SequenceI getSequence()
1719     {
1720       return sequence;
1721     }
1722
1723     public PDBEntry getPdbEntry()
1724     {
1725       return pdbEntry;
1726     }
1727
1728   }
1729
1730   private IProgressIndicator progressBar;
1731
1732   @Override
1733   public void setProgressBar(String message, long id)
1734   {
1735     if (!Platform.isHeadless() && progressBar != null)
1736       progressBar.setProgressBar(message, id);
1737   }
1738
1739   @Override
1740   public void registerHandler(long id, IProgressIndicatorHandler handler)
1741   {
1742     if (progressBar != null)
1743       progressBar.registerHandler(id, handler);
1744   }
1745
1746   @Override
1747   public boolean operationInProgress()
1748   {
1749     return progressBar == null ? false : progressBar.operationInProgress();
1750   }
1751
1752   public JalviewStructureDisplayI getOpenedStructureViewer()
1753   {
1754     return sViewer == null ? null : sViewer.sview;
1755   }
1756
1757   @Override
1758   protected void setFTSDocFieldPrefs(FTSDataColumnPreferences newPrefs)
1759   {
1760     data.setDocFieldPrefs(newPrefs);
1761
1762   }
1763
1764   /**
1765    * 
1766    * @return true when all initialisation threads have finished and dialog is
1767    *         visible
1768    */
1769   public boolean isDialogVisible()
1770   {
1771     return mainFrame != null && data != null && cmb_filterOption != null
1772             && mainFrame.isVisible()
1773             && cmb_filterOption.getSelectedItem() != null;
1774   }
1775
1776   /**
1777    * 
1778    * @return true if the 3D-Beacons query button will/has been displayed
1779    */
1780   public boolean isCanQueryTDB()
1781   {
1782     return canQueryTDB;
1783   }
1784
1785   public boolean isNotQueriedTDBYet()
1786   {
1787     return notQueriedTDBYet;
1788   }
1789
1790   /**
1791    * Open a single structure file for a given sequence
1792    */
1793   public static void openStructureFileForSequence(
1794           StructureSelectionManager ssm, StructureChooser sc,
1795           AlignmentPanel ap, SequenceI seq, boolean prompt,
1796           String sFilename, TFType tft, String paeFilename,
1797           boolean doXferSettings)
1798   {
1799     openStructureFileForSequence(ssm, sc, ap, seq, prompt, sFilename, tft,
1800             paeFilename, false, true, doXferSettings, null);
1801   }
1802
1803   public static StructureViewer openStructureFileForSequence(
1804           StructureSelectionManager ssm, StructureChooser sc,
1805           AlignmentPanel ap, SequenceI seq, boolean prompt,
1806           String sFilename, TFType tft, String paeFilename,
1807           boolean forceHeadless, boolean showRefAnnotations,
1808           boolean doXferSettings, ViewerType viewerType)
1809   {
1810     StructureViewer sv = null;
1811     boolean headless = forceHeadless;
1812     if (sc == null)
1813     {
1814       // headless = true;
1815       prompt = false;
1816       // suppress structure viewer's external service queries
1817       sc = new StructureChooser(new SequenceI[] { seq }, seq, ap, false,true);
1818     }
1819     if (ssm == null)
1820     {
1821       ssm = ap.getStructureSelectionManager();
1822     }
1823
1824     PDBEntry fileEntry = new AssociatePdbFileWithSeq().associatePdbWithSeq(
1825             sFilename, DataSourceType.FILE, seq, prompt, Desktop.instance,
1826             tft, paeFilename, doXferSettings);
1827
1828     // if headless, "false" in the sc constructor above will avoid GUI behaviour
1829     // in sc.launchStructureViewer()
1830     if (!headless && !(viewerType == null))
1831     {
1832       sv = sc.launchStructureViewer(ssm, new PDBEntry[] { fileEntry }, ap,
1833               new SequenceI[]
1834               { seq }, viewerType);
1835     }
1836
1837     sc.mainFrame.dispose();
1838
1839     if (showRefAnnotations)
1840     {
1841       showReferenceAnnotationsForSequence(ap.alignFrame, seq);
1842     }
1843
1844     return sv;
1845   }
1846
1847   public static void showReferenceAnnotationsForSequence(AlignFrame af,
1848           SequenceI sequence)
1849   {
1850     AlignViewport av = af.getCurrentView();
1851     AlignmentI al = av.getAlignment();
1852
1853     List<SequenceI> forSequences = new ArrayList<>();
1854     forSequences.add(sequence);
1855     final Map<SequenceI, List<AlignmentAnnotation>> candidates = new LinkedHashMap<>();
1856     AlignmentUtils.findAddableReferenceAnnotations(forSequences, null,
1857             candidates, al);
1858     final SequenceGroup selectionGroup = av.getSelectionGroup();
1859     AlignmentUtils.addReferenceAnnotations(candidates, al, selectionGroup);
1860     for (AlignmentViewPanel ap : af.getAlignPanels())
1861     {
1862       // required to readjust the height and position of the PAE
1863       // annotation
1864       ap.adjustAnnotationHeight();
1865     }
1866
1867   }
1868 }