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