JAL-3829 alternative UX: need to select 3d-beacons from dropdown if db-refs need...
[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.ItemEvent;
25 import java.util.ArrayList;
26 import java.util.Collection;
27 import java.util.HashSet;
28 import java.util.LinkedHashSet;
29 import java.util.List;
30 import java.util.concurrent.Executors;
31
32 import javax.swing.JCheckBox;
33 import javax.swing.JComboBox;
34 import javax.swing.JLabel;
35 import javax.swing.JTable;
36 import javax.swing.SwingUtilities;
37 import javax.swing.table.AbstractTableModel;
38
39 import jalview.api.structures.JalviewStructureDisplayI;
40 import jalview.bin.Cache;
41 import jalview.bin.Jalview;
42 import jalview.datamodel.PDBEntry;
43 import jalview.datamodel.SequenceI;
44 import jalview.fts.api.FTSData;
45 import jalview.fts.api.FTSDataColumnI;
46 import jalview.fts.api.FTSRestClientI;
47 import jalview.fts.core.FTSDataColumnPreferences;
48 import jalview.fts.core.FTSRestRequest;
49 import jalview.fts.core.FTSRestResponse;
50 import jalview.fts.service.pdb.PDBFTSRestClient;
51 import jalview.gui.structurechooser.PDBStructureChooserQuerySource;
52 import jalview.gui.structurechooser.StructureChooserQuerySource;
53 import jalview.gui.structurechooser.ThreeDBStructureChooserQuerySource;
54 import jalview.io.DataSourceType;
55 import jalview.jbgui.FilterOption;
56 import jalview.jbgui.GStructureChooser;
57 import jalview.structure.StructureMapping;
58 import jalview.structure.StructureSelectionManager;
59 import jalview.util.MessageManager;
60 import jalview.ws.DBRefFetcher;
61 import jalview.ws.seqfetcher.DbSourceProxy;
62 import jalview.ws.sifts.SiftsSettings;
63
64 /**
65  * Provides the behaviors for the Structure chooser Panel
66  * 
67  * @author tcnofoegbu
68  *
69  */
70 @SuppressWarnings("serial")
71 public class StructureChooser extends GStructureChooser
72         implements IProgressIndicator
73 {
74   private static final String AUTOSUPERIMPOSE = "AUTOSUPERIMPOSE";
75
76   /**
77    * transient combo box choice for initiating 3db fetch
78    */
79   private static final String VIEWS_QUERYING_TDB = "QUERY_3DB";
80
81   private SequenceI selectedSequence;
82
83   private SequenceI[] selectedSequences;
84
85   private IProgressIndicator progressIndicator;
86
87   private Collection<FTSData> discoveredStructuresSet;
88
89   private StructureChooserQuerySource data;
90
91   @Override
92   protected FTSDataColumnPreferences getFTSDocFieldPrefs()
93   {
94     return data.getDocFieldPrefs();
95   }
96
97   private String selectedPdbFileName;
98
99   private boolean isValidPBDEntry;
100
101   private boolean cachedPDBExists;
102
103   private Collection<FTSData> lastDiscoveredStructuresSet;
104
105   private boolean canQueryTDB = false;
106
107   private boolean notQueriedTDBYet = true;
108
109   List<SequenceI> seqsWithoutSourceDBRef = null;
110
111   private static StructureViewer lastTargetedView = null;
112
113   public StructureChooser(SequenceI[] selectedSeqs, SequenceI selectedSeq,
114           AlignmentPanel ap)
115   {
116     // which FTS engine to use
117     data = StructureChooserQuerySource.getQuerySourceFor(selectedSeqs);
118     initDialog();
119
120     this.ap = ap;
121     this.selectedSequence = selectedSeq;
122     this.selectedSequences = selectedSeqs;
123     this.progressIndicator = (ap == null) ? null : ap.alignFrame;
124     init();
125
126   }
127
128   /**
129    * sets canQueryTDB if protein sequences without a canonical uniprot ref or at
130    * least one structure are discovered.
131    */
132   private void populateSeqsWithoutSourceDBRef()
133   {
134     seqsWithoutSourceDBRef = new ArrayList<SequenceI>();
135     boolean needCanonical = false;
136     for (SequenceI seq : selectedSequences)
137     {
138       if (seq.isProtein())
139       {
140         int dbRef = ThreeDBStructureChooserQuerySource
141                 .checkUniprotRefs(seq.getDBRefs());
142         if (dbRef < 0)
143         {
144           if (dbRef == -1)
145           {
146             // need to retrieve canonicals
147             needCanonical = true;
148             seqsWithoutSourceDBRef.add(seq);
149           }
150           else
151           {
152             // could be a sequence with pdb ref
153             if (seq.getAllPDBEntries() == null
154                     || seq.getAllPDBEntries().size() == 0)
155             {
156               seqsWithoutSourceDBRef.add(seq);
157             }
158           }
159         }
160       }
161     }
162     // retrieve database refs for protein sequences
163     if (!seqsWithoutSourceDBRef.isEmpty())
164     {
165       canQueryTDB = true;
166       if (needCanonical)
167       {
168         notQueriedTDBYet = false;
169       }
170     }
171   };
172
173   /**
174    * Initializes parameters used by the Structure Chooser Panel
175    */
176   protected void init()
177   {
178     if (!Jalview.isHeadlessMode())
179     {
180       progressBar = new ProgressBar(this.statusPanel, this.statusBar);
181     }
182
183     chk_superpose.setSelected(Cache.getDefault(AUTOSUPERIMPOSE, true));
184
185     Executors.defaultThreadFactory().newThread(new Runnable()
186     {
187       public void run()
188       {
189         populateSeqsWithoutSourceDBRef();
190         initialStructureDiscovery();
191       }
192
193     }).start();
194
195   }
196
197   // called by init
198   private void initialStructureDiscovery()
199   {
200     // check which FTS engine to use
201     data = StructureChooserQuerySource.getQuerySourceFor(selectedSequences);
202
203     // ensure a filter option is in force for search
204     populateFilterComboBox(true, cachedPDBExists);
205
206     // looks for any existing structures already loaded
207     // for the sequences (the cached ones)
208     // then queries the StructureChooserQuerySource to
209     // discover more structures.
210     //
211     // Possible optimisation is to only begin querying
212     // the structure chooser if there are no cached structures.
213
214     long startTime = System.currentTimeMillis();
215     updateProgressIndicator(
216             MessageManager.getString("status.loading_cached_pdb_entries"),
217             startTime);
218     loadLocalCachedPDBEntries();
219     updateProgressIndicator(null, startTime);
220     updateProgressIndicator(
221             MessageManager.getString("status.searching_for_pdb_structures"),
222             startTime);
223     fetchStructuresMetaData();
224     // revise filter options if no results were found
225     populateFilterComboBox(isStructuresDiscovered(), cachedPDBExists);
226     discoverStructureViews();
227     updateProgressIndicator(null, startTime);
228     mainFrame.setVisible(true);
229     updateCurrentView();
230   }
231
232   private void promptForTDBFetch()
233   {
234     final long progressId = System.currentTimeMillis();
235
236     // final action after prompting and discovering db refs
237     final Runnable strucDiscovery = new Runnable()
238     {
239       @Override
240       public void run()
241       {
242         // TODO: warn if no accessions discovered
243         populateSeqsWithoutSourceDBRef();
244         // redo initial discovery - this time with 3d beacons
245         // Executors.
246         previousWantedFields=null;
247         
248         initialStructureDiscovery();
249       }
250     };
251
252     // fetch db refs if OK pressed
253     final Runnable discoverCanonicalDBrefs = new Runnable()
254     {
255       @Override
256       public void run()
257       {
258         populateSeqsWithoutSourceDBRef();
259
260         final int y = seqsWithoutSourceDBRef.size();
261         if (y > 0)
262         {
263           SwingUtilities.invokeLater(new Runnable() {
264             @Override
265             public void run()
266             {
267               updateProgressIndicator(MessageManager.formatMessage(
268                       "status.fetching_dbrefs_for_sequences_without_valid_refs",
269                       y), progressId);
270               
271             }
272           });
273           
274           final SequenceI[] seqWithoutSrcDBRef = seqsWithoutSourceDBRef
275                   .toArray(new SequenceI[y]);
276           DBRefFetcher dbRefFetcher = new DBRefFetcher(seqWithoutSrcDBRef,
277                   progressBar, new DbSourceProxy[]
278                   { new jalview.ws.dbsources.Uniprot() }, null, false);
279
280           // ideally this would also gracefully run with callbacks
281           dbRefFetcher.fetchDBRefs(true);
282         }
283         SwingUtilities.invokeLater(new Runnable() {
284           @Override
285           public void run()
286           {
287             if (y>0) {
288               updateProgressIndicator("Fetch complete.", progressId); // todo i18n
289             }
290             // filter has been selected, so we set flag to remove ourselves
291             notQueriedTDBYet = false;
292
293             Executors.defaultThreadFactory().newThread(strucDiscovery).start();
294
295           }
296         });
297         
298       }
299     };
300     final Runnable revertview = new Runnable() {
301       public void run() {
302         if (lastSelected!=null) {
303           cmb_filterOption.setSelectedItem(lastSelected);
304         }
305       };
306     };
307     // need cancel and no to result in the discoverPDB action - mocked is
308     // 'cancel'
309     JvOptionPane.newOptionDialog(this)
310             .setResponseHandler(JvOptionPane.OK_OPTION,
311                     discoverCanonicalDBrefs)
312             .setResponseHandler(JvOptionPane.CANCEL_OPTION, revertview)
313             .setResponseHandler(JvOptionPane.NO_OPTION, revertview)
314             .showDialog(
315                     MessageManager.formatMessage(
316                             "label.fetch_references_for_3dbeacons",
317                             seqsWithoutSourceDBRef.size()),
318                     MessageManager
319                             .getString("label.3dbeacons"),
320                     JvOptionPane.YES_NO_OPTION, JvOptionPane.PLAIN_MESSAGE,
321                     null, new Object[]
322                     { MessageManager.getString("action.ok"),
323                         MessageManager.getString("action.cancel") },
324                     MessageManager.getString("action.ok"));
325   }
326
327   /**
328    * Builds a drop-down choice list of existing structure viewers to which new
329    * structures may be added. If this list is empty then it, and the 'Add'
330    * button, are hidden.
331    */
332   private void discoverStructureViews()
333   {
334     if (Desktop.instance != null)
335     {
336       targetView.removeAllItems();
337       if (lastTargetedView != null && !lastTargetedView.isVisible())
338       {
339         lastTargetedView = null;
340       }
341       int linkedViewsAt = 0;
342       for (StructureViewerBase view : Desktop.instance
343               .getStructureViewers(null, null))
344       {
345         StructureViewer viewHandler = (lastTargetedView != null
346                 && lastTargetedView.sview == view) ? lastTargetedView
347                         : StructureViewer.reconfigure(view);
348
349         if (view.isLinkedWith(ap))
350         {
351           targetView.insertItemAt(viewHandler, linkedViewsAt++);
352         }
353         else
354         {
355           targetView.addItem(viewHandler);
356         }
357       }
358
359       /*
360        * show option to Add to viewer if at least 1 viewer found
361        */
362       targetView.setVisible(false);
363       if (targetView.getItemCount() > 0)
364       {
365         targetView.setVisible(true);
366         if (lastTargetedView != null)
367         {
368           targetView.setSelectedItem(lastTargetedView);
369         }
370         else
371         {
372           targetView.setSelectedIndex(0);
373         }
374       }
375       btn_add.setVisible(targetView.isVisible());
376     }
377   }
378
379   /**
380    * Updates the progress indicator with the specified message
381    * 
382    * @param message
383    *          displayed message for the operation
384    * @param id
385    *          unique handle for this indicator
386    */
387   protected void updateProgressIndicator(String message, long id)
388   {
389     if (progressIndicator != null)
390     {
391       progressIndicator.setProgressBar(message, id);
392     }
393   }
394
395   /**
396    * Retrieve meta-data for all the structure(s) for a given sequence(s) in a
397    * selection group
398    */
399   void fetchStructuresMetaData()
400   {
401     long startTime = System.currentTimeMillis();
402     Collection<FTSDataColumnI> wantedFields = data.getDocFieldPrefs()
403             .getStructureSummaryFields();
404
405     discoveredStructuresSet = new LinkedHashSet<>();
406     HashSet<String> errors = new HashSet<>();
407
408     FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
409             .getSelectedItem());
410
411     for (SequenceI seq : selectedSequences)
412     {
413
414       FTSRestResponse resultList;
415       try
416       {
417         resultList = data.fetchStructuresMetaData(seq, wantedFields,
418                 selectedFilterOpt, !chk_invertFilter.isSelected());
419         // null response means the FTSengine didn't yield a query for this
420         // consider designing a special exception if we really wanted to be
421         // OOCrazy
422         if (resultList == null)
423         {
424           continue;
425         }
426       } catch (Exception e)
427       {
428         e.printStackTrace();
429         errors.add(e.getMessage());
430         continue;
431       }
432       if (resultList.getSearchSummary() != null
433               && !resultList.getSearchSummary().isEmpty())
434       {
435         discoveredStructuresSet.addAll(resultList.getSearchSummary());
436       }
437     }
438
439     int noOfStructuresFound = 0;
440     String totalTime = (System.currentTimeMillis() - startTime)
441             + " milli secs";
442     if (discoveredStructuresSet != null
443             && !discoveredStructuresSet.isEmpty())
444     {
445       getResultTable()
446               .setModel(data.getTableModel(discoveredStructuresSet));
447
448       noOfStructuresFound = discoveredStructuresSet.size();
449       lastDiscoveredStructuresSet = discoveredStructuresSet;
450       mainFrame.setTitle(MessageManager.formatMessage(
451               "label.structure_chooser_no_of_structures",
452               noOfStructuresFound, totalTime));
453     }
454     else
455     {
456       mainFrame.setTitle(MessageManager
457               .getString("label.structure_chooser_manual_association"));
458       if (errors.size() > 0)
459       {
460         StringBuilder errorMsg = new StringBuilder();
461         for (String error : errors)
462         {
463           errorMsg.append(error).append("\n");
464         }
465         JvOptionPane.showMessageDialog(this, errorMsg.toString(),
466                 MessageManager.getString("label.pdb_web-service_error"),
467                 JvOptionPane.ERROR_MESSAGE);
468       }
469     }
470   }
471
472   protected void loadLocalCachedPDBEntries()
473   {
474     ArrayList<CachedPDB> entries = new ArrayList<>();
475     for (SequenceI seq : selectedSequences)
476     {
477       if (seq.getDatasetSequence() != null
478               && seq.getDatasetSequence().getAllPDBEntries() != null)
479       {
480         for (PDBEntry pdbEntry : seq.getDatasetSequence()
481                 .getAllPDBEntries())
482         {
483           if (pdbEntry.getFile() != null)
484           {
485             entries.add(new CachedPDB(seq, pdbEntry));
486           }
487         }
488       }
489     }
490     cachedPDBExists = !entries.isEmpty();
491     PDBEntryTableModel tableModelx = new PDBEntryTableModel(entries);
492     tbl_local_pdb.setModel(tableModelx);
493   }
494
495   /**
496    * Filters a given list of discovered structures based on supplied argument
497    * 
498    * @param fieldToFilterBy
499    *          the field to filter by
500    */
501   void filterResultSet(final String fieldToFilterBy)
502   {
503     Thread filterThread = new Thread(new Runnable()
504     {
505
506       @Override
507       public void run()
508       {
509         long startTime = System.currentTimeMillis();
510         lbl_loading.setVisible(true);
511         Collection<FTSDataColumnI> wantedFields = data.getDocFieldPrefs()
512                 .getStructureSummaryFields();
513         Collection<FTSData> filteredResponse = new HashSet<>();
514         HashSet<String> errors = new HashSet<>();
515
516         for (SequenceI seq : selectedSequences)
517         {
518
519           FTSRestResponse resultList;
520           try
521           {
522             resultList = data.selectFirstRankedQuery(seq,
523                     discoveredStructuresSet, wantedFields, fieldToFilterBy,
524                     !chk_invertFilter.isSelected());
525
526           } catch (Exception e)
527           {
528             e.printStackTrace();
529             errors.add(e.getMessage());
530             continue;
531           }
532           if (resultList.getSearchSummary() != null
533                   && !resultList.getSearchSummary().isEmpty())
534           {
535             filteredResponse.addAll(resultList.getSearchSummary());
536           }
537         }
538
539         String totalTime = (System.currentTimeMillis() - startTime)
540                 + " milli secs";
541         if (!filteredResponse.isEmpty())
542         {
543           final int filterResponseCount = filteredResponse.size();
544           Collection<FTSData> reorderedStructuresSet = new LinkedHashSet<>();
545           reorderedStructuresSet.addAll(filteredResponse);
546           reorderedStructuresSet.addAll(discoveredStructuresSet);
547           getResultTable()
548                   .setModel(data.getTableModel(reorderedStructuresSet));
549
550           FTSRestResponse.configureTableColumn(getResultTable(),
551                   wantedFields, tempUserPrefs);
552           getResultTable().getColumn("Ref Sequence").setPreferredWidth(120);
553           getResultTable().getColumn("Ref Sequence").setMinWidth(100);
554           getResultTable().getColumn("Ref Sequence").setMaxWidth(200);
555           // Update table selection model here
556           getResultTable().addRowSelectionInterval(0,
557                   filterResponseCount - 1);
558           mainFrame.setTitle(MessageManager.formatMessage(
559                   "label.structure_chooser_filter_time", totalTime));
560         }
561         else
562         {
563           mainFrame.setTitle(MessageManager.formatMessage(
564                   "label.structure_chooser_filter_time", totalTime));
565           if (errors.size() > 0)
566           {
567             StringBuilder errorMsg = new StringBuilder();
568             for (String error : errors)
569             {
570               errorMsg.append(error).append("\n");
571             }
572             JvOptionPane.showMessageDialog(null, errorMsg.toString(),
573                     MessageManager.getString("label.pdb_web-service_error"),
574                     JvOptionPane.ERROR_MESSAGE);
575           }
576         }
577
578         lbl_loading.setVisible(false);
579
580         validateSelections();
581       }
582     });
583     filterThread.start();
584   }
585
586   /**
587    * Handles action event for btn_pdbFromFile
588    */
589   @Override
590   protected void pdbFromFile_actionPerformed()
591   {
592     // TODO: JAL-3048 not needed for Jalview-JS until JSmol dep and
593     // StructureChooser
594     // works
595     jalview.io.JalviewFileChooser chooser = new jalview.io.JalviewFileChooser(
596             jalview.bin.Cache.getProperty("LAST_DIRECTORY"));
597     chooser.setFileView(new jalview.io.JalviewFileView());
598     chooser.setDialogTitle(
599             MessageManager.formatMessage("label.select_pdb_file_for",
600                     selectedSequence.getDisplayId(false)));
601     chooser.setToolTipText(MessageManager.formatMessage(
602             "label.load_pdb_file_associate_with_sequence",
603             selectedSequence.getDisplayId(false)));
604
605     int value = chooser.showOpenDialog(null);
606     if (value == jalview.io.JalviewFileChooser.APPROVE_OPTION)
607     {
608       selectedPdbFileName = chooser.getSelectedFile().getPath();
609       jalview.bin.Cache.setProperty("LAST_DIRECTORY", selectedPdbFileName);
610       validateSelections();
611     }
612   }
613
614   /**
615    * Populates the filter combo-box options dynamically depending on discovered
616    * structures
617    */
618   protected void populateFilterComboBox(boolean haveData,
619           boolean cachedPDBExist)
620   {
621     populateFilterComboBox(haveData, cachedPDBExist, null);
622   }
623
624   /**
625    * Populates the filter combo-box options dynamically depending on discovered
626    * structures
627    */
628   protected void populateFilterComboBox(boolean haveData,
629           boolean cachedPDBExist, FilterOption lastSel)
630   {
631
632     /*
633      * temporarily suspend the change listener behaviour
634      */
635     cmb_filterOption.removeItemListener(this);
636     int selSet = -1;
637     cmb_filterOption.removeAllItems();
638     if (haveData)
639     {
640       List<FilterOption> filters = data
641               .getAvailableFilterOptions(VIEWS_FILTER);
642       data.updateAvailableFilterOptions(VIEWS_FILTER, filters,
643               lastDiscoveredStructuresSet);
644       int p = 0;
645       for (FilterOption filter : filters)
646       {
647         if (lastSel != null && filter.equals(lastSel))
648         {
649           selSet = p;
650         }
651         p++;
652         cmb_filterOption.addItem(filter);
653       }
654     }
655
656     cmb_filterOption.addItem(
657             new FilterOption(MessageManager.getString("label.enter_pdb_id"),
658                     "-", VIEWS_ENTER_ID, false, null));
659     cmb_filterOption.addItem(
660             new FilterOption(MessageManager.getString("label.from_file"),
661                     "-", VIEWS_FROM_FILE, false, null));
662     if (canQueryTDB && notQueriedTDBYet)
663     {
664       FilterOption queryTDBOption = new FilterOption(
665               MessageManager.getString("label.search_3dbeacons"), "-",
666               VIEWS_QUERYING_TDB, false, null);
667       cmb_filterOption.addItem(queryTDBOption);
668     }
669
670     if (cachedPDBExist)
671     {
672       FilterOption cachedOption = new FilterOption(
673               MessageManager.getString("label.cached_structures"), "-",
674               VIEWS_LOCAL_PDB, false, null);
675       cmb_filterOption.addItem(cachedOption);
676       if (selSet == -1)
677       {
678         cmb_filterOption.setSelectedItem(cachedOption);
679       }
680     }
681     if (selSet > -1)
682     {
683       cmb_filterOption.setSelectedIndex(selSet);
684     }
685     cmb_filterOption.addItemListener(this);
686   }
687
688   /**
689    * Updates the displayed view based on the selected filter option
690    */
691   protected void updateCurrentView()
692   {
693     FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
694             .getSelectedItem());
695     
696     // first check if we need to rebuild dialog
697     if (selectedFilterOpt.getView() == VIEWS_QUERYING_TDB)
698     {
699       promptForTDBFetch();
700       return;
701     }
702     if (lastSelected == selectedFilterOpt)
703     {
704       // don't need to do anything, probably
705       return;
706     }
707     // otherwise, record selection
708     // and update the layout and dialog accordingly
709     lastSelected = selectedFilterOpt;
710
711     layout_switchableViews.show(pnl_switchableViews,
712             selectedFilterOpt.getView());
713     String filterTitle = mainFrame.getTitle();
714     mainFrame.setTitle(frameTitle);
715     chk_invertFilter.setVisible(false);
716     
717     if (selectedFilterOpt.getView() == VIEWS_FILTER)
718     {
719       mainFrame.setTitle(filterTitle);
720       // TDB Query has no invert as yet
721       chk_invertFilter.setVisible(selectedFilterOpt
722               .getQuerySource() instanceof PDBStructureChooserQuerySource);
723
724       if (data != selectedFilterOpt.getQuerySource()
725               || data.needsRefetch(selectedFilterOpt))
726       {
727         data = selectedFilterOpt.getQuerySource();
728         // rebuild the views completely, since prefs will also change
729         tabRefresh();
730         return;
731       }
732       else
733       {
734         filterResultSet(selectedFilterOpt.getValue());
735       }
736     }
737     else if (selectedFilterOpt.getView() == VIEWS_ENTER_ID
738             || selectedFilterOpt.getView() == VIEWS_FROM_FILE)
739     {
740       mainFrame.setTitle(MessageManager
741               .getString("label.structure_chooser_manual_association"));
742       idInputAssSeqPanel.loadCmbAssSeq();
743       fileChooserAssSeqPanel.loadCmbAssSeq();
744     }
745     validateSelections();
746   }
747
748   /**
749    * Validates user selection and enables the 'Add' and 'New View' buttons if
750    * all parameters are correct (the Add button will only be visible if there is
751    * at least one existing structure viewer open). This basically means at least
752    * one structure selected and no error messages.
753    * <p>
754    * The 'Superpose Structures' option is enabled if either more than one
755    * structure is selected, or the 'Add' to existing view option is enabled, and
756    * disabled if the only option is to open a new view of a single structure.
757    */
758   @Override
759   protected void validateSelections()
760   {
761     FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
762             .getSelectedItem());
763     btn_add.setEnabled(false);
764     String currentView = selectedFilterOpt.getView();
765     int selectedCount = 0;
766     if (currentView == VIEWS_FILTER)
767     {
768       selectedCount = getResultTable().getSelectedRows().length;
769       if (selectedCount > 0)
770       {
771         btn_add.setEnabled(true);
772       }
773     }
774     else if (currentView == VIEWS_LOCAL_PDB)
775     {
776       selectedCount = tbl_local_pdb.getSelectedRows().length;
777       if (selectedCount > 0)
778       {
779         btn_add.setEnabled(true);
780       }
781     }
782     else if (currentView == VIEWS_ENTER_ID)
783     {
784       validateAssociationEnterPdb();
785     }
786     else if (currentView == VIEWS_FROM_FILE)
787     {
788       validateAssociationFromFile();
789     }
790
791     btn_newView.setEnabled(btn_add.isEnabled());
792
793     /*
794      * enable 'Superpose' option if more than one structure is selected,
795      * or there are view(s) available to add structure(s) to
796      */
797     chk_superpose
798             .setEnabled(selectedCount > 1 || targetView.getItemCount() > 0);
799   }
800
801   /**
802    * Validates inputs from the Manual PDB entry panel
803    */
804   protected void validateAssociationEnterPdb()
805   {
806     AssociateSeqOptions assSeqOpt = (AssociateSeqOptions) idInputAssSeqPanel
807             .getCmb_assSeq().getSelectedItem();
808     lbl_pdbManualFetchStatus.setIcon(errorImage);
809     lbl_pdbManualFetchStatus.setToolTipText("");
810     if (txt_search.getText().length() > 0)
811     {
812       lbl_pdbManualFetchStatus.setToolTipText(JvSwingUtils.wrapTooltip(true,
813               MessageManager.formatMessage("info.no_pdb_entry_found_for",
814                       txt_search.getText())));
815     }
816
817     if (errorWarning.length() > 0)
818     {
819       lbl_pdbManualFetchStatus.setIcon(warningImage);
820       lbl_pdbManualFetchStatus.setToolTipText(
821               JvSwingUtils.wrapTooltip(true, errorWarning.toString()));
822     }
823
824     if (selectedSequences.length == 1 || !assSeqOpt.getName()
825             .equalsIgnoreCase("-Select Associated Seq-"))
826     {
827       txt_search.setEnabled(true);
828       if (isValidPBDEntry)
829       {
830         btn_add.setEnabled(true);
831         lbl_pdbManualFetchStatus.setToolTipText("");
832         lbl_pdbManualFetchStatus.setIcon(goodImage);
833       }
834     }
835     else
836     {
837       txt_search.setEnabled(false);
838       lbl_pdbManualFetchStatus.setIcon(errorImage);
839     }
840   }
841
842   /**
843    * Validates inputs for the manual PDB file selection options
844    */
845   protected void validateAssociationFromFile()
846   {
847     AssociateSeqOptions assSeqOpt = (AssociateSeqOptions) fileChooserAssSeqPanel
848             .getCmb_assSeq().getSelectedItem();
849     lbl_fromFileStatus.setIcon(errorImage);
850     if (selectedSequences.length == 1 || (assSeqOpt != null && !assSeqOpt
851             .getName().equalsIgnoreCase("-Select Associated Seq-")))
852     {
853       btn_pdbFromFile.setEnabled(true);
854       if (selectedPdbFileName != null && selectedPdbFileName.length() > 0)
855       {
856         btn_add.setEnabled(true);
857         lbl_fromFileStatus.setIcon(goodImage);
858       }
859     }
860     else
861     {
862       btn_pdbFromFile.setEnabled(false);
863       lbl_fromFileStatus.setIcon(errorImage);
864     }
865   }
866
867   @Override
868   protected void cmbAssSeqStateChanged()
869   {
870     validateSelections();
871   }
872   private FilterOption lastSelected=null;
873   /**
874    * Handles the state change event for the 'filter' combo-box and 'invert'
875    * check-box
876    */
877   @Override
878   protected void stateChanged(ItemEvent e)
879   {
880     if (e.getSource() instanceof JCheckBox)
881     {
882       updateCurrentView();
883     }
884     else
885     {
886       if (e.getStateChange() == ItemEvent.SELECTED)
887       {
888         updateCurrentView();
889       }
890     }
891
892   }
893
894   /**
895    * select structures for viewing by their PDB IDs
896    * 
897    * @param pdbids
898    * @return true if structures were found and marked as selected
899    */
900   public boolean selectStructure(String... pdbids)
901   {
902     boolean found = false;
903
904     FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
905             .getSelectedItem());
906     String currentView = selectedFilterOpt.getView();
907     JTable restable = (currentView == VIEWS_FILTER) ? getResultTable()
908             : (currentView == VIEWS_LOCAL_PDB) ? tbl_local_pdb : null;
909
910     if (restable == null)
911     {
912       // can't select (enter PDB ID, or load file - need to also select which
913       // sequence to associate with)
914       return false;
915     }
916
917     int pdbIdColIndex = restable.getColumn("PDB Id").getModelIndex();
918     for (int r = 0; r < restable.getRowCount(); r++)
919     {
920       for (int p = 0; p < pdbids.length; p++)
921       {
922         if (String.valueOf(restable.getValueAt(r, pdbIdColIndex))
923                 .equalsIgnoreCase(pdbids[p]))
924         {
925           restable.setRowSelectionInterval(r, r);
926           found = true;
927         }
928       }
929     }
930     return found;
931   }
932
933   /**
934    * Handles the 'New View' action
935    */
936   @Override
937   protected void newView_ActionPerformed()
938   {
939     targetView.setSelectedItem(null);
940     showStructures(false);
941   }
942
943   /**
944    * Handles the 'Add to existing viewer' action
945    */
946   @Override
947   protected void add_ActionPerformed()
948   {
949     showStructures(false);
950   }
951
952   /**
953    * structure viewer opened by this dialog, or null
954    */
955   private StructureViewer sViewer = null;
956
957   public void showStructures(boolean waitUntilFinished)
958   {
959
960     final StructureSelectionManager ssm = ap.getStructureSelectionManager();
961
962     final int preferredHeight = pnl_filter.getHeight();
963
964     Runnable viewStruc = new Runnable()
965     {
966       @Override
967       public void run()
968       {
969         FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
970                 .getSelectedItem());
971         String currentView = selectedFilterOpt.getView();
972         JTable restable = (currentView == VIEWS_FILTER) ? getResultTable()
973                 : tbl_local_pdb;
974
975         if (currentView == VIEWS_FILTER)
976         {
977           int[] selectedRows = restable.getSelectedRows();
978           PDBEntry[] pdbEntriesToView = new PDBEntry[selectedRows.length];
979           List<SequenceI> selectedSeqsToView = new ArrayList<>();
980           pdbEntriesToView = data.collectSelectedRows(restable,
981                   selectedRows, selectedSeqsToView);
982
983           SequenceI[] selectedSeqs = selectedSeqsToView
984                   .toArray(new SequenceI[selectedSeqsToView.size()]);
985           sViewer = launchStructureViewer(ssm, pdbEntriesToView, ap,
986                   selectedSeqs);
987         }
988         else if (currentView == VIEWS_LOCAL_PDB)
989         {
990           int[] selectedRows = tbl_local_pdb.getSelectedRows();
991           PDBEntry[] pdbEntriesToView = new PDBEntry[selectedRows.length];
992           int count = 0;
993           int pdbIdColIndex = tbl_local_pdb.getColumn("PDB Id")
994                   .getModelIndex();
995           int refSeqColIndex = tbl_local_pdb.getColumn("Ref Sequence")
996                   .getModelIndex();
997           List<SequenceI> selectedSeqsToView = new ArrayList<>();
998           for (int row : selectedRows)
999           {
1000             PDBEntry pdbEntry = ((PDBEntryTableModel) tbl_local_pdb
1001                     .getModel()).getPDBEntryAt(row).getPdbEntry();
1002
1003             pdbEntriesToView[count++] = pdbEntry;
1004             SequenceI selectedSeq = (SequenceI) tbl_local_pdb
1005                     .getValueAt(row, refSeqColIndex);
1006             selectedSeqsToView.add(selectedSeq);
1007           }
1008           SequenceI[] selectedSeqs = selectedSeqsToView
1009                   .toArray(new SequenceI[selectedSeqsToView.size()]);
1010           sViewer = launchStructureViewer(ssm, pdbEntriesToView, ap,
1011                   selectedSeqs);
1012         }
1013         else if (currentView == VIEWS_ENTER_ID)
1014         {
1015           SequenceI userSelectedSeq = ((AssociateSeqOptions) idInputAssSeqPanel
1016                   .getCmb_assSeq().getSelectedItem()).getSequence();
1017           if (userSelectedSeq != null)
1018           {
1019             selectedSequence = userSelectedSeq;
1020           }
1021           String pdbIdStr = txt_search.getText();
1022           PDBEntry pdbEntry = selectedSequence.getPDBEntry(pdbIdStr);
1023           if (pdbEntry == null)
1024           {
1025             pdbEntry = new PDBEntry();
1026             if (pdbIdStr.split(":").length > 1)
1027             {
1028               pdbEntry.setId(pdbIdStr.split(":")[0]);
1029               pdbEntry.setChainCode(pdbIdStr.split(":")[1].toUpperCase());
1030             }
1031             else
1032             {
1033               pdbEntry.setId(pdbIdStr);
1034             }
1035             pdbEntry.setType(PDBEntry.Type.PDB);
1036             selectedSequence.getDatasetSequence().addPDBId(pdbEntry);
1037           }
1038
1039           PDBEntry[] pdbEntriesToView = new PDBEntry[] { pdbEntry };
1040           sViewer = launchStructureViewer(ssm, pdbEntriesToView, ap,
1041                   new SequenceI[]
1042                   { selectedSequence });
1043         }
1044         else if (currentView == VIEWS_FROM_FILE)
1045         {
1046           SequenceI userSelectedSeq = ((AssociateSeqOptions) fileChooserAssSeqPanel
1047                   .getCmb_assSeq().getSelectedItem()).getSequence();
1048           if (userSelectedSeq != null)
1049           {
1050             selectedSequence = userSelectedSeq;
1051           }
1052           PDBEntry fileEntry = new AssociatePdbFileWithSeq()
1053                   .associatePdbWithSeq(selectedPdbFileName,
1054                           DataSourceType.FILE, selectedSequence, true,
1055                           Desktop.instance);
1056
1057           sViewer = launchStructureViewer(ssm, new PDBEntry[] { fileEntry },
1058                   ap, new SequenceI[]
1059                   { selectedSequence });
1060         }
1061         SwingUtilities.invokeLater(new Runnable()
1062         {
1063           @Override
1064           public void run()
1065           {
1066             closeAction(preferredHeight);
1067             mainFrame.dispose();
1068           }
1069         });
1070       }
1071     };
1072     Thread runner = new Thread(viewStruc);
1073     runner.start();
1074     if (waitUntilFinished)
1075     {
1076       while (sViewer == null ? runner.isAlive()
1077               : (sViewer.sview == null ? true
1078                       : !sViewer.sview.hasMapping()))
1079       {
1080         try
1081         {
1082           Thread.sleep(300);
1083         } catch (InterruptedException ie)
1084         {
1085
1086         }
1087       }
1088     }
1089   }
1090
1091   /**
1092    * Answers a structure viewer (new or existing) configured to superimpose
1093    * added structures or not according to the user's choice
1094    * 
1095    * @param ssm
1096    * @return
1097    */
1098   StructureViewer getTargetedStructureViewer(StructureSelectionManager ssm)
1099   {
1100     Object sv = targetView.getSelectedItem();
1101
1102     return sv == null ? new StructureViewer(ssm) : (StructureViewer) sv;
1103   }
1104
1105   /**
1106    * Adds PDB structures to a new or existing structure viewer
1107    * 
1108    * @param ssm
1109    * @param pdbEntriesToView
1110    * @param alignPanel
1111    * @param sequences
1112    * @return
1113    */
1114   private StructureViewer launchStructureViewer(
1115           StructureSelectionManager ssm, final PDBEntry[] pdbEntriesToView,
1116           final AlignmentPanel alignPanel, SequenceI[] sequences)
1117   {
1118     long progressId = sequences.hashCode();
1119     setProgressBar(MessageManager
1120             .getString("status.launching_3d_structure_viewer"), progressId);
1121     final StructureViewer theViewer = getTargetedStructureViewer(ssm);
1122     boolean superimpose = chk_superpose.isSelected();
1123     theViewer.setSuperpose(superimpose);
1124
1125     /*
1126      * remember user's choice of superimpose or not
1127      */
1128     Cache.setProperty(AUTOSUPERIMPOSE,
1129             Boolean.valueOf(superimpose).toString());
1130
1131     setProgressBar(null, progressId);
1132     if (SiftsSettings.isMapWithSifts())
1133     {
1134       List<SequenceI> seqsWithoutSourceDBRef = new ArrayList<>();
1135       int p = 0;
1136       // TODO: skip PDBEntry:Sequence pairs where PDBEntry doesn't look like a
1137       // real PDB ID. For moment, we can also safely do this if there is already
1138       // a known mapping between the PDBEntry and the sequence.
1139       for (SequenceI seq : sequences)
1140       {
1141         PDBEntry pdbe = pdbEntriesToView[p++];
1142         if (pdbe != null && pdbe.getFile() != null)
1143         {
1144           StructureMapping[] smm = ssm.getMapping(pdbe.getFile());
1145           if (smm != null && smm.length > 0)
1146           {
1147             for (StructureMapping sm : smm)
1148             {
1149               if (sm.getSequence() == seq)
1150               {
1151                 continue;
1152               }
1153             }
1154           }
1155         }
1156         if (seq.getPrimaryDBRefs().isEmpty())
1157         {
1158           seqsWithoutSourceDBRef.add(seq);
1159           continue;
1160         }
1161       }
1162       if (!seqsWithoutSourceDBRef.isEmpty())
1163       {
1164         int y = seqsWithoutSourceDBRef.size();
1165         setProgressBar(MessageManager.formatMessage(
1166                 "status.fetching_dbrefs_for_sequences_without_valid_refs",
1167                 y), progressId);
1168         SequenceI[] seqWithoutSrcDBRef = seqsWithoutSourceDBRef
1169                 .toArray(new SequenceI[y]);
1170         DBRefFetcher dbRefFetcher = new DBRefFetcher(seqWithoutSrcDBRef);
1171         dbRefFetcher.fetchDBRefs(true);
1172
1173         setProgressBar("Fetch complete.", progressId); // todo i18n
1174       }
1175     }
1176     if (pdbEntriesToView.length > 1)
1177     {
1178       setProgressBar(
1179               MessageManager.getString(
1180                       "status.fetching_3d_structures_for_selected_entries"),
1181               progressId);
1182       theViewer.viewStructures(pdbEntriesToView, sequences, alignPanel);
1183     }
1184     else
1185     {
1186       setProgressBar(MessageManager.formatMessage(
1187               "status.fetching_3d_structures_for",
1188               pdbEntriesToView[0].getId()), progressId);
1189       theViewer.viewStructures(pdbEntriesToView[0], sequences, alignPanel);
1190     }
1191     setProgressBar(null, progressId);
1192     // remember the last viewer we used...
1193     lastTargetedView = theViewer;
1194     return theViewer;
1195   }
1196
1197   /**
1198    * Populates the combo-box used in associating manually fetched structures to
1199    * a unique sequence when more than one sequence selection is made.
1200    */
1201   @Override
1202   protected void populateCmbAssociateSeqOptions(
1203           JComboBox<AssociateSeqOptions> cmb_assSeq,
1204           JLabel lbl_associateSeq)
1205   {
1206     cmb_assSeq.removeAllItems();
1207     cmb_assSeq.addItem(
1208             new AssociateSeqOptions("-Select Associated Seq-", null));
1209     lbl_associateSeq.setVisible(false);
1210     if (selectedSequences.length > 1)
1211     {
1212       for (SequenceI seq : selectedSequences)
1213       {
1214         cmb_assSeq.addItem(new AssociateSeqOptions(seq));
1215       }
1216     }
1217     else
1218     {
1219       String seqName = selectedSequence.getDisplayId(false);
1220       seqName = seqName.length() <= 40 ? seqName : seqName.substring(0, 39);
1221       lbl_associateSeq.setText(seqName);
1222       lbl_associateSeq.setVisible(true);
1223       cmb_assSeq.setVisible(false);
1224     }
1225   }
1226
1227   protected boolean isStructuresDiscovered()
1228   {
1229     return discoveredStructuresSet != null
1230             && !discoveredStructuresSet.isEmpty();
1231   }
1232
1233   protected int PDB_ID_MIN = 3;// or: (Jalview.isJS() ? 3 : 1); // Bob proposes
1234                                // this.
1235   // Doing a search for "1" or "1c" is valuable?
1236   // Those work but are enormously slow.
1237
1238   @Override
1239   protected void txt_search_ActionPerformed()
1240   {
1241     String text = txt_search.getText().trim();
1242     if (text.length() >= PDB_ID_MIN)
1243       new Thread()
1244       {
1245
1246         @Override
1247         public void run()
1248         {
1249           errorWarning.setLength(0);
1250           isValidPBDEntry = false;
1251           if (text.length() > 0)
1252           {
1253             // TODO move this pdb id search into the PDB specific
1254             // FTSSearchEngine
1255             // for moment, it will work fine as is because it is self-contained
1256             String searchTerm = text.toLowerCase();
1257             searchTerm = searchTerm.split(":")[0];
1258             // System.out.println(">>>>> search term : " + searchTerm);
1259             List<FTSDataColumnI> wantedFields = new ArrayList<>();
1260             FTSRestRequest pdbRequest = new FTSRestRequest();
1261             pdbRequest.setAllowEmptySeq(false);
1262             pdbRequest.setResponseSize(1);
1263             pdbRequest.setFieldToSearchBy("(pdb_id:");
1264             pdbRequest.setWantedFields(wantedFields);
1265             pdbRequest.setSearchTerm(searchTerm + ")");
1266             pdbRequest.setAssociatedSequence(selectedSequence);
1267             FTSRestClientI pdbRestClient = PDBFTSRestClient.getInstance();
1268             wantedFields.add(pdbRestClient.getPrimaryKeyColumn());
1269             FTSRestResponse resultList;
1270             try
1271             {
1272               resultList = pdbRestClient.executeRequest(pdbRequest);
1273             } catch (Exception e)
1274             {
1275               errorWarning.append(e.getMessage());
1276               return;
1277             } finally
1278             {
1279               validateSelections();
1280             }
1281             if (resultList.getSearchSummary() != null
1282                     && resultList.getSearchSummary().size() > 0)
1283             {
1284               isValidPBDEntry = true;
1285             }
1286           }
1287           validateSelections();
1288         }
1289       }.start();
1290   }
1291
1292   @Override
1293   protected void tabRefresh()
1294   {
1295     if (selectedSequences != null)
1296     {
1297       Thread refreshThread = new Thread(new Runnable()
1298       {
1299         @Override
1300         public void run()
1301         {
1302           fetchStructuresMetaData();
1303           // populateFilterComboBox(true, cachedPDBExists);
1304
1305           filterResultSet(
1306                   ((FilterOption) cmb_filterOption.getSelectedItem())
1307                           .getValue());
1308         }
1309       });
1310       refreshThread.start();
1311     }
1312   }
1313
1314   public class PDBEntryTableModel extends AbstractTableModel
1315   {
1316     String[] columns = { "Ref Sequence", "PDB Id", "Chain", "Type",
1317         "File" };
1318
1319     private List<CachedPDB> pdbEntries;
1320
1321     public PDBEntryTableModel(List<CachedPDB> pdbEntries)
1322     {
1323       this.pdbEntries = new ArrayList<>(pdbEntries);
1324     }
1325
1326     @Override
1327     public String getColumnName(int columnIndex)
1328     {
1329       return columns[columnIndex];
1330     }
1331
1332     @Override
1333     public int getRowCount()
1334     {
1335       return pdbEntries.size();
1336     }
1337
1338     @Override
1339     public int getColumnCount()
1340     {
1341       return columns.length;
1342     }
1343
1344     @Override
1345     public boolean isCellEditable(int row, int column)
1346     {
1347       return false;
1348     }
1349
1350     @Override
1351     public Object getValueAt(int rowIndex, int columnIndex)
1352     {
1353       Object value = "??";
1354       CachedPDB entry = pdbEntries.get(rowIndex);
1355       switch (columnIndex)
1356       {
1357       case 0:
1358         value = entry.getSequence();
1359         break;
1360       case 1:
1361         value = entry.getQualifiedId();
1362         break;
1363       case 2:
1364         value = entry.getPdbEntry().getChainCode() == null ? "_"
1365                 : entry.getPdbEntry().getChainCode();
1366         break;
1367       case 3:
1368         value = entry.getPdbEntry().getType();
1369         break;
1370       case 4:
1371         value = entry.getPdbEntry().getFile();
1372         break;
1373       }
1374       return value;
1375     }
1376
1377     @Override
1378     public Class<?> getColumnClass(int columnIndex)
1379     {
1380       return columnIndex == 0 ? SequenceI.class : PDBEntry.class;
1381     }
1382
1383     public CachedPDB getPDBEntryAt(int row)
1384     {
1385       return pdbEntries.get(row);
1386     }
1387
1388   }
1389
1390   private class CachedPDB
1391   {
1392     private SequenceI sequence;
1393
1394     private PDBEntry pdbEntry;
1395
1396     public CachedPDB(SequenceI sequence, PDBEntry pdbEntry)
1397     {
1398       this.sequence = sequence;
1399       this.pdbEntry = pdbEntry;
1400     }
1401
1402     public String getQualifiedId()
1403     {
1404       if (pdbEntry.hasProvider())
1405       {
1406         return pdbEntry.getProvider() + ":" + pdbEntry.getId();
1407       }
1408       return pdbEntry.toString();
1409     }
1410
1411     public SequenceI getSequence()
1412     {
1413       return sequence;
1414     }
1415
1416     public PDBEntry getPdbEntry()
1417     {
1418       return pdbEntry;
1419     }
1420
1421   }
1422
1423   private IProgressIndicator progressBar;
1424
1425   @Override
1426   public void setProgressBar(String message, long id)
1427   {
1428     progressBar.setProgressBar(message, id);
1429   }
1430
1431   @Override
1432   public void registerHandler(long id, IProgressIndicatorHandler handler)
1433   {
1434     progressBar.registerHandler(id, handler);
1435   }
1436
1437   @Override
1438   public boolean operationInProgress()
1439   {
1440     return progressBar.operationInProgress();
1441   }
1442
1443   public JalviewStructureDisplayI getOpenedStructureViewer()
1444   {
1445     return sViewer == null ? null : sViewer.sview;
1446   }
1447
1448   @Override
1449   protected void setFTSDocFieldPrefs(FTSDataColumnPreferences newPrefs)
1450   {
1451     data.setDocFieldPrefs(newPrefs);
1452
1453   }
1454
1455   /**
1456    * 
1457    * @return true when all initialisation threads have finished and dialog is
1458    *         visible
1459    */
1460   public boolean isDialogVisible()
1461   {
1462     return mainFrame != null && data != null && cmb_filterOption != null
1463             && mainFrame.isVisible()
1464             && cmb_filterOption.getSelectedItem() != null;
1465   }
1466 }