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