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