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