Implement UI for subanalysis mode
[mussa.git] / qui / MussaWindow.cpp
1 #include <QAction>
2 #include <QApplication>
3 #include <QAssistantClient>
4 #include <QDir>
5 #include <QFileDialog>
6 #include <QHBoxLayout>
7 #include <QIcon>
8 #include <QMenuBar>
9 #include <QMessageBox>
10 #include <QScrollBar>
11 #include <QStatusBar>
12 #include <QString>
13 #include <QStringList>
14 #include <QWhatsThis>
15
16 #if defined(Q_WS_MAC)
17 #include <CoreFoundation/CoreFoundation.h>
18 #endif 
19
20 #include "qui/MussaWindow.hpp"
21 #include "mussa_exceptions.hpp"
22
23 #include <memory>
24 #include <iterator>
25 #include <iostream>
26
27 #include <boost/filesystem/path.hpp>
28 namespace fs = boost::filesystem;
29 #include <boost/bind.hpp>
30
31 using namespace std;
32
33 MussaWindow::MussaWindow(Mussa *analysis_, QWidget *parent) :
34   QMainWindow(parent),
35   analysis(analysis_),
36   motif_editor(0),
37   setup_analysis_dialog(this),
38   browser(this),
39   mussaViewTB("Path Views"),
40   zoom(),
41   threshold(),
42   progress_dialog(0),
43   aboutAction(0),
44   closeAction(0),
45   createNewAnalysisAction(0),
46   createSubAnalysisAction(0),
47   editMotifsAction(0),
48   loadMotifListAction(0),
49   loadMupaAction(0),
50   loadSavedAnalysisAction(0),
51   mussaManualAssistantAction(0),
52   newMussaWindowAction(0),
53   saveMotifListAction(0),
54   showMussaViewToolbarAction(0),
55   toggleMotifsAction(0),
56   saveBrowserPixmapAction(0),
57   whatsThisAction(0),
58   viewMussaAlignmentAction(0),
59   manualAssistant(0)
60 {
61   setupActions();
62   setupMainMenu();
63   setupAssistant();
64
65   //This next setWhatsThis function prevents
66   // a segfault when using WhatsThis feature with 
67   // opengl widget.
68   //scene->setWhatsThis(tr("Mussa in OpenGL!"));
69   setCentralWidget(&browser);
70   // well updatePosition isn't quite right as we really just need
71   // to call update()
72   connect(this, SIGNAL(changedAnnotations()), &browser, SLOT(update()));
73
74   mussaViewTB.addAction(toggleMotifsAction);
75   mussaViewTB.addWidget(&zoom);
76   
77   connect(&zoom, SIGNAL(valueChanged(double)), 
78           &browser, SLOT(setZoom(double)));
79   
80   // threshold range is set in updateAnalysis
81   
82   //scene->setClipPlane(20);
83   // FIXME: for when we get the paths drawn at the appropriate depth
84   //connect(&threshold, SIGNAL(thresholdChanged(int)),
85   //        this, SLOT(setClipPlane(int)));
86   connect(&threshold, SIGNAL(thresholdChanged(int)),
87           this, SLOT(setSoftThreshold(int)));
88   mussaViewTB.addWidget(&threshold);
89
90   addToolBar(&mussaViewTB);
91
92   statusBar()->showMessage("Welcome to mussa", 2000);
93   connect(analysis, SIGNAL(progress(const std::string&, int, int)),
94           this, SLOT(updateProgress(const std::string&, int, int)));
95   updateAnalysis();
96 }
97
98 MussaWindow::~MussaWindow()
99 {
100   if (analysis != 0) delete analysis;
101   aligned_windows.clear();
102   if (motif_editor != 0) delete motif_editor;
103   if (progress_dialog != 0) delete progress_dialog;
104
105   if (aboutAction != 0) delete aboutAction;
106   if (closeAction != 0) delete closeAction;
107   if (createNewAnalysisAction != 0) delete createNewAnalysisAction;
108   if (createSubAnalysisAction != 0) delete createSubAnalysisAction;
109   if (editMotifsAction != 0) delete editMotifsAction;
110   if (loadMotifListAction != 0) delete loadMotifListAction;
111   if (loadMupaAction != 0) delete loadMupaAction;
112   if (loadSavedAnalysisAction != 0) delete loadSavedAnalysisAction;
113   if (mussaManualAssistantAction != 0) delete mussaManualAssistantAction;
114   if (newMussaWindowAction != 0) delete newMussaWindowAction;
115   if (saveBrowserPixmapAction != 0) delete saveBrowserPixmapAction;
116   if (saveMotifListAction != 0) delete saveMotifListAction;
117   if (showMussaViewToolbarAction != 0) delete showMussaViewToolbarAction;
118   if (toggleMotifsAction != 0) delete toggleMotifsAction;
119   if (whatsThisAction != 0) delete whatsThisAction;
120   if (viewMussaAlignmentAction != 0) delete viewMussaAlignmentAction;
121   
122   if (manualAssistant != 0) delete manualAssistant;
123   
124 }
125
126 void MussaWindow::setAnalysis(Mussa *new_analysis)
127 {
128   if (new_analysis != 0) {
129     // only switch mussas if we loaded without error
130     clear();
131     delete analysis;
132     analysis = new_analysis;
133     setWindowTitle(analysis->get_name().c_str());
134     updateAnalysis();
135   }
136 }
137
138 void MussaWindow::setupActions()
139 {
140   // we really don't want to run this more than once.
141   assert (closeAction == 0);
142
143   // the ever popular about box
144   aboutAction = new QAction(tr("&About"), this);
145   connect(aboutAction, SIGNAL(triggered()), this, SLOT(about()));
146   aboutAction->setIcon(QIcon(":/icons/info.png"));
147
148   // add exit
149   closeAction = new QAction(tr("&Close"), this);
150   closeAction->setStatusTip(tr("Close this window"));
151   connect(closeAction, SIGNAL(triggered()), this, SLOT(close()));
152   closeAction->setIcon(QIcon(":/icons/exit.png"));
153   
154   createNewAnalysisAction = new QAction(tr("Create Analysis"), this);
155   connect(createNewAnalysisAction, SIGNAL(triggered()), 
156           this, SLOT(createNewAnalysis()));
157   createNewAnalysisAction->setIcon(QIcon(":/icons/filenew.png"));
158   
159   createSubAnalysisAction = new QAction(tr("Add to Subanalysis"), this);
160   connect(createSubAnalysisAction, SIGNAL(triggered()), 
161           this, SLOT(createSubAnalysis()));
162
163   editMotifsAction = new QAction(tr("Edit Motifs"), this);;
164   connect(editMotifsAction, SIGNAL(triggered()), this, SLOT(editMotifs()));
165   
166   loadMotifListAction = new QAction(tr("Load Motif List"), this);
167   connect(loadMotifListAction, SIGNAL(triggered()), 
168           this, SLOT(loadMotifList()));
169   loadMotifListAction->setIcon(QIcon(":/icons/fileopen.png"));
170   
171   loadMupaAction = new QAction(tr("Create Analysis from File"), this);
172   connect(loadMupaAction, SIGNAL(triggered()), 
173           this, SLOT(loadMupa()));
174   loadMupaAction->setIcon(QIcon(":/icons/fileopen.png"));
175
176   loadSavedAnalysisAction = new QAction(tr("Load Existing &Analysis"), this);
177   connect(loadSavedAnalysisAction, SIGNAL(triggered()), 
178           this, SLOT(loadSavedAnalysis()));
179   loadSavedAnalysisAction->setIcon(QIcon(":/icons/fileopen.png"));
180
181   mussaManualAssistantAction = new QAction(tr("Mussagl Manual..."), this);
182   mussaManualAssistantAction->setIcon(QIcon(":/icons/contents.png"));
183   connect(mussaManualAssistantAction, SIGNAL(triggered()),
184           this, SLOT(showManual()));
185
186   newMussaWindowAction = new QAction(tr("&New Mussa Window"), this);
187   newMussaWindowAction->setStatusTip("open another mussa window to allow comparing results");
188   connect(newMussaWindowAction, SIGNAL(triggered()), 
189           this, SLOT(newMussaWindow()));
190   newMussaWindowAction->setIcon(QIcon(":/icons/window_new.png"));
191
192   saveMotifListAction = new QAction(tr("Save Motifs"), this);
193   connect(saveMotifListAction, SIGNAL(triggered()), 
194           this, SLOT(saveMotifList()));
195   saveMotifListAction->setIcon(QIcon(":/icons/filesave.png"));
196
197   showMussaViewToolbarAction = new QAction(tr("Show Toolbar"), this);
198   connect(showMussaViewToolbarAction, SIGNAL(triggered()), 
199           this, SLOT(showMussaToolbar()));
200   showMussaViewToolbarAction->setCheckable(true);
201   showMussaViewToolbarAction->setChecked(true);
202
203   toggleMotifsAction = new QAction(tr("Toggle Motifs"), this);
204   connect(toggleMotifsAction, SIGNAL(triggered()), 
205           this, SLOT(toggleMotifs()));
206   toggleMotifsAction->setCheckable(true);
207   toggleMotifsAction->setIcon(QIcon(":/icons/motif_icon.png"));
208   toggleMotifsAction->setWhatsThis(tr("Toggle motif annotations on/off\n\n"
209                                    "You can load motif annotations via "
210                                    "'File->Load Motif List' menu option."));
211
212   //Save pixel map action
213   saveBrowserPixmapAction = new QAction(tr("Save to image..."), this);
214   connect(saveBrowserPixmapAction, (SIGNAL(triggered())),
215           &browser, SLOT(promptSaveBrowserPixmap()));
216   saveBrowserPixmapAction->setIcon(QIcon(":/icons/image2.png"));
217
218   viewMussaAlignmentAction = new QAction(tr("View sequence alignment"), this);
219   connect(viewMussaAlignmentAction, SIGNAL(triggered()),
220           this, SLOT(viewMussaAlignment() ));
221   viewMussaAlignmentAction->setWhatsThis(tr("Create a zoomed in window "
222                                             "showing alignment of the seqcomp "
223                                             "defined paths"));
224
225   whatsThisAction = QWhatsThis::createAction(this);
226   whatsThisAction->setIcon(QIcon(":/icons/help.png"));
227
228
229 }
230
231 void MussaWindow::setupMainMenu()
232 {
233   // we need to run setupActions first
234   assert (closeAction != 0);
235
236   QMenu *newMenu = menuBar()->addMenu(tr("&File"));
237   
238   newMenu->addAction(newMussaWindowAction);
239   newMenu->addAction(createNewAnalysisAction);
240   newMenu->addAction(loadMupaAction);
241   newMenu->addAction(loadSavedAnalysisAction);
242   newMenu->addSeparator();
243   newMenu->addAction(loadMotifListAction);
244   newMenu->addAction(saveMotifListAction);
245   newMenu->addSeparator();
246   newMenu->addAction(saveBrowserPixmapAction);
247   newMenu->addSeparator();
248   newMenu->addAction(closeAction);
249
250   newMenu = menuBar()->addMenu(tr("&Edit"));
251   newMenu->addAction(editMotifsAction);
252   newMenu->addAction(&browser.getCopySelectedSequenceAsFastaAction());
253   newMenu->addAction(createSubAnalysisAction);
254  
255   newMenu = menuBar()->addMenu(tr("&View"));
256   newMenu->addAction(viewMussaAlignmentAction);
257   newMenu->addAction(showMussaViewToolbarAction);
258
259   newMenu = menuBar()->addMenu(tr("&Help"));
260 #if defined(QT_ASSISTANT_LIB)
261   newMenu->addAction(mussaManualAssistantAction);
262 #endif
263   newMenu->addAction(whatsThisAction);
264   newMenu->addSeparator();
265   newMenu->addAction(aboutAction);
266
267   // add some extra features to the context menu
268   QMenu& popupMenu = browser.getPopupMenu();
269   popupMenu.addAction(viewMussaAlignmentAction);
270   popupMenu.addAction(createSubAnalysisAction);
271 }
272
273 void MussaWindow::setupAssistant()
274 {
275 #if 0 && defined(Q_WS_MAC)
276     CFURLRef pluginRef = CFBundleCopyBundleURL(CFBundleGetMainBundle());
277     CFStringRef macPath = CFURLCopyFileSystemPath(pluginRef,
278                                            kCFURLPOSIXPathStyle);
279     const char *pathPtr = CFStringGetCStringPtr(macPath,
280                                            CFStringGetSystemEncoding());
281     qDebug("Path = %s", pathPtr);
282     CFRelease(pluginRef);
283     CFRelease(macPath);
284 #endif
285 #if defined(QT_ASSISTANT_LIB)
286   QStringList manualAssistantArgs;
287   manualAssistantArgs = QStringList();
288   manualAssistantArgs << "-profile" << "./doc/manual/mussagl_manual.adp";
289   manualAssistant = new QAssistantClient("assistant", this);
290   manualAssistant->setArguments(manualAssistantArgs);
291   connect(manualAssistant, SIGNAL(error(QString)),
292           this, SLOT(assistantError(QString)));
293 #endif
294 }
295   
296 void MussaWindow::about()
297 {
298   QString msg("Welcome to Multiple Species Sequence Analysis\n"
299               "(c) 2005-2006 California Institute of Technology\n"
300               "Tristan De Buysscher, Diane Trout\n");
301    msg += "\n\r";
302    msg += "Path: ";
303    msg += QDir::currentPath();
304    msg += "\n";
305    msg += "OpenGL: ";
306    msg += (char *)glGetString(GL_VERSION);
307    msg += "\n";
308    QMessageBox::about(this, tr("About mussa"), msg);
309 }
310
311 void MussaWindow::clear()
312 {
313   aligned_windows.clear();
314   browser.clear();
315 }
316
317 void MussaWindow::createNewAnalysis()
318 {
319   try {
320     if (setup_analysis_dialog.exec()) {
321       Mussa *m = 0;
322       m = setup_analysis_dialog.getMussa();
323       setAnalysis(m);
324     } else {
325       std::cout << "New mussa exp. aborted!\n";
326     }
327   } catch(mussa_error e) {
328     QString msg(e.what());
329     QMessageBox::warning(this, tr("Create New Analysis"), msg);
330   }
331 }
332
333 void MussaWindow::createSubAnalysis()
334 {
335   list<SequenceLocation> result;
336   SequenceLocationModel& model = subanalysis_window.getModel();
337   browser.copySelectedTracksAsSeqLocation(result);
338   for(list<SequenceLocation>::iterator result_itor = result.begin();
339       result_itor != result.end();
340       ++result_itor)
341   {
342     model.push_back(*result_itor);
343   }
344
345   if (not subanalysis_window.isVisible()) {
346     subanalysis_window.show();
347   }
348 }
349
350 void MussaWindow::editMotifs()
351 {
352   if (motif_editor != 0) {
353     motif_editor->hide();
354     delete motif_editor;
355   }
356   motif_editor = new MotifEditor(analysis);
357   connect(motif_editor, SIGNAL(changedMotifs()), 
358           this, SLOT(updateAnnotations()));
359   motif_editor->show();
360 }
361
362 void MussaWindow::loadMotifList()
363 {
364   QString caption("Load a motif list");
365   QString filter("Motif list(*.txt *.mtl)");
366   QString path = QFileDialog::getOpenFileName(this,
367                                                    caption, 
368                                                    QDir::currentPath(),
369                                                    filter);
370   // user hit cancel?
371   if (path.isNull()) 
372     return;
373   // try to load safely
374   try {
375     fs::path converted_path(path.toStdString(), fs::native);
376     analysis->load_motifs(converted_path);
377   } catch (runtime_error e) {
378     QString msg("Unable to load ");
379     msg += path;
380     msg += "\n";
381     msg += e.what();
382     QMessageBox::warning(this, "Load Motifs", msg);
383   }
384   assert (analysis != 0);
385 }
386
387 void MussaWindow::saveMotifList()
388 {
389   NotImplementedBox();
390 }
391
392 void MussaWindow::loadMupa()
393 {
394   QString caption("Load a mussa parameter file");
395   QString filter("Mussa Parameters (*.mupa)");
396   QString mupa_path = QFileDialog::getOpenFileName(this,
397                                                    caption, 
398                                                    QDir::currentPath(),
399                                                    filter);
400   // user hit cancel?
401   if (mupa_path.isNull()) 
402     return;
403   // try to load safely
404   try {
405     Mussa *m = new Mussa;
406     fs::path converted_path(mupa_path.toStdString(), fs::native);
407     connect(m, SIGNAL(progress(const std::string&, int, int)),
408             this, SLOT(updateProgress(const std::string&, int, int)));
409     m->load_mupa_file(converted_path);
410     m->analyze();
411     setAnalysis(m);
412     setWindowTitle(converted_path.native_file_string().c_str());
413   } catch (mussa_load_error e) {
414     QString msg("Unable to load ");
415     msg += mupa_path;
416     msg += "\n";
417     msg += e.what();
418     QMessageBox::warning(this, "Load Parameter", msg);
419   }
420   assert (analysis != 0);
421 }
422
423 void MussaWindow::loadSavedAnalysis()
424 {
425   QString caption("Load a previously run analysis");
426   QString muway_dir = QFileDialog::getExistingDirectory(this,
427                                                         caption, 
428                                                         QDir::currentPath());
429   // user hit cancel?
430   if (muway_dir.isNull()) 
431     return;
432   // try to safely load
433   try {
434     Mussa *m = new Mussa;
435     fs::path converted_path(muway_dir.toStdString(), fs::native);
436     connect(m, SIGNAL(progress(const std::string&, int, int)),
437             this, SLOT(updateProgress(const std::string&, int, int)));
438     m->load(converted_path);
439     // only switch mussas if we loaded without error
440     setAnalysis(m);
441     setWindowTitle(converted_path.native_file_string().c_str());
442   } catch (mussa_load_error e) {
443     QString msg("Unable to load ");
444     msg += muway_dir;
445     msg += "\n";
446     msg += e.what();
447     QMessageBox::warning(this, "Load Parameter", msg);
448   }
449   assert (analysis != 0);
450 }
451
452 void MussaWindow::newMussaWindow()
453 {
454   Mussa *a = new Mussa();
455   MussaWindow *win = new MussaWindow(a);
456   win->show();
457 }
458
459 void MussaWindow::setSoftThreshold(int threshold)
460 {
461   if (analysis->get_soft_threshold() != threshold) {
462     analysis->set_soft_threshold(threshold);
463     analysis->nway();
464     updateLinks();
465     update();
466   }
467 }
468
469 void MussaWindow::showMussaToolbar()
470 {
471   if (mussaViewTB.isVisible())
472     mussaViewTB.hide();
473   else
474     mussaViewTB.show();
475 }
476
477 void MussaWindow::toggleMotifs()
478 {
479   NotImplementedBox();
480 }
481
482 void MussaWindow::showManual()
483 {
484   manualAssistant->openAssistant();
485 }
486
487 void MussaWindow::assistantError(QString message)
488 {
489   //std::cout << "QAssistantError: " << message.toStdString() << "\n";
490   QMessageBox::warning ( this, "Warning: Mussagl Manual", message, 
491                          QMessageBox::Ok, 
492                          QMessageBox::NoButton);
493 }
494
495 void MussaWindow::NotImplementedBox()
496 {
497   QMessageBox::warning(this, QObject::tr("mussa"), QObject::tr("Not implemented yet"));
498 }      
499
500 void MussaWindow::viewMussaAlignment()
501 {
502   const set<int>& selected_paths = browser.selectedPaths();
503   if (selected_paths.size() == 0 ) {
504     QMessageBox::warning(this, 
505                          QObject::tr("mussa"),
506                          QObject::tr("you should probably select some paths "
507                                      "first"));
508   } else {
509     boost::shared_ptr<MussaAlignedWindow> ma_win( 
510       new MussaAlignedWindow(*analysis, selected_paths)
511     );
512
513     aligned_windows.push_back(ma_win);
514     connect(this, SIGNAL(changedAnnotations()), 
515             aligned_windows.back().get(), SLOT(update()));
516     aligned_windows.back()->show();
517   }
518 }
519                         
520 void MussaWindow::updateAnalysis()
521 {
522   threshold.setRange(analysis->get_threshold(),analysis->get_window());
523
524   const Mussa::vector_sequence_type& seqs = analysis->sequences();
525   browser.setSequences(seqs, analysis->colorMapper());
526   updateLinks();
527   browser.zoomOut();
528   zoom.setValue(browser.zoom());
529 }
530
531 void MussaWindow::updateAnnotations()
532 {
533   // motifs were changed in the sequences by 
534   // Mussa::update_sequences_motifs
535   emit changedAnnotations();
536   browser.update();
537 }
538
539 void MussaWindow::updateLinks()
540 {
541   browser.clear_links();
542   bool reversed = false;
543   const NwayPaths& nway = analysis->paths();
544
545   typedef list<ConservedPath> conserved_paths;
546   typedef conserved_paths::const_iterator const_conserved_paths_itor;
547   for(const_conserved_paths_itor path_itor = nway.refined_pathz.begin();
548       path_itor != nway.refined_pathz.end();
549       ++path_itor)
550   {
551     // since we were drawing to the start of a window, and opengl lines
552     // are centered around the two connecting points our lines were slightly
553     // offset. the idea of window_offset is to adjust them to the
554     // right for forward compliment or left for reverse compliment
555     // FIXME: figure out how to unit test these computations
556     //GLfloat window_offset = (path_itor->window_size)/2.0;
557
558     size_t track_len = path_itor->track_indexes.size();
559     vector<int> normalized_path;
560     normalized_path.reserve(track_len);
561     vector<bool> rc_flags(false, track_len);
562     for (size_t track_i=0; track_i != track_len; ++track_i)
563     {
564       int x = path_itor->track_indexes[track_i];
565       // at some point when we modify the pathz data structure to keep
566       // track of the score we can put grab the depth here.
567       //
568       // are we reverse complimented?
569       if ( x>=0) {
570         reversed = false;
571       } else {
572         reversed = true;
573         // make positive 
574         x = -x;
575       }
576       normalized_path.push_back(x);
577       rc_flags.push_back(reversed);
578     }
579     browser.link(normalized_path, rc_flags, path_itor->window_size);
580   }
581   browser.update();
582 }
583
584 void 
585 MussaWindow::updateProgress(const string& description, int current, int max)
586 {  
587   // if we're done  
588   if (current == max) {
589     if (progress_dialog != 0) {
590       progress_dialog->hide();
591       delete progress_dialog;
592       progress_dialog = 0;
593     }
594   } else {
595     // if we're starting, create the dialog
596     if (progress_dialog == 0) {
597       QString desc(description.c_str());
598       QString cancel("Cancel");
599       progress_dialog = new QProgressDialog(desc, cancel, current, max, this);
600       progress_dialog->show();
601     } else {
602       // just update the dialog
603       progress_dialog->setValue(current);
604     }
605   }
606   qApp->processEvents();
607 }