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