#include #include #include #include #include #include #include #include #include #include #include "config.h" #include "icons.h" #include "keys.h" #include "gpx.h" #include "map.h" #include "maplist.h" #include "elevationgraph.h" #include "speedgraph.h" #include "track.h" #include "infoitem.h" #include "filebrowser.h" #include "gui.h" static QString timeSpan(qreal time) { unsigned h, m, s; h = time / 3600; m = (time - (h * 3600)) / 60; s = time - (h * 3600) - (m * 60); return QString("%1:%2:%3").arg(h).arg(m, 2, 10, QChar('0')) .arg(s, 2, 10, QChar('0')); } GUI::GUI() { loadFiles(); createActions(); createMenus(); createToolBars(); createTrackView(); createTrackGraphs(); createStatusBar(); connect(_elevationGraph, SIGNAL(sliderPositionChanged(qreal)), _track, SLOT(movePositionMarker(qreal))); connect(_speedGraph, SIGNAL(sliderPositionChanged(qreal)), _track, SLOT(movePositionMarker(qreal))); _browser = new FileBrowser(this); _browser->setFilter(QStringList("*.gpx")); QVBoxLayout *layout = new QVBoxLayout; layout->addWidget(_track); layout->addWidget(_trackGraphs); QWidget *widget = new QWidget; widget->setLayout(layout); setCentralWidget(widget); setWindowTitle(APP_NAME); setUnifiedTitleAndToolBarOnMac(true); _distance = 0; _time = 0; _trackCount = 0; resize(600, 800); } void GUI::loadFiles() { // Maps _maps = MapList::load(QString("%1/" MAP_LIST_FILE).arg(QDir::homePath())); _maps += MapList::load(":/maps.txt"); // POI files QDir dir(QString("%1/" POI_DIR).arg(QDir::homePath())); QFileInfoList list = dir.entryInfoList(QStringList(), QDir::Files); for (int i = 0; i < list.size(); ++i) { if (!_poi.loadFile(list.at(i).absoluteFilePath())) fprintf(stderr, "Error loading POI file: %s: %s", qPrintable(list.at(i).absoluteFilePath()), qPrintable(_poi.errorString())); } } void GUI::createMapActions() { QActionGroup *ag = new QActionGroup(this); ag->setExclusive(true); QSignalMapper *sm = new QSignalMapper(this); for (int i = 0; i < _maps.count(); i++) { QAction *a = new QAction(_maps.at(i)->name(), this); a->setCheckable(true); a->setActionGroup(ag); sm->setMapping(a, i); connect(a, SIGNAL(triggered()), sm, SLOT(map())); _mapActions.append(a); } connect(sm, SIGNAL(mapped(int)), this, SLOT(mapChanged(int))); _mapActions.at(0)->setChecked(true); _currentMap = _maps.at(0); } void GUI::createActions() { // Action Groups _fileActionGroup = new QActionGroup(this); _fileActionGroup->setExclusive(false); _fileActionGroup->setEnabled(false); _navigationActionGroup = new QActionGroup(this); _navigationActionGroup->setEnabled(false); // General actions _exitAction = new QAction(QIcon(QPixmap(QUIT_ICON)), tr("Quit"), this); _exitAction->setShortcut(QKeySequence::Quit); connect(_exitAction, SIGNAL(triggered()), this, SLOT(close())); // Help & About _dataSourcesAction = new QAction(tr("Data sources"), this); connect(_dataSourcesAction, SIGNAL(triggered()), this, SLOT(dataSources())); _keysAction = new QAction(tr("Keyboard controls"), this); connect(_keysAction, SIGNAL(triggered()), this, SLOT(keys())); _aboutAction = new QAction(QIcon(QPixmap(APP_ICON)), tr("About GPXSee"), this); connect(_aboutAction, SIGNAL(triggered()), this, SLOT(about())); _aboutQtAction = new QAction(QIcon(QPixmap(QT_ICON)), tr("About Qt"), this); connect(_aboutQtAction, SIGNAL(triggered()), qApp, SLOT(aboutQt())); // File related actions _openFileAction = new QAction(QIcon(QPixmap(OPEN_FILE_ICON)), tr("Open"), this); _openFileAction->setShortcut(QKeySequence::Open); connect(_openFileAction, SIGNAL(triggered()), this, SLOT(openFile())); _saveFileAction = new QAction(QIcon(QPixmap(SAVE_FILE_ICON)), tr("Save"), this); _saveFileAction->setShortcut(QKeySequence::Save); _saveFileAction->setActionGroup(_fileActionGroup); connect(_saveFileAction, SIGNAL(triggered()), this, SLOT(saveFile())); _saveAsAction = new QAction(QIcon(QPixmap(SAVE_AS_ICON)), tr("Save as"), this); _saveAsAction->setShortcut(QKeySequence::SaveAs); _saveAsAction->setActionGroup(_fileActionGroup); connect(_saveAsAction, SIGNAL(triggered()), this, SLOT(saveAs())); _closeFileAction = new QAction(QIcon(QPixmap(CLOSE_FILE_ICON)), tr("Close"), this); _closeFileAction->setShortcut(QKeySequence::Close); _closeFileAction->setActionGroup(_fileActionGroup); connect(_closeFileAction, SIGNAL(triggered()), this, SLOT(closeFile())); _reloadFileAction = new QAction(QIcon(QPixmap(RELOAD_FILE_ICON)), tr("Reload"), this); _reloadFileAction->setShortcut(QKeySequence::Refresh); _reloadFileAction->setActionGroup(_fileActionGroup); connect(_reloadFileAction, SIGNAL(triggered()), this, SLOT(reloadFile())); // POI actions _openPOIAction = new QAction(QIcon(QPixmap(OPEN_FILE_ICON)), tr("Load POI file"), this); connect(_openPOIAction, SIGNAL(triggered()), this, SLOT(openPOIFile())); _showPOIAction = new QAction(QIcon(QPixmap(SHOW_POI_ICON)), tr("Show POIs"), this); _showPOIAction->setCheckable(true); _showPOIAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_P)); connect(_showPOIAction, SIGNAL(triggered(bool)), this, SLOT(showPOI(bool))); // Map actions _showMapAction = new QAction(QIcon(QPixmap(SHOW_MAP_ICON)), tr("Show map"), this); _showMapAction->setCheckable(true); _showMapAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_M)); connect(_showMapAction, SIGNAL(triggered(bool)), this, SLOT(showMap(bool))); if (_maps.empty()) _showMapAction->setEnabled(false); else { createMapActions(); _showMapAction->setChecked(true); } // Settings actions _showGraphsAction = new QAction(tr("Show graphs"), this); _showGraphsAction->setCheckable(true); _showGraphsAction->setChecked(true); _showGraphsAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_G)); connect(_showGraphsAction, SIGNAL(triggered(bool)), this, SLOT(showGraphs(bool))); _showToolbarsAction = new QAction(tr("Show toolbars"), this); _showToolbarsAction->setCheckable(true); _showToolbarsAction->setChecked(true); connect(_showToolbarsAction, SIGNAL(triggered(bool)), this, SLOT(showToolbars(bool))); QActionGroup *ag = new QActionGroup(this); ag->setExclusive(true); _metricUnitsAction = new QAction(tr("Metric"), this); _metricUnitsAction->setCheckable(true); _metricUnitsAction->setActionGroup(ag); _metricUnitsAction->setChecked(true); connect(_metricUnitsAction, SIGNAL(triggered()), this, SLOT(setMetricUnits())); _imperialUnitsAction = new QAction(tr("Imperial"), this); _imperialUnitsAction->setCheckable(true); _imperialUnitsAction->setActionGroup(ag); connect(_imperialUnitsAction, SIGNAL(triggered()), this, SLOT(setImperialUnits())); // Navigation actions _nextAction = new QAction(QIcon(QPixmap(NEXT_FILE_ICON)), tr("Next"), this); _nextAction->setActionGroup(_navigationActionGroup); connect(_nextAction, SIGNAL(triggered()), this, SLOT(next())); _prevAction = new QAction(QIcon(QPixmap(PREV_FILE_ICON)), tr("Previous"), this); _prevAction->setActionGroup(_navigationActionGroup); connect(_prevAction, SIGNAL(triggered()), this, SLOT(prev())); _lastAction = new QAction(QIcon(QPixmap(LAST_FILE_ICON)), tr("Last"), this); _lastAction->setActionGroup(_navigationActionGroup); connect(_lastAction, SIGNAL(triggered()), this, SLOT(last())); _firstAction = new QAction(QIcon(QPixmap(FIRST_FILE_ICON)), tr("First"), this); _firstAction->setActionGroup(_navigationActionGroup); connect(_firstAction, SIGNAL(triggered()), this, SLOT(first())); } void GUI::createMenus() { _fileMenu = menuBar()->addMenu(tr("File")); _fileMenu->addAction(_openFileAction); _fileMenu->addSeparator(); _fileMenu->addAction(_saveFileAction); _fileMenu->addAction(_saveAsAction); _fileMenu->addSeparator(); _fileMenu->addAction(_reloadFileAction); _fileMenu->addSeparator(); _fileMenu->addAction(_closeFileAction); #ifndef Q_OS_MAC _fileMenu->addSeparator(); _fileMenu->addAction(_exitAction); #endif // Q_OS_MAC _mapMenu = menuBar()->addMenu(tr("Map")); _mapMenu->addActions(_mapActions); _mapMenu->addSeparator(); _mapMenu->addAction(_showMapAction); _poiMenu = menuBar()->addMenu(tr("POI")); _poiMenu->addAction(_openPOIAction); _poiMenu->addAction(_showPOIAction); _settingsMenu = menuBar()->addMenu(tr("Settings")); _unitsMenu = _settingsMenu->addMenu(tr("Units")); _unitsMenu->addAction(_metricUnitsAction); _unitsMenu->addAction(_imperialUnitsAction); _settingsMenu->addSeparator(); _settingsMenu->addAction(_showToolbarsAction); _settingsMenu->addAction(_showGraphsAction); _helpMenu = menuBar()->addMenu(tr("Help")); _helpMenu->addAction(_dataSourcesAction); _helpMenu->addAction(_keysAction); _helpMenu->addSeparator(); _helpMenu->addAction(_aboutAction); _helpMenu->addAction(_aboutQtAction); } void GUI::createToolBars() { _fileToolBar = addToolBar(tr("File")); _fileToolBar->addAction(_openFileAction); _fileToolBar->addAction(_saveFileAction); _fileToolBar->addAction(_reloadFileAction); _fileToolBar->addAction(_closeFileAction); #ifdef Q_OS_MAC _fileToolBar->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); #endif // Q_OS_MAC _showToolBar = addToolBar(tr("Show")); _showToolBar->addAction(_showPOIAction); _showToolBar->addAction(_showMapAction); #ifdef Q_OS_MAC _showToolBar->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); #endif // Q_OS_MAC _navigationToolBar = addToolBar(tr("Navigation")); _navigationToolBar->addAction(_firstAction); _navigationToolBar->addAction(_prevAction); _navigationToolBar->addAction(_nextAction); _navigationToolBar->addAction(_lastAction); #ifdef Q_OS_MAC _navigationToolBar->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); #endif // Q_OS_MAC } void GUI::createTrackView() { _track = new Track(this); if (_showMapAction->isChecked()) _track->setMap(_currentMap); } void GUI::createTrackGraphs() { _elevationGraph = new ElevationGraph; _speedGraph = new SpeedGraph; _trackGraphs = new QTabWidget; _trackGraphs->addTab(_elevationGraph, tr("Elevation")); _trackGraphs->addTab(_speedGraph, tr("Speed")); connect(_trackGraphs, SIGNAL(currentChanged(int)), this, SLOT(graphChanged(int))); _trackGraphs->setFixedHeight(200); _trackGraphs->setSizePolicy( QSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed)); } void GUI::createStatusBar() { _fileNameLabel = new QLabel(); _distanceLabel = new QLabel(); _timeLabel = new QLabel(); _distanceLabel->setAlignment(Qt::AlignHCenter); _timeLabel->setAlignment(Qt::AlignHCenter); statusBar()->addPermanentWidget(_fileNameLabel, 8); statusBar()->addPermanentWidget(_distanceLabel, 1); statusBar()->addPermanentWidget(_timeLabel, 1); statusBar()->setSizeGripEnabled(false); } void GUI::about() { QMessageBox msgBox(this); msgBox.setWindowTitle(tr("About GPXSee")); msgBox.setText(QString("

") + QString(APP_NAME " " APP_VERSION) + QString("

") + tr("GPX viewer and analyzer") + QString("

")); msgBox.setInformativeText(QString("
") + tr("GPXSee is distributed under the terms of the GNU General Public " "License version 3. For more info about GPXSee visit the project " "homepage at ") + QString("" APP_HOMEPAGE ".
")); QIcon icon = msgBox.windowIcon(); QSize size = icon.actualSize(QSize(64, 64)); msgBox.setIconPixmap(icon.pixmap(size)); msgBox.exec(); } void GUI::keys() { QMessageBox msgBox(this); msgBox.setWindowTitle(tr("Keyboard controls")); msgBox.setText(QString("

") + tr("Keyboard controls") + QString("

")); msgBox.setInformativeText( QString("
" "
") + tr("Next file") + QString("SPACE
") + tr("Previous file") + QString("BACKSPACE
") + tr("First file") + QString("HOME
") + tr("Last file") + QString("END
") + tr("Append modifier") + QString("SHIFT
")); msgBox.exec(); } void GUI::dataSources() { QMessageBox msgBox(this); msgBox.setWindowTitle(tr("Data sources")); msgBox.setText(QString("

") + tr("Data sources") + QString("

")); msgBox.setInformativeText( QString("

") + tr("Map sources") + QString("

") + tr("Map (tiles) source URLs are read on program startup from the " "following file:") + QString("

") + QDir::homePath() + QString("/" MAP_LIST_FILE "

") + tr("The file format is one map entry per line, consisting of the map " "name and tiles URL delimited by a TAB character. The tile X and Y " "coordinates are replaced with $x and $y in the URL and the zoom " "level is replaced with $z. An example map file could look like:") + QString("

Map1 http://tile.server.com/map/$z/$x/$y.png" "
Map2 http://mapserver.org/map/$z-$x-$y

") + QString("

") + tr("POIs") + QString("

") + tr("To make GPXSee load a POI file automatically on startup, add " "the file to the following directory:") + QString("

") + QDir::homePath() + QString("/" POI_DIR "

") ); msgBox.exec(); } void GUI::openFile() { QStringList files = QFileDialog::getOpenFileNames(this, tr("Open file")); QStringList list = files; for (QStringList::Iterator it = list.begin(); it != list.end(); it++) openFile(*it); } bool GUI::openFile(const QString &fileName) { if (fileName.isEmpty() || _files.contains(fileName)) return false; if (loadFile(fileName)) { _files.append(fileName); _browser->setCurrent(fileName); updateStatusBarInfo(); _fileActionGroup->setEnabled(true); _navigationActionGroup->setEnabled(true); updateNavigationActions(); return true; } else { updateNavigationActions(); return false; } } bool GUI::loadFile(const QString &fileName) { GPX gpx; if (gpx.loadFile(fileName)) { _elevationGraph->loadGPX(gpx); _speedGraph->loadGPX(gpx); _track->loadGPX(gpx); if (_showPOIAction->isChecked()) _track->loadPOI(_poi); for (int i = 0; i < gpx.count(); i++) { _distance += gpx.distance(i); _time += gpx.time(i); } _trackCount += gpx.count(); return true; } else { QString error = fileName + QString("\n\n") + tr("Error loading GPX file:\n%1").arg(gpx.errorString()) + QString("\n"); if (gpx.errorLine()) error.append(tr("Line: %1").arg(gpx.errorLine())); QMessageBox::critical(this, tr("Error"), error); return false; } } void GUI::openPOIFile() { QString fileName = QFileDialog::getOpenFileName(this, tr("Open POI file")); if (!fileName.isEmpty()) { if (!_poi.loadFile(fileName)) { QString error = tr("Error loading POI file:\n%1") .arg(_poi.errorString()) + QString("\n"); if (_poi.errorLine()) error.append(tr("Line: %1").arg(_poi.errorLine())); QMessageBox::critical(this, tr("Error"), error); } else { _showPOIAction->setChecked(true); _track->loadPOI(_poi); } } } void GUI::saveAs() { QString fileName = QFileDialog::getSaveFileName(this, "Export to PDF", QString(), "*.pdf"); if (!fileName.isEmpty()) { saveFile(fileName); _saveFileName = fileName; } } void GUI::saveFile() { if (_saveFileName.isEmpty()) emit saveAs(); else saveFile(_saveFileName); } void GUI::saveFile(const QString &fileName) { QPrinter printer(QPrinter::HighResolution); printer.setPageSize(QPrinter::A4); printer.setOrientation(_track->orientation()); printer.setOutputFormat(QPrinter::PdfFormat); printer.setOutputFileName(fileName); QPainter p(&printer); _track->plot(&p, QRectF(0, 300, printer.width(), (0.80 * printer.height()) - 400)); _elevationGraph->plot(&p, QRectF(0, 0.80 * printer.height(), printer.width(), printer.height() * 0.20)); QGraphicsScene scene; InfoItem info; if (_imperialUnitsAction->isChecked()) { info.insert(tr("Distance"), QString::number(_distance * M2MI, 'f', 1) + THIN_SPACE + tr("mi")); info.insert(tr("Time"), timeSpan(_time)); info.insert(tr("Ascent"), QString::number(_elevationGraph->ascent() * M2FT, 'f', 0) + THIN_SPACE + tr("ft")); info.insert(tr("Descent"), QString::number(_elevationGraph->descent() * M2FT, 'f', 0) + THIN_SPACE + tr("ft")); info.insert(tr("Maximum"), QString::number(_elevationGraph->max() * M2FT, 'f', 0) + THIN_SPACE + tr("ft")); info.insert(tr("Minimum"), QString::number(_elevationGraph->min() * M2FT, 'f', 0) + THIN_SPACE + tr("ft")); } else { info.insert(tr("Distance"), QString::number(_distance * M2KM, 'f', 1) + THIN_SPACE + tr("km")); info.insert(tr("Time"), timeSpan(_time)); info.insert(tr("Ascent"), QString::number(_elevationGraph->ascent(), 'f', 0) + THIN_SPACE + tr("m")); info.insert(tr("Descent"), QString::number(_elevationGraph->descent(), 'f', 0) + THIN_SPACE + tr("m")); info.insert(tr("Maximum"), QString::number(_elevationGraph->max(), 'f', 0) + THIN_SPACE + tr("m")); info.insert(tr("Minimum"), QString::number(_elevationGraph->min(), 'f', 0) + THIN_SPACE + tr("m")); } scene.addItem(&info); scene.render(&p, QRectF(0, 0, printer.width(), 200)); p.end(); } void GUI::reloadFile() { _distance = 0; _time = 0; _elevationGraph->clear(); _speedGraph->clear(); _track->clear(); for (int i = 0; i < _files.size(); i++) { if (!loadFile(_files.at(i))) { _files.removeAt(i); i--; } } updateStatusBarInfo(); if (_files.isEmpty()) _fileActionGroup->setEnabled(false); else _browser->setCurrent(_files.last()); } void GUI::closeFile() { _distance = 0; _time = 0; _trackCount = 0; _elevationGraph->clear(); _speedGraph->clear(); _track->clear(); _files.clear(); _fileActionGroup->setEnabled(false); updateStatusBarInfo(); } void GUI::showPOI(bool checked) { if (checked) _track->loadPOI(_poi); else _track->clearPOI(); } void GUI::showMap(bool checked) { if (checked) _track->setMap(_currentMap); else _track->setMap(0); } void GUI::showGraphs(bool checked) { _trackGraphs->setHidden(!checked); } void GUI::showToolbars(bool checked) { if (checked) { addToolBar(_fileToolBar); addToolBar(_showToolBar); addToolBar(_navigationToolBar); _fileToolBar->show(); _showToolBar->show(); _navigationToolBar->show(); } else { removeToolBar(_fileToolBar); removeToolBar(_showToolBar); removeToolBar(_navigationToolBar); } } void GUI::updateStatusBarInfo() { if (_files.count() == 0) { _fileNameLabel->clear(); _distanceLabel->clear(); _timeLabel->clear(); return; } else if (_files.count() == 1) _fileNameLabel->setText(_files.at(0)); else _fileNameLabel->setText(tr("%1 tracks").arg(_trackCount)); if (_imperialUnitsAction->isChecked()) _distanceLabel->setText(QString::number(_distance * M2MI, 'f', 1) + " " + tr("mi")); else _distanceLabel->setText(QString::number(_distance * M2KM, 'f', 1) + " " + tr("km")); _timeLabel->setText(timeSpan(_time)); } void GUI::mapChanged(int index) { _currentMap = _maps.at(index); if (_showMapAction->isChecked()) _track->setMap(_currentMap); } void GUI::graphChanged(int index) { if (_trackGraphs->widget(index) == _elevationGraph) _elevationGraph->setSliderPosition(_speedGraph->sliderPosition()); else if (_trackGraphs->widget(index) == _speedGraph) _speedGraph->setSliderPosition(_elevationGraph->sliderPosition()); } void GUI::updateNavigationActions() { if (_browser->isLast()) { _nextAction->setEnabled(false); _lastAction->setEnabled(false); } else { _nextAction->setEnabled(true); _lastAction->setEnabled(true); } if (_browser->isFirst()) { _prevAction->setEnabled(false); _firstAction->setEnabled(false); } else { _prevAction->setEnabled(true); _firstAction->setEnabled(true); } } void GUI::setMetricUnits() { _track->setUnits(Metric); _elevationGraph->setUnits(Metric); _speedGraph->setUnits(Metric); updateStatusBarInfo(); } void GUI::setImperialUnits() { _track->setUnits(Imperial); _elevationGraph->setUnits(Imperial); _speedGraph->setUnits(Imperial); updateStatusBarInfo(); } void GUI::next() { QString file = _browser->next(); if (file.isNull()) return; closeFile(); openFile(file); } void GUI::prev() { QString file = _browser->prev(); if (file.isNull()) return; closeFile(); openFile(file); } void GUI::last() { QString file = _browser->last(); if (file.isNull()) return; closeFile(); openFile(file); } void GUI::first() { QString file = _browser->first(); if (file.isNull()) return; closeFile(); openFile(file); } void GUI::keyPressEvent(QKeyEvent *event) { QString file; switch (event->key()) { case PREV_KEY: file = _browser->prev(); break; case NEXT_KEY: file = _browser->next(); break; case FIRST_KEY: file = _browser->first(); break; case LAST_KEY: file = _browser->last(); break; } if (!file.isNull()) { if (!(event->modifiers() & MODIFIER)) closeFile(); openFile(file); } }