JAL-1668 added validation for manual entry of pdb structures
[jalview.git] / src / jalview / gui / StructureChooser.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.8.2)
3  * Copyright (C) 2014 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.datamodel.DBRefEntry;
25 import jalview.datamodel.PDBEntry;
26 import jalview.datamodel.SequenceI;
27 import jalview.jbgui.GStructureChooser;
28 import jalview.util.MessageManager;
29 import jalview.ws.dbsources.PDBRestClient;
30 import jalview.ws.dbsources.PDBRestClient.PDBDocField;
31 import jalview.ws.uimodel.PDBRestRequest;
32 import jalview.ws.uimodel.PDBRestResponse;
33 import jalview.ws.uimodel.PDBRestResponse.PDBResponseSummary;
34
35 import java.awt.event.ItemEvent;
36 import java.util.ArrayList;
37 import java.util.Collection;
38 import java.util.HashSet;
39 import java.util.List;
40 import java.util.Vector;
41
42 import javax.swing.JCheckBox;
43 import javax.swing.JComboBox;
44 import javax.swing.ListSelectionModel;
45
46 /**
47  * Provides the behaviors for the Structure chooser Panel
48  * 
49  * @author tcnofoegbu
50  *
51  */
52 @SuppressWarnings("serial")
53 public class StructureChooser extends GStructureChooser
54 {
55   private boolean structuresDiscovered = false;
56
57   private SequenceI selectedSequence;
58
59   private SequenceI[] selectedSequences;
60
61   private IProgressIndicator progressIndicator;
62
63   private Collection<PDBResponseSummary> discoveredStructuresSet = new HashSet<PDBResponseSummary>();
64
65   private PDBRestRequest pdbRequest;
66
67   private PDBRestClient pdbRestCleint;
68
69   private String selectedPdbFileName;
70
71   private boolean isValidPBDEntry;
72
73   public StructureChooser(SequenceI[] selectedSeqs, SequenceI selectedSeq,
74           AlignmentPanel ap)
75   {
76     this.ap = ap;
77     this.selectedSequence = selectedSeq;
78     this.selectedSequences = selectedSeqs;
79     this.progressIndicator = (ap == null) ? null : ap.alignFrame;
80     init();
81   }
82
83   /**
84    * Initializes parameters used by the Structure Chooser Panel
85    */
86   public void init()
87   {
88     Thread discoverPDBStructuresThread = new Thread(new Runnable()
89     {
90       @Override
91       public void run()
92       {
93         long startTime = System.currentTimeMillis();
94         String msg = MessageManager.getString("status.fetching_db_refs");
95         updateProgressIndicator(msg, startTime);
96         fetchStructuresMetaData();
97         populateFilterComboBox();
98         updateProgressIndicator(null, startTime);
99         mainFrame.setVisible(true);
100         updateCurrentView();
101       }
102     });
103     discoverPDBStructuresThread.start();
104   }
105
106   /**
107    * Updates the progress indicator with the specified message
108    * 
109    * @param message
110    *          displayed message for the operation
111    * @param id
112    *          unique handle for this indicator
113    */
114   public void updateProgressIndicator(String message, long id)
115   {
116     if (progressIndicator != null)
117     {
118       progressIndicator.setProgressBar(message, id);
119     }
120   }
121
122   /**
123    * Retrieve meta-data for all the structure(s) for a given sequence(s) in a
124    * selection group
125    */
126   public void fetchStructuresMetaData()
127   {
128     long startTime = System.currentTimeMillis();
129     List<PDBDocField> wantedFields = new ArrayList<PDBDocField>();
130     // wantedFields.add(PDBDocField.MOLECULE_TYPE);
131     wantedFields.add(PDBDocField.PDB_ID);
132     // wantedFields.add(PDBDocField.GENUS);
133     // wantedFields.add(PDBDocField.GENE_NAME);
134     wantedFields.add(PDBDocField.TITLE);
135     pdbRequest = new PDBRestRequest();
136     pdbRequest.setAllowEmptySeq(false);
137     pdbRequest.setResponseSize(500);
138     pdbRequest.setFieldToSearchBy("(text:");
139     pdbRequest.setWantedFields(wantedFields);
140     for (SequenceI seq : selectedSequences)
141     {
142       pdbRequest.setSearchTerm(buildQuery(seq) + ")");
143       pdbRequest.setAssociatedSequence(seq.getName());
144       pdbRestCleint = new PDBRestClient();
145       PDBRestResponse resultList = pdbRestCleint.executeRequest(pdbRequest);
146       if (resultList.getSearchSummary() != null
147               && !resultList.getSearchSummary().isEmpty())
148       {
149         discoveredStructuresSet.addAll(resultList.getSearchSummary());
150         updateSequenceDbRef(seq, resultList.getSearchSummary());
151       }
152     }
153
154     int noOfStructuresFound = 0;
155     if (discoveredStructuresSet != null
156             && !discoveredStructuresSet.isEmpty())
157     {
158       tbl_summary.setModel(PDBRestResponse.getTableModel(pdbRequest,
159               discoveredStructuresSet));
160       structuresDiscovered = true;
161       noOfStructuresFound = discoveredStructuresSet.size();
162     }
163     String totalTime = (System.currentTimeMillis() - startTime)
164             + " milli secs";
165     mainFrame.setTitle("Structure Chooser - " + noOfStructuresFound
166             + " Found (" + totalTime + ")");
167   }
168
169   /**
170    * Update the DBRef entry for a given sequence with values retrieved from
171    * PDBResponseSummary
172    * 
173    * @param seq
174    *          the Sequence to update its DBRef entry
175    * @param responseSummaries
176    *          a collection of PDBResponseSummary
177    */
178   public void updateSequenceDbRef(SequenceI seq,
179           Collection<PDBResponseSummary> responseSummaries)
180   {
181     for (PDBResponseSummary response : responseSummaries)
182     {
183       PDBEntry newEntry = new PDBEntry();
184       newEntry.setId(response.getPdbId());
185       newEntry.setType("PDB");
186       seq.getDatasetSequence().addPDBId(newEntry);
187     }
188   }
189
190   /**
191    * Builds a query string for a given sequences using its DBRef entries
192    * 
193    * @param seq
194    *          the sequences to build a query for
195    * @return the built query string
196    */
197   @SuppressWarnings("unchecked")
198   public static String buildQuery(SequenceI seq)
199   {
200     String query = seq.getName();
201     StringBuilder queryBuilder = new StringBuilder();
202     int count = 0;
203
204     if (seq.getPDBId() != null)
205     {
206       for (PDBEntry entry : (Vector<PDBEntry>) seq.getPDBId())
207       {
208         queryBuilder.append("text:").append(entry.getId()).append(" OR ");
209       }
210     }
211
212     if (seq.getDBRef() != null && seq.getDBRef().length != 0)
213     {
214       for (DBRefEntry dbRef : seq.getDBRef())
215       {
216         queryBuilder.append("text:")
217                 .append(dbRef.getAccessionId().replaceAll("GO:", ""))
218                 .append(" OR ");
219         ++count;
220         if (count > 10)
221         {
222           break;
223         }
224       }
225       int endIndex = queryBuilder.lastIndexOf(" OR ");
226       query = queryBuilder.toString().substring(5, endIndex);
227     }
228     return query;
229   }
230
231   /**
232    * Filters a given list of discovered structures based on supplied argument
233    * 
234    * @param fieldToFilterBy
235    *          the field to filter by
236    */
237   public void filterResultSet(final String fieldToFilterBy)
238   {
239     Thread filterThread = new Thread(new Runnable()
240     {
241       @Override
242       public void run()
243       {
244         long startTime = System.currentTimeMillis();
245         try
246         {
247           lbl_loading.setVisible(true);
248           pdbRequest.setResponseSize(1);
249           pdbRequest.setFieldToSearchBy("(text:");
250           pdbRequest.setFieldToSortBy(fieldToFilterBy,
251                   !chk_invertFilter.isSelected());
252
253           Collection<PDBResponseSummary> filteredResponse = new HashSet<PDBResponseSummary>();
254           for (SequenceI seq : selectedSequences)
255           {
256             pdbRequest.setSearchTerm(buildQuery(seq) + ")");
257             pdbRequest.setAssociatedSequence(seq.getName());
258             pdbRestCleint = new PDBRestClient();
259             PDBRestResponse resultList = pdbRestCleint
260                     .executeRequest(pdbRequest);
261             if (resultList.getSearchSummary() != null
262                     && !resultList.getSearchSummary().isEmpty())
263             {
264               filteredResponse.addAll(resultList.getSearchSummary());
265             }
266           }
267
268           if (filteredResponse != null)
269           {
270             int filterResponseCount = filteredResponse.size();
271             List<PDBResponseSummary> originalDiscoveredStructuresList = new ArrayList<PDBResponseSummary>(
272                     discoveredStructuresSet);
273             originalDiscoveredStructuresList.removeAll(filteredResponse);
274             Collection<PDBResponseSummary> reorderedStructuresSet = new ArrayList<PDBResponseSummary>();
275             reorderedStructuresSet.addAll(filteredResponse);
276             reorderedStructuresSet.addAll(originalDiscoveredStructuresList);
277
278             tbl_summary.setModel(PDBRestResponse.getTableModel(pdbRequest,
279                     reorderedStructuresSet));
280
281             ListSelectionModel model = tbl_summary.getSelectionModel();
282             model.clearSelection();
283             for (int x = 0; x < filterResponseCount; x++)
284             {
285               model.addSelectionInterval(x, x);
286             }
287
288             // Discard unwanted objects to make them eligible for garbage
289             // collection
290             originalDiscoveredStructuresList = null;
291             reorderedStructuresSet = null;
292           }
293
294           lbl_loading.setVisible(false);
295         } catch (Exception e)
296         {
297           e.printStackTrace();
298         }
299         String totalTime = (System.currentTimeMillis() - startTime)
300                 + " milli secs";
301         mainFrame.setTitle("Structure Chooser - Filter time (" + totalTime
302                 + ")");
303
304         validateSelections();
305       }
306     });
307     filterThread.start();
308   }
309
310   /**
311    * Determines the column index for the pdb id in the summary table. The pdb id
312    * serves as a unique identifier for a given row in the summary table
313    * 
314    * @param wantedFeilds
315    *          the available table columns in no particular order
316    * @return the pdb id field column index
317    */
318   public static int getPDBIdColumIndex(Collection<PDBDocField> wantedFeilds)
319   {
320     int pdbFeildIndex = 1;
321     for (PDBDocField feild : wantedFeilds)
322     {
323       if (feild.equals(PDBDocField.PDB_ID))
324       {
325         break;
326       }
327       ++pdbFeildIndex;
328     }
329     return pdbFeildIndex;
330   }
331
332   /**
333    * Handles action event for btn_pdbFromFile
334    */
335   public void pdbFromFile_actionPerformed()
336   {
337     jalview.io.JalviewFileChooser chooser = new jalview.io.JalviewFileChooser(
338             jalview.bin.Cache.getProperty("LAST_DIRECTORY"));
339     chooser.setFileView(new jalview.io.JalviewFileView());
340     chooser.setDialogTitle(MessageManager.formatMessage(
341             "label.select_pdb_file_for", new String[]
342             { selectedSequence.getDisplayId(false) }));
343     chooser.setToolTipText(MessageManager.formatMessage(
344             "label.load_pdb_file_associate_with_sequence", new String[]
345             { selectedSequence.getDisplayId(false) }));
346
347     int value = chooser.showOpenDialog(null);
348     if (value == jalview.io.JalviewFileChooser.APPROVE_OPTION)
349     {
350       selectedPdbFileName = chooser.getSelectedFile().getPath();
351       jalview.bin.Cache.setProperty("LAST_DIRECTORY", selectedPdbFileName);
352       validateSelections();
353     }
354   }
355
356   /**
357    * Populates the filter combo-box options dynamically depending on discovered
358    * structures
359    */
360   protected void populateFilterComboBox()
361   {
362     if (isStructuresDiscovered())
363     {
364       cmb_filterOption.addItem(new FilterOption("Best Quality",
365               PDBDocField.OVERALL_QUALITY.getCode(), VIEWS_FILTER));
366       cmb_filterOption.addItem(new FilterOption("Best UniProt Coverage",
367               PDBDocField.UNIPROT_COVERAGE.getCode(), VIEWS_FILTER));
368       cmb_filterOption.addItem(new FilterOption("Highest Resolution",
369               PDBDocField.RESOLUTION.getCode(), VIEWS_FILTER));
370       cmb_filterOption.addItem(new FilterOption("Highest Protein Chain",
371               PDBDocField.PROTEIN_CHAIN_COUNT.getCode(), VIEWS_FILTER));
372       cmb_filterOption.addItem(new FilterOption("Highest Bound Molecules",
373               PDBDocField.BOUND_MOLECULE_COUNT.getCode(), VIEWS_FILTER));
374       cmb_filterOption.addItem(new FilterOption("Highest Polymer Residues",
375               PDBDocField.POLYMER_RESIDUE_COUNT.getCode(), VIEWS_FILTER));
376     }
377     cmb_filterOption.addItem(new FilterOption("Enter PDB Id", "-",
378             VIEWS_ENTER_ID));
379     cmb_filterOption.addItem(new FilterOption("From File", "-",
380             VIEWS_FROM_FILE));
381   }
382
383   /**
384    * Updates the displayed view based on the selected filter option
385    */
386   protected void updateCurrentView()
387   {
388     FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
389             .getSelectedItem());
390     layout_switchableViews.show(pnl_switchableViews,
391             selectedFilterOpt.getView());
392     String filterTitle = mainFrame.getTitle();
393     mainFrame.setTitle(frameTitle);
394     chk_invertFilter.setVisible(false);
395     if (selectedFilterOpt.getView() == VIEWS_FILTER)
396     {
397       mainFrame.setTitle(filterTitle);
398       chk_invertFilter.setVisible(true);
399       filterResultSet(selectedFilterOpt.getValue());
400     }
401     else
402     {
403       idInputAssSeqPanel.loadCmbAssSeq();
404       fileChooserAssSeqPanel.loadCmbAssSeq();
405     }
406     validateSelections();
407   }
408
409   /**
410    * Validates user selection and activates the view button if all parameters
411    * are correct
412    */
413   public void validateSelections()
414   {
415     FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
416             .getSelectedItem());
417     btn_view.setEnabled(false);
418     String currentView = selectedFilterOpt.getView();
419     if (currentView == VIEWS_FILTER)
420     {
421       if (tbl_summary.getSelectedRows().length > 0)
422       {
423         btn_view.setEnabled(true);
424       }
425     }
426     else if (currentView == VIEWS_ENTER_ID)
427     {
428       validateAssociationEnterPdb();
429     }
430     else if (currentView == VIEWS_FROM_FILE)
431     {
432       validateAssociationFromFile();
433     }
434
435   }
436
437   /**
438    * Validates inputs from the Manual PDB entry panel
439    */
440   public void validateAssociationEnterPdb()
441   {
442     AssociateSeqOptions assSeqOpt = (AssociateSeqOptions) idInputAssSeqPanel
443             .getCmb_assSeq().getSelectedItem();
444     lbl_pdbManualFetchStatus.setIcon(errorImage);
445     if (selectedSequences.length == 1
446             || !assSeqOpt.getName().equalsIgnoreCase(
447                     "-Select Associated Seq-"))
448     {
449       txt_search.setEnabled(true);
450       if (isValidPBDEntry)
451       {
452         btn_view.setEnabled(true);
453         lbl_pdbManualFetchStatus.setIcon(goodImage);
454       }
455     }
456     else
457     {
458       txt_search.setEnabled(false);
459       lbl_pdbManualFetchStatus.setIcon(errorImage);
460     }
461   }
462
463   /**
464    * Validates inputs for the manual PDB file selection options
465    */
466   public void validateAssociationFromFile()
467   {
468     AssociateSeqOptions assSeqOpt = (AssociateSeqOptions) fileChooserAssSeqPanel
469             .getCmb_assSeq().getSelectedItem();
470     lbl_fromFileStatus.setIcon(errorImage);
471     if (selectedSequences.length == 1
472             || (assSeqOpt != null
473             && !assSeqOpt.getName().equalsIgnoreCase(
474                     "-Select Associated Seq-")))
475     {
476       btn_pdbFromFile.setEnabled(true);
477       if (selectedPdbFileName != null && selectedPdbFileName.length() > 0)
478       {
479         btn_view.setEnabled(true);
480         lbl_fromFileStatus.setIcon(goodImage);
481       }
482     }
483     else
484     {
485       btn_pdbFromFile.setEnabled(false);
486       lbl_fromFileStatus.setIcon(errorImage);
487     }
488   }
489
490   @Override
491   public void cmbAssSeqStateChanged()
492   {
493     validateSelections();
494   }
495
496   /**
497    * Handles the state change event for the 'filter' combo-box and 'invert'
498    * check-box
499    */
500   @Override
501   protected void stateChanged(ItemEvent e)
502   {
503     if (e.getSource() instanceof JCheckBox)
504     {
505       updateCurrentView();
506     }
507     else
508     {
509       if (e.getStateChange() == ItemEvent.SELECTED)
510       {
511         updateCurrentView();
512       }
513     }
514
515   }
516
517   /**
518    * Handles action event for btn_ok
519    */
520   @Override
521   public void ok_ActionPerformed()
522   {
523     FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
524             .getSelectedItem());
525     String currentView = selectedFilterOpt.getView();
526     if (currentView == VIEWS_FILTER)
527     {
528       int pdbIdCol = getPDBIdColumIndex(pdbRequest.getWantedFields());
529       int[] selectedRows = tbl_summary.getSelectedRows();
530       PDBEntry[] pdbEntriesToView = new PDBEntry[selectedRows.length];
531       int count = 0;
532       for (int summaryRow : selectedRows)
533       {
534         String pdbIdStr = tbl_summary.getValueAt(summaryRow, pdbIdCol)
535                 .toString();
536         PDBEntry pdbEntry = new PDBEntry();
537         pdbEntry.setId(pdbIdStr);
538         pdbEntry.setType("PDB");
539         pdbEntriesToView[count++] = pdbEntry;
540       }
541       new StructureViewer(ap.getStructureSelectionManager())
542               .viewStructures(ap, pdbEntriesToView,
543                       ap.av.collateForPDB(pdbEntriesToView));
544     }
545     else if (currentView == VIEWS_ENTER_ID)
546     {
547       selectedSequence = ((AssociateSeqOptions) idInputAssSeqPanel
548               .getCmb_assSeq().getSelectedItem()).getSequence();
549       PDBEntry pdbEntry = new PDBEntry();
550       pdbEntry.setId(txt_search.getText());
551       pdbEntry.setType("PDB");
552       selectedSequence.getDatasetSequence().addPDBId(pdbEntry);
553       PDBEntry[] pdbEntriesToView = new PDBEntry[]
554       { pdbEntry };
555       new StructureViewer(ap.getStructureSelectionManager())
556               .viewStructures(ap, pdbEntriesToView,
557                       ap.av.collateForPDB(pdbEntriesToView));
558     }
559     else if (currentView == VIEWS_FROM_FILE)
560     {
561       selectedSequence = ((AssociateSeqOptions) fileChooserAssSeqPanel
562               .getCmb_assSeq().getSelectedItem()).getSequence();
563       new AssociatePdbFileWithSeq().associatePdbWithSeq(
564               selectedPdbFileName, jalview.io.AppletFormatAdapter.FILE,
565               selectedSequence, true, Desktop.instance);
566     }
567     mainFrame.dispose();
568   }
569
570   /**
571    * Populates the combo-box used in associating manually fetched structures to
572    * a unique sequence when more than one sequence selection is made.
573    */
574   public void populateCmbAssociateSeqOptions(
575           JComboBox<AssociateSeqOptions> cmb_assSeq)
576   {
577     cmb_assSeq.removeAllItems();
578     cmb_assSeq.addItem(new AssociateSeqOptions("-Select Associated Seq-",
579             null));
580     // cmb_assSeq.addItem(new AssociateSeqOptions("Auto Detect", null));
581     if (selectedSequences.length > 1)
582     {
583       for (SequenceI seq : selectedSequences)
584       {
585         cmb_assSeq.addItem(new AssociateSeqOptions(seq));
586       }
587     }
588     else
589     {
590       cmb_assSeq.setVisible(false);
591     }
592   }
593
594   public boolean isStructuresDiscovered()
595   {
596     return structuresDiscovered;
597   }
598
599   public void setStructuresDiscovered(boolean structuresDiscovered)
600   {
601     this.structuresDiscovered = structuresDiscovered;
602   }
603
604   public Collection<PDBResponseSummary> getDiscoveredStructuresSet()
605   {
606     return discoveredStructuresSet;
607   }
608
609   @Override
610   protected void txt_search_ActionPerformed()
611   {
612     isValidPBDEntry = false;
613     if (txt_search.getText().length() > 0)
614     {
615       List<PDBDocField> wantedFields = new ArrayList<PDBDocField>();
616       wantedFields.add(PDBDocField.PDB_ID);
617       pdbRequest = new PDBRestRequest();
618       pdbRequest.setAllowEmptySeq(false);
619       pdbRequest.setResponseSize(1);
620       pdbRequest.setFieldToSearchBy("(pdb_id:");
621       pdbRequest.setWantedFields(wantedFields);
622       pdbRequest.setSearchTerm(txt_search.getText() + ")");
623       pdbRequest.setAssociatedSequence(selectedSequence.getName());
624       pdbRestCleint = new PDBRestClient();
625       PDBRestResponse resultList = pdbRestCleint.executeRequest(pdbRequest);
626       if (resultList.getSearchSummary() != null
627               && resultList.getSearchSummary().size() > 0)
628       {
629         isValidPBDEntry = true;
630       }
631     }
632     validateSelections();
633   }
634
635 }