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