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