src/EditProductTab9.cpp

Sat, 08 Jun 2024 15:54:30 +0200

author
Michiel Broek <mbroek@mbse.eu>
date
Sat, 08 Jun 2024 15:54:30 +0200
changeset 527
84091b9cb800
parent 461
add4dbef0c81
permissions
-rw-r--r--

Version 0.4.6a1. Added HLT equipment volume and deadspace settings. In EditProduct the target water selection is now sticky. Changed the water treatment tab. Added a row wich displays the salt adjustments. This can be selected between actual and target values. The treated water show can select between mash or sparge water. The total line will become the final water in the boil kettle. Database update function is expanded with the new settings. Added a popup message warning that the database is upgraded and user action is required for the equipment profiles.

/**
 * EditProduct.cpp is part of bmsapp.
 *
 * Tab 9, brewday.
 *
 * bmsapp 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 3 of the License, or
 * (at your option) any later version.
 *
 * bmsapp 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, see <http://www.gnu.org/licenses/>.
 */


/**
 * @brief Check the state by examining the date values.
 *        1. startdate and enddate invalid, planning/wait status.
 *           The enddate cannot be set.
 *        2. startdate valid and endate invalid, brewdate is planned.
 *           The fase will be brew. Enable setting of enddate.
 *        3. startdate valid, enddate and start and endtime can be set.
 *           The enddate cannot be before the startdate and not after 4
 *           days from the start.
 *        4. startdate and enddate and times are set and valid. Block
 *           the startdate setting. But only after setting a lot of
 *           brewdata and acknowledge set the fase to primary.
 *
 */
void EditProduct::updateBrewday()
{
    setStage();

    qDebug() << "updateBrewday" << product->brew_date_start << product->brew_date_end;

    ui->brew_startDate->setDate(product->brew_date_start.date());
    ui->brew_startTime->setTime(product->brew_date_start.time());
    ui->brew_endDate->setDate(product->brew_date_end.date());
    ui->brew_endTime->setTime(product->brew_date_end.time());
}


void EditProduct::brew_date_clear()
{
    product->brew_date_start.setDate(QDate());
    ui->brew_startDate->setDate(QDate());
}


void EditProduct::brew_date_today()
{
    product->brew_date_start.setDate(QDate::currentDate());
    ui->brew_startDate->setDate(QDate::currentDate());
}


void EditProduct::brew_start_date_changed(QDate val)
{
    product->brew_date_start.setDate(ui->brew_startDate->nullDate());
    updateBrewday();
    is_changed();
}


void EditProduct::brew_end_today()	// Not really, the brew start date is used.
{
    product->brew_date_end.setDate(product->brew_date_start.date());
    ui->brew_endDate->setDate(product->brew_date_end.date());
}


void EditProduct::brew_end_date_changed(QDate val)
{
    product->brew_date_end.setDate(ui->brew_endDate->nullDate());
    updateBrewday();
    is_changed();
}


void EditProduct::brew_start_time_changed(QTime val)
{
    product->brew_date_start.setTime(ui->brew_startTime->time());
    updateBrewday();
    is_changed();
}


void EditProduct::brew_end_time_changed(QTime val)
{
    product->brew_date_end.setTime(ui->brew_endTime->time());
    updateBrewday();
    is_changed();
}


void EditProduct::brew_date_ack()
{
    int rc = QMessageBox::warning(this, tr("Confirm brew"), tr("Confirm that the brew date and time are correct"),
			    QMessageBox::Yes | QMessageBox::No, QMessageBox::No);

    if (rc == QMessageBox::No)
        return;

    product->stage = PROD_STAGE_PRIMARY;
    setStage();
    is_changed();
}


void EditProduct::brew_mashph_changed(double val)
{
    if (product->brew_mash_ph == 0) {
	product->brew_mash_ph = 4.8;
	const QSignalBlocker blocker1(ui->brew_mashphEdit);
	ui->brew_mashphEdit->setValue(4.8);
    } else {
    	product->brew_mash_ph = val;
    }
    is_changed();
}


void EditProduct::brew_mashsg_changed(double val)
{
    product->brew_mash_sg = val;
    double c = Utils::sg_to_plato(product->est_mash_sg);
    double m = Utils::sg_to_plato(val);

    if (c > 0.5)
	product->brew_mash_efficiency = 100 * m / c;
    else
	product->brew_mash_efficiency = 0;
    ui->brew_masheffShow->setValue(product->brew_mash_efficiency);
    is_changed();
}


void EditProduct::brew_spargeph_changed(double val)
{
    if (product->brew_sparge_ph == 0) {
        product->brew_sparge_ph = 4.8;
        const QSignalBlocker blocker1(ui->brew_spargephEdit);
        ui->brew_spargephEdit->setValue(4.8);
    } else {
    	product->brew_sparge_ph = val;
    }
    is_changed();
}


void EditProduct::brew_brix_changed(double val)
{
    sg_return = Utils::brix_to_sg(val);
}


double EditProduct::brew_brix_edit(double sg, double sg_default)
{
    double brix = 0.0;

    QDialog* dialog = new QDialog(this);
    dialog->resize(360, 110);
    QDialogButtonBox *buttonBox = new QDialogButtonBox(dialog);
    buttonBox->setObjectName(QString::fromUtf8("buttonBox"));
    buttonBox->setGeometry(QRect(30, 60, 300, 32));
    buttonBox->setLayoutDirection(Qt::LeftToRight);
    buttonBox->setOrientation(Qt::Horizontal);
    buttonBox->setStandardButtons(QDialogButtonBox::Cancel|QDialogButtonBox::Ok);
    buttonBox->setCenterButtons(true);

    QLabel *brixLabel = new QLabel(dialog);
    brixLabel->setObjectName(QString::fromUtf8("brixLabel"));
    brixLabel->setText(tr("Refractometer Brix:"));
    brixLabel->setGeometry(QRect(10, 20, 161, 24));
    brixLabel->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);

    QDoubleSpinBox *brixEdit = new QDoubleSpinBox(dialog);
    brixEdit->setObjectName(QString::fromUtf8("brixEdit"));
    brixEdit->setGeometry(QRect(180, 20, 101, 24));
    brixEdit->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
    brixEdit->setAccelerated(true);
    brixEdit->setDecimals(1);
    brixEdit->setMaximum(32.0);
    brixEdit->setSingleStep(0.1);

    if (sg > 1.001)
	brix = Utils::sg_to_brix(sg);
    else
	brix = Utils::sg_to_brix(sg_default);
    brixEdit->setValue(brix);

    connect(brixEdit, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &EditProduct::brew_brix_changed);
    connect(buttonBox, SIGNAL(rejected()), dialog, SLOT(reject()));
    connect(buttonBox, SIGNAL(accepted()), dialog, SLOT(accept()));

    dialog->setModal(true);
    dialog->exec();
    if (dialog->result() == QDialog::Rejected) {
        sg_return = sg;
    }

    disconnect(brixEdit, nullptr, nullptr, nullptr);
    disconnect(buttonBox, nullptr, nullptr, nullptr);

    qDebug() << "brew_brix_edit(" << sg << sg_default << ") return" << sg_return;
    return sg_return;
}


void EditProduct::brew_volume_calc(double volume, double kettle_volume, double kettle_height, double est_volume, bool aboil, bool chiller)
{
    double k_cm;

    if ((chiller && volume > product->eq_chiller_volume) || (! chiller && volume > 0))
        k_cm = Utils::kettle_cm(volume, kettle_volume, kettle_height);
    else
        k_cm = Utils::kettle_cm(est_volume, kettle_volume, kettle_height);

    qDebug() << "brew_volume_calc(" << volume << kettle_volume << kettle_height << est_volume << aboil << chiller << ") cm:" << k_cm;

    QDialog* dialog = new QDialog(this);
    if (chiller)
	dialog->resize(320, 140);
    else
    	dialog->resize(320, 110);
    QDialogButtonBox *buttonBox = new QDialogButtonBox(dialog);
    buttonBox->setObjectName(QString::fromUtf8("buttonBox"));
    if (chiller)
	buttonBox->setGeometry(QRect(40, 95, 240, 32));
    else
    	buttonBox->setGeometry(QRect(40, 65, 240, 32));
    buttonBox->setLayoutDirection(Qt::LeftToRight);
    buttonBox->setOrientation(Qt::Horizontal);
    buttonBox->setStandardButtons(QDialogButtonBox::Ok);
    buttonBox->setCenterButtons(true);

    QLabel *cmLabel = new QLabel(dialog);
    cmLabel->setObjectName(QString::fromUtf8("cmLabel"));
    cmLabel->setText(tr("Distance from top:"));
    cmLabel->setGeometry(QRect(10, 20, 141, 20));
    cmLabel->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);

    QDoubleSpinBox *cmEdit = new QDoubleSpinBox(dialog);
    cmEdit->setObjectName(QString::fromUtf8("cmEdit"));
    cmEdit->setGeometry(QRect(160, 20, 121, 24));
    cmEdit->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
    cmEdit->setAccelerated(true);
    cmEdit->setDecimals(1);
    cmEdit->setSuffix(tr(" cm"));
    cmEdit->setMaximum(kettle_height * 100);
    cmEdit->setSingleStep(0.1);
    cmEdit->setValue(k_cm);

    if (chiller) {
	QLabel *opmLabel = new QLabel(dialog);
	opmLabel->setObjectName(QString::fromUtf8("opmLabel"));
	opmLabel->setText(tr("Measure with placed immersion chiller."));
	opmLabel->setGeometry(QRect(10, 60, 300, 24));
	opmLabel->setAlignment(Qt::AlignCenter);
    }

    if (aboil)
	connect(cmEdit, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &EditProduct::brew_aboil_cm_changed);
    else
	connect(cmEdit, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &EditProduct::brew_preboil_cm_changed);
    connect(buttonBox, SIGNAL(rejected()), dialog, SLOT(reject()));
    connect(buttonBox, SIGNAL(accepted()), dialog, SLOT(accept()));

    dialog->setModal(true);
    dialog->exec();
    disconnect(cmEdit, nullptr, nullptr, nullptr);
}


void EditProduct::brew_preboilph_changed(double val)
{
    if (product->brew_preboil_ph == 0) {
        product->brew_preboil_ph = 4.8;
        const QSignalBlocker blocker1(ui->brew_preboilphEdit);
        ui->brew_preboilphEdit->setValue(4.8);
    } else {
        product->brew_preboil_ph = val;
    }
    is_changed();
}


void EditProduct::calcEfficiencyBeforeBoil()
{
    double m = 0;
    double tot;
    double result = 0;

    if (product->fermentables.size() == 0)
	return;	// no fermentables loaded yet
    for (int i = 0; i < product->fermentables.size(); i++) {
   	if (product->fermentables.at(i).added == FERMENTABLE_ADDED_MASH) {
	    m += product->fermentables.at(i).amount * (product->fermentables.at(i).yield / 100) * (1 - product->fermentables.at(i).moisture / 100);
	}
    }
    tot = Utils::sg_to_plato(product->brew_preboil_sg) * (product->brew_preboil_volume / 1.04) * product->brew_preboil_sg * 10 / 1000;

    if (m > 0)
	result = round((tot / m * 100) * 10.0) / 10.0;

    if (result < 0)
	result = 0;
    ui->brew_preboileffShow->setValue(result);
}


void EditProduct::brew_preboilsg_changed(double val)
{
    if (product->brew_preboil_sg == 0) {
	product->brew_preboil_sg = product->preboil_sg;
	const QSignalBlocker blocker1(ui->brew_preboilsgEdit);
        ui->brew_preboilsgEdit->setValue(product->preboil_sg);
    } else {
    	product->brew_preboil_sg = val;
    }
    is_changed();
    calcEfficiencyBeforeBoil();
}


void EditProduct::brew_preboilvol_changed(double val)
{
    if (product->brew_preboil_volume == 0) {
	product->brew_preboil_volume = product->boil_size * 1.04;
	const QSignalBlocker blocker1(ui->brew_preboilvolEdit);
        ui->brew_preboilvolEdit->setValue(product->boil_size * 1.04);
    } else {
    	product->brew_preboil_volume = val;
    }
    is_changed();
    calcEfficiencyBeforeBoil();
}


void EditProduct::brew_preboil_cm_changed(double val)
{
    double vol = Utils::kettle_vol(val, product->eq_kettle_volume, product->eq_kettle_height);
    product->brew_preboil_volume = vol;
    const QSignalBlocker blocker1(ui->brew_preboilvolEdit);
    ui->brew_preboilvolEdit->setValue(vol);
    is_changed();
    calcEfficiencyBeforeBoil();
}


void EditProduct::brew_preboil_brix_button()
{
    product->brew_preboil_sg = brew_brix_edit(product->brew_preboil_sg, product->preboil_sg);
    ui->brew_preboilsgEdit->setValue(product->brew_preboil_sg);
}


void EditProduct::brew_preboil_button()
{
    brew_volume_calc(product->brew_preboil_volume, product->eq_kettle_volume, product->eq_kettle_height, product->boil_size * 1.04, false, false);
}


void EditProduct::brew_aboilph_changed(double val)
{
    if (product->brew_aboil_ph == 0) {
        product->brew_aboil_ph = 4.8;
        const QSignalBlocker blocker1(ui->brew_aboilphEdit);
        ui->brew_aboilphEdit->setValue(4.8);
    } else {
        product->brew_aboil_ph = val;
    }
    is_changed();
}


void EditProduct::calcEfficiencyAfterBoil()
{
    double m = 0;	// Sugars added at mash
    double b = 0;	// Sugars added at boil
    double result = 0;

    if (product->fermentables.size() == 0)
        return; // no fermentables loaded yet
    for (int i = 0; i < product->fermentables.size(); i++) {
	if (product->fermentables.at(i).added == FERMENTABLE_ADDED_MASH) {
	    m += product->fermentables.at(i).amount * (product->fermentables.at(i).yield / 100) * (1 - product->fermentables.at(i).moisture / 100);
	} else if (product->fermentables.at(i).added == FERMENTABLE_ADDED_BOIL) {
	    b += product->fermentables.at(i).amount * (product->fermentables.at(i).yield / 100) * (1 - product->fermentables.at(i).moisture / 100);
	}
    }
    double tot = Utils::sg_to_plato(product->brew_aboil_sg) * (product->brew_aboil_volume / 1.04) * product->brew_aboil_sg * 10 / 1000;
    tot -= b;	// total sugars in wort  minus added sugars.

    if (m > 0)
	result = round((tot / m * 100) * 10.0) / 10.0;
    product->brew_aboil_efficiency = result;
    ui->brew_aboileffShow->setValue(result);
    calcFermentables();	// This will also recalculate all volumes.
}


void EditProduct::brew_aboilsg_changed(double val)
{
    if (product->brew_aboil_sg == 0) {
        product->brew_aboil_sg = product->est_og3;
        const QSignalBlocker blocker1(ui->brew_aboilsgEdit);
        ui->brew_aboilsgEdit->setValue(product->est_og3);
    } else {
        product->brew_aboil_sg = val;
    }
    is_changed();
    calcEfficiencyAfterBoil();
}


void EditProduct::brew_aboilvol_changed(double val)
{
    if (product->brew_aboil_volume == 0) {
        product->brew_aboil_volume = product->batch_size * 1.04;
        const QSignalBlocker blocker1(ui->brew_aboilvolEdit);
        ui->brew_aboilvolEdit->setValue(product->brew_aboil_volume + product->eq_chiller_volume);
    } else {
        product->brew_aboil_volume = val - product->eq_chiller_volume;
    }
    is_changed();
    calcEfficiencyAfterBoil();
}


void EditProduct::brew_aboil_cm_changed(double val)
{
    double vol = Utils::kettle_vol(val, product->eq_kettle_volume, product->eq_kettle_height);
    product->brew_aboil_volume = vol - product->eq_chiller_volume;
    const QSignalBlocker blocker1(ui->brew_aboilvolEdit);
    ui->brew_aboilvolEdit->setValue(vol);
    is_changed();
    calcEfficiencyAfterBoil();
}


void EditProduct::brew_aboil_brix_button()
{
    product->brew_aboil_sg = brew_brix_edit(product->brew_aboil_sg, product->est_og3);
    ui->brew_aboilsgEdit->setValue(product->brew_aboil_sg);
}


void EditProduct::brew_aboil_button()
{
    brew_volume_calc(product->brew_aboil_volume + product->eq_chiller_volume,
		     product->eq_kettle_volume, product->eq_kettle_height,
		     (product->batch_size * 1.04) + product->eq_chiller_volume,
		     true, (product->eq_chiller_volume > 0));
}


void EditProduct::brew_cooling_to_changed(double val)
{
    product->brew_cooling_to = val;
    is_changed();
}


void EditProduct::brew_cooling_time_changed(double val)
{
    product->brew_cooling_time = val;
    is_changed();
}


void EditProduct::brew_whirlpool9_changed(double val)
{
    product->brew_whirlpool9 = val;
    is_changed();
}


void EditProduct::brew_whirlpool7_changed(double val)
{
    product->brew_whirlpool7 = val;
    is_changed();
}


void EditProduct::brew_whirlpool6_changed(double val)
{
    product->brew_whirlpool6 = val;
    is_changed();
}


void EditProduct::brew_whirlpool2_changed(double val)
{
    product->brew_whirlpool2 = val;
    is_changed();
}


void EditProduct::brew_aerwith_changed(int val)
{
    product->brew_aeration_type = val;
    is_changed();
}


void EditProduct::brew_aerspeed_changed(double val)
{
    product->brew_aeration_speed = val;
    is_changed();
}


void EditProduct::brew_aertime_changed(double val)
{
    product->brew_aeration_time = val;
    is_changed();
}


void EditProduct::brew_trubloss_changed(double val)
{
    product->brew_fermenter_tcloss = val;
    is_changed();
    calcFermentables(); // This will also recalculate all volumes.
    calcIBUs();
}


void EditProduct::brew_topupwater_changed(double val)
{
    product->brew_fermenter_extrawater = val;
    is_changed();
    calcFermentables(); // This will also recalculate all volumes.
    calcIBUs();
}


void EditProduct::brew_log_button()
{
    QSqlQuery query;
    double timestamp;

    QDialog* dialog = new QDialog(this);
    dialog->resize(1024, 600);

    QPushButton *saveButton = new QPushButton(tr("Save"));
    saveButton->setAutoDefault(false);
    QIcon icon1;
    icon1.addFile(QString::fromUtf8(":icons/silk/disk.png"), QSize(), QIcon::Normal, QIcon::Off);
    saveButton->setIcon(icon1);

    QDialogButtonBox *buttonBox = new QDialogButtonBox(dialog);
    buttonBox->setObjectName(QString::fromUtf8("buttonBox"));
    buttonBox->setOrientation(Qt::Vertical);
    buttonBox->setStandardButtons(QDialogButtonBox::Ok);
    buttonBox->addButton(saveButton,QDialogButtonBox::ActionRole);

    QLineSeries *pv_mlt = new QLineSeries();
    pv_mlt->setName("MLT");
    QLineSeries *sp_mlt = new QLineSeries();
    sp_mlt->setName("Set");
    sp_mlt->setOpacity(0.75);
    QLineSeries *pv_hlt = new QLineSeries();
    pv_hlt->setName("HLT");
    QLineSeries *pwm_mlt1 = new QLineSeries();	// Top side of area
    QLineSeries *pwm_mlt0 = new QLineSeries();	// Bottom side of area

    query.prepare("SELECT * FROM log_brews WHERE code=:code ORDER BY datetime");
    query.bindValue(":code", product->code);
    query.exec();
    while (query.next()) {
	timestamp = query.value("datetime").toDateTime().toSecsSinceEpoch() * 1000;
	pv_mlt->append(timestamp, query.value("pv_mlt").toDouble());
	sp_mlt->append(timestamp, query.value("sp_mlt").toDouble());
	if (query.value("pv_hlt").toDouble() > 0)
	    pv_hlt->append(timestamp, query.value("pv_hlt").toDouble());
	pwm_mlt0->append(timestamp, 0);
	pwm_mlt1->append(timestamp, query.value("pwm_mlt").toInt());
    }

    QPen pen(QColorConstants::Svg::navy);
    pen.setWidth(2);
    pv_mlt->setPen(pen);
    QAreaSeries *pwm_mlt = new QAreaSeries(pwm_mlt0, pwm_mlt1);
    pwm_mlt->setName("MLT pwr");
    pwm_mlt->setOpacity(0.25);

    chart = new QChart();
    chart->setTitle(QString("%1 \"%2\"").arg(product->code).arg(product->name));
    chart->addSeries(pwm_mlt);	// Order is important, first drawn is lowest layer.
    chart->addSeries(sp_mlt);
    chart->addSeries(pv_hlt);
    chart->addSeries(pv_mlt);	// Top layer.

    QDateTimeAxis *axisX = new QDateTimeAxis;
    axisX->setTickCount(20);
    axisX->setFormat("HH:mm");
    axisX->setTitleText(tr("Time"));
    axisX->setLabelsFont(QFont("Helvetica", 8, QFont::Normal));
    chart->addAxis(axisX, Qt::AlignBottom);
    pv_mlt->attachAxis(axisX);
    pv_hlt->attachAxis(axisX);

    QValueAxis *axisY = new QValueAxis;
    axisY->setRange(0, 110);
    axisY->setTickCount(12);
    axisY->setMinorTickCount(1);
    axisY->setLabelFormat("%i");
    axisY->setTitleText(tr("Temperature °C or Power %"));
    axisY->setLabelsFont(QFont("Helvetica", 8, QFont::Normal));
    chart->addAxis(axisY, Qt::AlignLeft);

    pwm_mlt->attachAxis(axisY);
    pv_mlt->attachAxis(axisY);
    sp_mlt->attachAxis(axisY);
    pv_hlt->attachAxis(axisY);

    connect(pv_mlt, &QLineSeries::hovered, this, &EditProduct::tooltip);
    connect(pv_hlt, &QLineSeries::hovered, this, &EditProduct::tooltip);

    chartView = new QChartView(chart);
    chartView->setRenderHint(QPainter::Antialiasing);
    dialog->setLayout(new QHBoxLayout);
    dialog->layout()->addWidget(chartView);
    dialog->layout()->addWidget(buttonBox);

    connect(buttonBox, SIGNAL(accepted()), dialog, SLOT(accept()));
    connect(saveButton, SIGNAL(clicked()), this, SLOT(savePNG()));

    dialog->setModal(true);
    dialog->exec();
}


void EditProduct::savePNG()
{
    QString path = QFileDialog::getSaveFileName(this, tr("Save Image"), QDir::homePath() + "/brewday.png", tr("Image (*.png)"));
    if (path.isEmpty()) {
	QMessageBox::warning(this, tr("Save File"), tr("No image file selected."));
	return;
    }

    QImage img((chartView->size()), QImage::Format_ARGB32);
    QPainter painter;
    painter.begin(&img);
    chartView->render(&painter);
    painter.setRenderHint(QPainter::Antialiasing);
    painter.end();
    img.save(path);
}


void EditProduct::tooltip(QPointF point, bool state)
{
    QAbstractSeries *series = qobject_cast<QAbstractSeries *>(sender());

    if (t_tooltip == 0)
	t_tooltip = new Callout(chart, series);

    if (state) {
	QDateTime timeis = QDateTime::fromMSecsSinceEpoch(point.x());
	t_tooltip->setSeries(series);
	t_tooltip->setText(QString("%1\n%2 %3°C").arg(timeis.toString("dd-MM-yyyy hh:mm")).arg(series->name()).arg(point.y(), 2, 'f', 1));
	t_tooltip->setAnchor(point);
	t_tooltip->setZValue(11);
	t_tooltip->updateGeometry();
	t_tooltip->show();
    } else {
	t_tooltip->hide();
    }
}

mercurial