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