From 5c8beee46dab22484719b5c6abd3ac95f04e8cdc Mon Sep 17 00:00:00 2001 From: Steven Gribble Date: Sun, 8 Jan 2012 08:53:46 -0800 Subject: [PATCH] added a new air density (rho) estimator dialog box, hooked it into the Tools menu, and updated src.pro to add compilation dependencies for it --- src/MainWindow.cpp | 8 ++ src/MainWindow.h | 1 + src/ToolsRhoEstimator.cpp | 288 ++++++++++++++++++++++++++++++++++++++ src/ToolsRhoEstimator.h | 60 ++++++++ src/src.pro | 2 + 5 files changed, 359 insertions(+) create mode 100644 src/ToolsRhoEstimator.cpp create mode 100644 src/ToolsRhoEstimator.h diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 0e7f53465..d0c98e5d9 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -50,6 +50,7 @@ #include "RideCalendar.h" #include "DatePickerDialog.h" #include "ToolsDialog.h" +#include "ToolsRhoEstimator.h" #include "MetricAggregator.h" #include "SplitActivityWizard.h" #include "BatchExportDialog.h" @@ -610,6 +611,7 @@ MainWindow::MainWindow(const QDir &home) : QMenu *optionsMenu = menuBar()->addMenu(tr("&Tools")); optionsMenu->addAction(tr("&Options..."), this, SLOT(showOptions()), tr("Ctrl+O")); optionsMenu->addAction(tr("Critical Power Calculator..."), this, SLOT(showTools())); + optionsMenu->addAction(tr("Air Density (Rho) Estimator..."), this, SLOT(showRhoEstimator())); optionsMenu->addSeparator(); optionsMenu->addAction(tr("Workout Wizard"), this, SLOT(showWorkoutWizard())); @@ -1075,6 +1077,12 @@ void MainWindow::showTools() td->show(); } +void MainWindow::showRhoEstimator() +{ + ToolsRhoEstimator *tre = new ToolsRhoEstimator(); + tre->show(); +} + void MainWindow::showWorkoutWizard() { WorkoutWizard *ww = new WorkoutWizard(this); diff --git a/src/MainWindow.h b/src/MainWindow.h index 7db264fa0..3a2f9beeb 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -245,6 +245,7 @@ class MainWindow : public QMainWindow bool saveRideExitDialog(); // save dirty rides on exit dialog void showOptions(); void showTools(); + void showRhoEstimator(); void toggleSidebar(); void showSidebar(bool want); void showToolbar(bool want); diff --git a/src/ToolsRhoEstimator.cpp b/src/ToolsRhoEstimator.cpp new file mode 100644 index 000000000..778854c00 --- /dev/null +++ b/src/ToolsRhoEstimator.cpp @@ -0,0 +1,288 @@ +/* + * Copyright (c) Steven Gribble (gribble [at] gmail [dot] com) + * http://www.gribble.org/ + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include "ToolsRhoEstimator.h" +#include "Settings.h" +#include "Units.h" +#include +#include +#include + +typedef QDoubleSpinBox* QDoubleSpinBoxPtr; + +ToolsRhoEstimator::ToolsRhoEstimator(QWidget *parent) : QDialog(parent) { + // Does the user prefer metric or imperial? Set the initial radio + // button and field settings to their GoldenCheetah preference. + QVariant unit = appsettings->value(NULL, GC_UNIT); + useMetricUnits = (unit.toString() == "Metric"); + + // Set the main window title. + setWindowTitle(tr("Air Density (Rho) Estimator")); + setAttribute(Qt::WA_DeleteOnClose); + + // Create the main layout box. + QVBoxLayout *mainVBox = new QVBoxLayout(this); + + // Set up the instructions field. + mainVBox->addWidget(new QLabel( + tr("Enter measured values for the following:"))); + mainVBox->addSpacing(5); + + // "metric vs. imperial" radio buttons. (Makes it easier than + // forcing them to change their preference in the preferences menu.) + QHBoxLayout *rads = new QHBoxLayout; + metBut = new QRadioButton(tr("Metric")); + metBut->setChecked(useMetricUnits); + rads->addWidget(metBut); + impBut = new QRadioButton(tr("Imperial")); + impBut->setChecked(!useMetricUnits); + // note that we only need to connect one of the radio button + // signals, since changing one also changes the other. + connect(impBut, SIGNAL(toggled(bool)), + this, SLOT(on_radio_toggled(bool))); + rads->addWidget(impBut); + mainVBox->addLayout(rads); + + // The temperature box. + QHBoxLayout *thl = new QHBoxLayout; + tempSpinBox = new QDoubleSpinBox(this); + tempSpinBox->setDecimals(2); + tempSpinBox->setRange(-200, 200); + if (useMetricUnits) { + tempLabel = new QLabel("Temperature (C):"); + thl->addWidget(tempLabel); + tempSpinBox->setValue(15); + } else { + tempLabel = new QLabel("Temperature (F):"); + thl->addWidget(tempLabel); + tempSpinBox->setValue(59); + } + tempSpinBox->setWrapping(false); + tempSpinBox->setAlignment(Qt::AlignRight); + connect(tempSpinBox, SIGNAL(valueChanged(double)), + this, SLOT(on_valueChanged(double))); + thl->addWidget(tempSpinBox); + mainVBox->addLayout(thl); + + // The air pressure box. + QHBoxLayout *phl = new QHBoxLayout; + pressSpinBox = new QDoubleSpinBox(this); + pressSpinBox->setDecimals(2); + pressSpinBox->setRange(0, 2000); + if (useMetricUnits) { + pressLabel = new QLabel("Air Pressure (hPa):"); + phl->addWidget(pressLabel); + pressSpinBox->setValue(1018); + } else { + pressLabel = new QLabel("Air Pressure (inHg):"); + phl->addWidget(pressLabel); + pressSpinBox->setValue(30.06); + } + pressSpinBox->setWrapping(false); + pressSpinBox->setAlignment(Qt::AlignRight); + connect(pressSpinBox, SIGNAL(valueChanged(double)), + this, SLOT(on_valueChanged(double))); + phl->addWidget(pressSpinBox); + mainVBox->addLayout(phl); + + // The dewpoint box. + QHBoxLayout *dhl = new QHBoxLayout; + dewpSpinBox = new QDoubleSpinBox(this); + dewpSpinBox->setDecimals(2); + dewpSpinBox->setRange(-200, 200); + if (useMetricUnits) { + dewpLabel = new QLabel("Dewpoint (C):"); + dhl->addWidget(dewpLabel); + dewpSpinBox->setValue(7.5); + } else { + dewpLabel = new QLabel("Dewpoint (F):"); + dhl->addWidget(dewpLabel); + dewpSpinBox->setValue(45.5); + } + dewpSpinBox->setWrapping(false); + dewpSpinBox->setAlignment(Qt::AlignRight); + connect(dewpSpinBox, SIGNAL(valueChanged(double)), + this, SLOT(on_valueChanged(double))); + dhl->addWidget(dewpSpinBox); + mainVBox->addLayout(dhl); + + // Label for the computed air density. + mainVBox->addSpacing(15); + mainVBox->addWidget(new QLabel( + tr("Estimated air density (rho):"))); + + // Estimated Rho (metric units). + QHBoxLayout *rhoMetHBox = new QHBoxLayout; + txtRhoMet = new QLineEdit(this); + txtRhoMet->setAlignment(Qt::AlignRight); + txtRhoMet->setReadOnly(true); + rhoMetHBox->addWidget(txtRhoMet); + rhoMetHBox->addWidget(new QLabel(tr("(kg/m^3)"))); + mainVBox->addLayout(rhoMetHBox); + + // Estimated Rho (imperial units). + QHBoxLayout *rhoImpHBox = new QHBoxLayout; + txtRhoImp = new QLineEdit(this); + txtRhoImp->setAlignment(Qt::AlignRight); + txtRhoImp->setReadOnly(true); + rhoImpHBox->addWidget(txtRhoImp); + rhoImpHBox->addWidget(new QLabel(tr("(lb/ft^3)"))); + mainVBox->addLayout(rhoImpHBox); + + // "Done" button. + mainVBox->addSpacing(15); + QHBoxLayout *buttonHBox = new QHBoxLayout; + btnOK = new QPushButton(this); + btnOK->setText(tr("Done")); + buttonHBox->addWidget(btnOK); + mainVBox->addLayout(buttonHBox); + connect(btnOK, SIGNAL(clicked()), this, SLOT(on_btnOK_clicked())); + + // Force the initial "rho" calculation when the dialog box comes up + // initially. + this->on_valueChanged(0.0); +} + +void ToolsRhoEstimator::on_radio_toggled(bool checked) { + checked = true; // hack to avoid the "unused parameter" g++ warning + + if (impBut->isChecked()) { + // we just changed from metric --> imperial, so relabel and do the + // field conversions for the user. + tempSpinBox->setValue(celsius_to_fahrenheit(tempSpinBox->value())); + tempLabel->setText("Temperature (F):"); + pressSpinBox->setValue( + hectopascals_to_inchesmercury(pressSpinBox->value())); + pressLabel->setText("Air Pressure (inHg):"); + dewpSpinBox->setValue(celsius_to_fahrenheit(dewpSpinBox->value())); + dewpLabel->setText("Dewpoint (F):"); + } else { + // we just changed from imperial --> metric, so relabel and do the + // field conversions for the user. + tempSpinBox->setValue(fahrenheit_to_celsius(tempSpinBox->value())); + tempLabel->setText("Temperature (C):"); + pressSpinBox->setValue( + inchesmercury_to_hectopascals(pressSpinBox->value())); + pressLabel->setText("Air Pressure (hPa):"); + dewpSpinBox->setValue(fahrenheit_to_celsius(dewpSpinBox->value())); + dewpLabel->setText("Dewpoint (C):"); + } + + // Relay the "something has changed" signal to + // on_valueChanged(double). + this->on_valueChanged(0.0); +} + +void ToolsRhoEstimator::on_btnOK_clicked() { + // all done! + accept(); +} + +void ToolsRhoEstimator::on_valueChanged(double newval) { + newval++; // hack to avoid the "unused parameter" g++ warning. + + // scrape the field values, convert to metric if needed. + double temp = tempSpinBox->value(); + double press = pressSpinBox->value(); + double dewp = dewpSpinBox->value(); + if (impBut->isChecked()) { + // yup, convert to metric. + temp = fahrenheit_to_celsius(temp); + press = inchesmercury_to_hectopascals(press); + dewp = fahrenheit_to_celsius(dewp); + } + + // calculate rho, in (kg/m^3) + double rho_met = calculate_rho(temp, press, dewp); + // calculate rho in imperial units. + double rho_imp = rho_met_to_imp(rho_met); + + // display the calculated Rho's. + std::stringstream ss_met, ss_imp; + ss_met.precision(6); + ss_met << rho_met; + txtRhoMet->setText(tr(ss_met.str().c_str())); + ss_imp.precision(6); + ss_imp << rho_imp; + txtRhoImp->setText(tr(ss_imp.str().c_str())); +} + +double ToolsRhoEstimator::fahrenheit_to_celsius(double f) { + return (f - 32.0) * (5.0/9.0); +} + +double ToolsRhoEstimator::celsius_to_fahrenheit(double c) { + return (c * 9.0 / 5.0) + 32.0; +} + +double ToolsRhoEstimator::hectopascals_to_inchesmercury(double hpa) { + return (hpa / 1000.0) * 29.53; +} + +double ToolsRhoEstimator::inchesmercury_to_hectopascals(double inhg) { + return (inhg * 1000.0) / 29.53; +} + +double ToolsRhoEstimator::rho_met_to_imp(double rho) { + return rho * ((0.30480061 * 0.30480061 * 0.30480061) / 0.45359237); +} + +double ToolsRhoEstimator::calculate_rho(double temp, + double press, + double dewp) { + // Step 1: calculate the saturation water pressure at the dew point, + // i.e., the vapor pressure in the air. Use Herman Wobus' equation. + double c0 = 0.99999683; + double c1 = -0.90826951E-02; + double c2 = 0.78736169E-04; + double c3 = -0.61117958E-06; + double c4 = 0.43884187E-08; + double c5 = -0.29883885E-10; + double c6 = 0.21874425E-12; + double c7 = -0.17892321E-14; + double c8 = 0.11112018E-16; + double c9 = -0.30994571E-19; + double p = c0 + + dewp*(c1 + + dewp*(c2 + + dewp*(c3 + + dewp*(c4 + + dewp*(c5 + + dewp*(c6 + + dewp*(c7 + + dewp*(c8 + + dewp*(c9))))))))); + double psat_mbar = 6.1078 / std::pow(p, 8); + + // Step 2: calculate the vapor pressure. + double pv_pascals = psat_mbar * 100.0; + + // Step 3: calculate the pressure of dry air, given the vapor + // pressure and actual pressure. + double pd_pascals = (press*100) - pv_pascals; + + // Step 4: calculate the air density, using the equation for + // the density of a mixture of dry air and water vapor. + double density = + (pd_pascals / (287.0531 * (temp + 273.15))) + + (pv_pascals / (461.4964 * (temp + 273.15))); + + return density; +} diff --git a/src/ToolsRhoEstimator.h b/src/ToolsRhoEstimator.h new file mode 100644 index 000000000..0102f509c --- /dev/null +++ b/src/ToolsRhoEstimator.h @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2012 Steven Gribble (gribble [at] gmail [dot] com) + * http://www.gribble.org/ + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA. + */ + +#include "GoldenCheetah.h" + +#include +#include + +// This class implements a dialog box containing a tool for +// helping the user estimate rho, the air density, given +// temperature, pressure, and dew point as inputs. +class ToolsRhoEstimator : public QDialog { + Q_OBJECT + G_OBJECT + + public: + ToolsRhoEstimator(QWidget *parent = 0); + + private: + bool useMetricUnits; + QRadioButton *metBut; + QRadioButton *impBut; + QPushButton *btnOK; + QLineEdit *txtRhoImp; + QLineEdit *txtRhoMet; + QLabel *tempLabel; + QLabel *pressLabel; + QLabel *dewpLabel; + QDoubleSpinBox *tempSpinBox; + QDoubleSpinBox *pressSpinBox; + QDoubleSpinBox *dewpSpinBox; + double fahrenheit_to_celsius(double f); + double celsius_to_fahrenheit(double c); + double hectopascals_to_inchesmercury(double hpa); + double inchesmercury_to_hectopascals(double inhg); + double rho_met_to_imp(double rho); + double calculate_rho(double temp, double press, double dewp); + + private slots: + void on_radio_toggled(bool checked); + void on_btnOK_clicked(); + void on_valueChanged(double newval); +}; diff --git a/src/src.pro b/src/src.pro index 096c55173..b589f9c3d 100644 --- a/src/src.pro +++ b/src/src.pro @@ -328,6 +328,7 @@ HEADERS += \ TxtRideFile.h \ TimeUtils.h \ ToolsDialog.h \ + ToolsRhoEstimator.h \ TrainTool.h \ TreeMapWindow.h \ TreeMapPlot.h \ @@ -506,6 +507,7 @@ SOURCES += \ TimeInZone.cpp \ TimeUtils.cpp \ ToolsDialog.cpp \ + ToolsRhoEstimator.cpp \ TrainTool.cpp \ TreeMapWindow.cpp \ TreeMapPlot.cpp \