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