# HG changeset patch # User Michiel Broek # Date 1649533819 -7200 # Node ID 2e79e0975e587ebf4b004029dac9be70a9e78734 # Parent ba26b19572aba7f5b19a1b16291c0bb79118af98 Start hops table display. The calculated preboil_sg value is stored global in memory. Added calculations for IBU, hop flavour and aroma. diff -r ba26b19572ab -r 2e79e0975e58 src/EditRecipe.cpp --- a/src/EditRecipe.cpp Sat Apr 09 13:34:45 2022 +0200 +++ b/src/EditRecipe.cpp Sat Apr 09 21:50:19 2022 +0200 @@ -488,6 +488,8 @@ connect(ui->addFermentable, SIGNAL(clicked()), this, SLOT(on_addFermentRow_clicked())); // All signals from tab "Hops" + connect(ui->hop_tasteShow, &QProgressBar::valueChanged, this, &EditRecipe::on_Flavour_valueChanged); + connect(ui->hop_aromaShow, &QProgressBar::valueChanged, this, &EditRecipe::on_Aroma_valueChanged); // connect(ui->hopsTable, SIGNAL(cellChanged(int, int)), this, SLOT(cell_Changed(int, int))); // All signals from tab "Miscs" @@ -643,8 +645,186 @@ } +bool EditRecipe::hop_sort_test(const Hops &D1, const Hops &D2) +{ + return (D1.h_useat <= D2.h_useat ) && (D1.h_time >= D2.h_time) && (D1.h_amount >= D2.h_amount); +} + + void EditRecipe::refreshHops() { + QString w; + QWidget* pWidget; + QHBoxLayout* pLayout; + QTableWidgetItem *item; + + qDebug() << "refreshHops" << recipe->hops.size(); + // std::sort(recipe->hops.begin(), recipe->hops.end(), hop_sort_test); + + /* + * During filling the table turn off the cellChanged signal because every cell that is filled + * triggers the cellChanged signal. The QTableWidget has no better signal to use. + */ + this->ignoreChanges = true; + + const QStringList labels({tr("Origin"), tr("Hop"), tr("Type"), tr("Form"), tr("Alpha"), tr("Use at"), tr("Time"), + tr("IBU"), tr("Amount"), tr("Delete"), tr("Edit") }); + + ui->hopsTable->setColumnCount(11); + ui->hopsTable->setColumnWidth(0, 150); /* Origin */ + ui->hopsTable->setColumnWidth(1, 225); /* Hop */ + ui->hopsTable->setColumnWidth(2, 75); /* Type */ + ui->hopsTable->setColumnWidth(3, 75); /* Form */ + ui->hopsTable->setColumnWidth(4, 75); /* Alpha% */ + ui->hopsTable->setColumnWidth(5, 75); /* Added */ + ui->hopsTable->setColumnWidth(6, 75); /* Time */ + ui->hopsTable->setColumnWidth(7, 60); /* IBU */ + ui->hopsTable->setColumnWidth(8, 75); /* Amount */ + ui->hopsTable->setColumnWidth(9, 80); /* Delete */ + ui->hopsTable->setColumnWidth(10, 80); /* Edit */ + ui->hopsTable->setHorizontalHeaderLabels(labels); + ui->hopsTable->verticalHeader()->hide(); + ui->hopsTable->setRowCount(recipe->hops.size()); + + for (int i = 0; i < recipe->hops.size(); i++) { + + ui->hopsTable->setItem(i, 0, new QTableWidgetItem(recipe->hops.at(i).h_origin)); + ui->hopsTable->setItem(i, 1, new QTableWidgetItem(recipe->hops.at(i).h_name)); + + item = new QTableWidgetItem(h_types[recipe->hops.at(i).h_type]); + item->setTextAlignment(Qt::AlignCenter|Qt::AlignVCenter); + ui->hopsTable->setItem(i, 2, item); + + item = new QTableWidgetItem(h_forms[recipe->hops.at(i).h_form]); + item->setTextAlignment(Qt::AlignCenter|Qt::AlignVCenter); + ui->hopsTable->setItem(i, 3, item); + + item = new QTableWidgetItem(QString("%1%").arg(recipe->hops.at(i).h_alpha, 2, 'f', 1, '0')); + item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter); + ui->hopsTable->setItem(i, 4, item); + + item = new QTableWidgetItem(h_useat[recipe->hops.at(i).h_useat]); + item->setTextAlignment(Qt::AlignCenter|Qt::AlignVCenter); + ui->hopsTable->setItem(i, 5, item); + + if (recipe->hops.at(i).h_useat == 2 || recipe->hops.at(i).h_useat == 4) { // Boil or whirlpool + item = new QTableWidgetItem(QString("%1 min.").arg(recipe->hops.at(i).h_time, 1, 'f', 0, '0')); + } else if (recipe->hops.at(i).h_useat == 5) { // Dry-hop + item = new QTableWidgetItem(QString("%1 days.").arg(recipe->hops.at(i).h_time / 1440, 1, 'f', 0, '0')); + } else { + item = new QTableWidgetItem(QString("")); + } + item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter); + ui->hopsTable->setItem(i, 6, item); + + double ibu = Utils::toIBU(recipe->hops.at(i).h_useat, recipe->hops.at(i).h_form, recipe->preboil_sg, recipe->batch_size, recipe->hops.at(i).h_amount, + recipe->hops.at(i).h_time, recipe->hops.at(i).h_alpha, recipe->ibu_method, 0, recipe->hops.at(i).h_time, 0); + item = new QTableWidgetItem(QString("%1").arg(ibu, 2, 'f', 1, '0')); + item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter); + ui->hopsTable->setItem(i, 7, item); + + if (recipe->hops.at(i).h_amount < 1.0) { + item = new QTableWidgetItem(QString("%1 gr").arg(recipe->hops.at(i).h_amount * 1000.0, 2, 'f', 1, '0')); + } else { + item = new QTableWidgetItem(QString("%1 kg").arg(recipe->hops.at(i).h_amount, 4, 'f', 3, '0')); + } + item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter); + ui->hopsTable->setItem(i, 8, item); + + /* Add the Delete row button */ + pWidget = new QWidget(); + QPushButton* btn_dele = new QPushButton(); + btn_dele->setObjectName(QString("%1").arg(i)); /* Send row with the button */ + btn_dele->setText(tr("Delete")); + connect(btn_dele, SIGNAL(clicked()), this, SLOT(on_deleteHopRow_clicked())); + pLayout = new QHBoxLayout(pWidget); + pLayout->addWidget(btn_dele); + pLayout->setContentsMargins(5, 0, 5, 0); + pWidget->setLayout(pLayout); + ui->hopsTable->setCellWidget(i, 9, pWidget); + + pWidget = new QWidget(); + QPushButton* btn_edit = new QPushButton(); + btn_edit->setObjectName(QString("%1").arg(i)); /* Send row with the button */ + btn_edit->setText(tr("Edit")); + connect(btn_edit, SIGNAL(clicked()), this, SLOT(on_editHopRow_clicked())); + pLayout = new QHBoxLayout(pWidget); + pLayout->addWidget(btn_edit); + pLayout->setContentsMargins(5, 0, 5, 0); + pWidget->setLayout(pLayout); + ui->hopsTable->setCellWidget(i, 10, pWidget); + } + this->ignoreChanges = false; +} + + +void EditRecipe::on_Flavour_valueChanged(int value) +{ + if (value < 20) { + ui->hop_tasteShow->setStyleSheet(bar_20); + ui->hop_tasteShow->setFormat(tr("Very low")); + } else if (value < 40) { + ui->hop_tasteShow->setStyleSheet(bar_40); + ui->hop_tasteShow->setFormat(tr("Low")); + } else if (value < 60) { + ui->hop_tasteShow->setStyleSheet(bar_60); + ui->hop_tasteShow->setFormat(tr("Moderate")); + } else if (value < 80) { + ui->hop_tasteShow->setStyleSheet(bar_80); + ui->hop_tasteShow->setFormat(tr("High")); + } else { + ui->hop_tasteShow->setStyleSheet(bar_100); + ui->hop_tasteShow->setFormat(tr("Very high")); + } +} + + +void EditRecipe::on_Aroma_valueChanged(int value) +{ + if (value < 20) { + ui->hop_aromaShow->setStyleSheet(bar_20); + ui->hop_aromaShow->setFormat(tr("Very low")); + } else if (value < 40) { + ui->hop_aromaShow->setStyleSheet(bar_40); + ui->hop_aromaShow->setFormat(tr("Low")); + } else if (value < 60) { + ui->hop_aromaShow->setStyleSheet(bar_60); + ui->hop_aromaShow->setFormat(tr("Moderate")); + } else if (value < 80) { + ui->hop_aromaShow->setStyleSheet(bar_80); + ui->hop_aromaShow->setFormat(tr("High")); + } else { + ui->hop_aromaShow->setStyleSheet(bar_100); + ui->hop_aromaShow->setFormat(tr("Very high")); + } +} + + +void EditRecipe::calcIBUs() +{ + double hop_flavour = 0, hop_aroma = 0, ibus = 0; + + for (int i = 0; i < recipe->hops.size(); i++) { + + ibus += Utils::toIBU(recipe->hops.at(i).h_useat, recipe->hops.at(i).h_form, recipe->preboil_sg, recipe->batch_size, recipe->hops.at(i).h_amount, + recipe->hops.at(i).h_time, recipe->hops.at(i).h_alpha, recipe->ibu_method, 0, recipe->hops.at(i).h_time, 0); + hop_flavour += Utils::hopFlavourContribution(recipe->hops.at(i).h_time, recipe->batch_size, recipe->hops.at(i).h_useat, recipe->hops.at(i).h_amount); + hop_aroma += Utils::hopAromaContribution(recipe->hops.at(i).h_time, recipe->batch_size, recipe->hops.at(i).h_useat, recipe->hops.at(i).h_amount); + } + + hop_flavour = round(hop_flavour * 1000.0 / 5.0) / 10; + hop_aroma = round(hop_aroma * 1000.0 / 6.0) / 10; + if (hop_flavour > 100) + hop_flavour = 100; + if (hop_aroma > 100) + hop_aroma = 100; + qDebug() << "ibu" << recipe->est_ibu << ibus << "flavour" << hop_flavour << "aroma" << hop_aroma; + + recipe->est_ibu = ibus; + ui->est_ibuEdit->setValue(recipe->est_ibu); + ui->est_ibu2Edit->setValue(recipe->est_ibu); + ui->hop_tasteShow->setValue(hop_flavour); + ui->hop_aromaShow->setValue(hop_aroma); } @@ -666,8 +846,12 @@ void EditRecipe::refreshAll() { refreshFermentables(); - - calcFermentables(); + calcFermentables(); /* Must be before Hops */ + refreshHops(); + calcIBUs(); + refreshMiscs(); + refreshYeasts(); + refreshMashs(); } @@ -789,8 +973,8 @@ ui->est_og3Edit->setValue(og); ui->est_ogShow->setValue(og); - double preboil_sg = Utils::estimate_sg(sugarsm, ui->boil_sizeEdit->value()); - qDebug() << " preboil SG" << preboil_sg; + recipe->preboil_sg = Utils::estimate_sg(sugarsm, ui->boil_sizeEdit->value()); + qDebug() << " preboil SG" << recipe->preboil_sg; /* * Color of the wort diff -r ba26b19572ab -r 2e79e0975e58 src/EditRecipe.h --- a/src/EditRecipe.h Sat Apr 09 13:34:45 2022 +0200 +++ b/src/EditRecipe.h Sat Apr 09 21:50:19 2022 +0200 @@ -233,6 +233,7 @@ int misc_row; int yeasts_row; int mashs_row; + double preboil_sg; }; @@ -280,6 +281,8 @@ void on_perc_sugars_valueChanged(int value); void on_perc_cara_valueChanged(int value); void on_lintner_valueChanged(int value); + void on_Flavour_valueChanged(int value); + void on_Aroma_valueChanged(int value); private: Ui::EditRecipe *ui; @@ -287,9 +290,17 @@ QStringList f_types = { tr("Grain"), tr("Sugar"), tr("Extract"), tr("Dry extract"), tr("Adjunct") }; QStringList f_graintypes = { tr("Base"), tr("Roast"), tr("Crystal"), tr("Kilned"), tr("Sour Malt"), tr("Special"), tr("No malt")}; QStringList f_added = { tr("Mash"), tr("Boil"), tr("Fermentation"), tr("Lagering"), tr("Bottle"), tr("Kegs") }; + QStringList h_types = { tr("Bittering"), tr("Aroma"), tr("Both") }; + QStringList h_forms = { tr("Pellet"), tr("Plug"), tr("Leaf"), tr("Leaf wet"), tr("Cryo") }; + QStringList h_useat = { tr("Mash"), tr("First wort"), tr("Boil"), tr("Aroma"), tr("Whirlpool"), tr("Dry hop") }; QString bar_red = "QProgressBar::chunk {background: #FF0000;}"; QString bar_orange = "QProgressBar::chunk {background: #EB7331;}"; QString bar_green = "QProgressBar::chunk {background: #008C00;}"; + QString bar_20 = "QProgressBar::chunk {background: #004D00;}"; + QString bar_40 = "QProgressBar::chunk {background: #008C00;}"; + QString bar_60 = "QProgressBar::chunk {background: #00BF00;}"; + QString bar_80 = "QProgressBar::chunk {background: #00FF00;}"; + QString bar_100 = "QProgressBar::chunk {background: #80FF80;}"; int recno; bool textIsChanged = false; bool ignoreChanges = false; @@ -304,8 +315,10 @@ void to100Fermentables(int row); static bool ferment_sort_test(const Fermentables &D1, const Fermentables &D2); + static bool hop_sort_test(const Hops &D1, const Hops &D2); void WindowTitle(); void calcFermentables(); + void calcIBUs(); }; #endif diff -r ba26b19572ab -r 2e79e0975e58 src/MainWindow.h --- a/src/MainWindow.h Sat Apr 09 13:34:45 2022 +0200 +++ b/src/MainWindow.h Sat Apr 09 21:50:19 2022 +0200 @@ -77,7 +77,19 @@ static IniWS wsProd; static IniWS wsDev; + + +static QString my_brewery_name = "No-name"; +static double my_factor_mashhop = -30; +static double my_factor_fwh = 10; +static double my_factor_pellet = 10; +static double my_factor_plug = 2; +static double my_factor_wethop = -82; +static double my_factor_cryohop = 100; +static int my_ibu_method = 0; +static int my_color_method = 0; static double my_brix_correction = 1.04; +static double my_grain_absorbtion = 1.01; namespace Ui { diff -r ba26b19572ab -r 2e79e0975e58 src/Utils.cpp --- a/src/Utils.cpp Sat Apr 09 13:34:45 2022 +0200 +++ b/src/Utils.cpp Sat Apr 09 21:50:19 2022 +0200 @@ -330,3 +330,136 @@ } +double Utils::toIBU(int Use, int Form, double SG, double Volume, double Amount, double Boiltime, double Alpha, + int Method, double Whirlpool9, double Whirlpool7, double Whirlpool6) +{ + double fmoment = 1.0, pfactor = 1.0, ibu = 0, boilfactor; + double sgfactor, AddedAlphaAcids, Bigness_factor, BoilTime_factor, utiisation; + + double gravity = SG; + double liters = Volume; + double alpha = Alpha / 100.0; + double mass = Amount * 1000.0; + double time = Boiltime; + + if ((Use == 3) || (Use == 4) || (Use == 5)) { // Aroma, Whirlpool or Dry hop. + fmoment = 0.0; + } else if (Use == 0) { // Mash + fmoment += my_factor_mashhop / 100.0; // Brouwhulp + } else if (Use == 1) { // First wort + fmoment += my_factor_fwh / 100.0; // Brouwhulp, Louis, Ozzie + } + + if (Form == 0) { // Pellet + pfactor += my_factor_pellet / 100.0; + } else if (Form == 1) { // Plug + pfactor += my_factor_plug / 100.0; + } else if (Form == 3) { // Wet leaf + pfactor += my_factor_wethop / 100.0; // From https://github.com/chrisgilmerproj/brewday/blob/master/brew/constants.py + } else if (Form == 4) { // Cryo hop + pfactor += my_factor_cryohop / 100.0; + } + + // Ideas from Zymurgy March-April 2018. These are not exact formulas! + double whirlibus = 0.0; + if (Use == 3 || Use == 4) { // Flameout or any whirlpool + + if (Whirlpool9) { + // 20 mg/l/50 min + whirlibus += (alpha * mass * 20) / liters * (Whirlpool9 / 50.0); + qDebug() << "Whirlpool9" << alpha * mass * 20 << " liter:" << liters << " time:" << Whirlpool9 << " ibu" << (alpha * mass * 20) / liters * (Whirlpool9 / 50.0); + } else { + if (Use == 3) { // Flameout hops are 2 minutes in this range. + whirlibus += (alpha * mass * 20) / liters * (2.0 / 50.0); + } + } + if (Whirlpool7) { + // 6 mg/l/50 min + whirlibus += (alpha * mass * 6) / liters * (Whirlpool7 / 50.0); + qDebug() << "Whirlpool7" << alpha * mass * 6 << " liter:" << liters << " time:" << Whirlpool7 << " ibu" << (alpha * mass * 6) / liters * (Whirlpool7 / 50.0); + } else { + if (Use == 3) { // Flameout hops are 4 minutes in this range. + whirlibus += (alpha * mass * 6) / liters * (4.0 / 50.0); + } + } + if (Whirlpool6) { + // 2 mg/l/50 min + whirlibus += (alpha * mass * 2) / liters * (Whirlpool6 / 50.0); + //console.log('Whirlpool6:' + alpha * mass * 2 + ' liter:' + liters + ' time:' + Whirlpool6 + ' ibu' + (alpha * mass * 2) / liters * (Whirlpool6 / 50)); + } + } + + if (Method == 0) { // Tinseth + /* http://realbeer.com/hops/research.html */ + AddedAlphaAcids = (alpha * mass * 1000) / liters; + Bigness_factor = 1.65 * pow(0.000125, gravity - 1); + BoilTime_factor = ((1 - exp(-0.04 * time)) / 4.15); + utiisation = Bigness_factor * BoilTime_factor; + ibu = round((utiisation * AddedAlphaAcids * fmoment * pfactor + whirlibus) * 100) / 100; + } + if (Method == 2) { // Daniels + if (Form == 2) // Leaf + boilfactor = -(0.0041 * time * time) + (0.6162 * time) + 1.5779; + else + boilfactor = -(0.0051 * time * time) + (0.7835 * time) + 1.9348; + if (gravity < 1050) + sgfactor = 0; + else + sgfactor = (gravity - 1050) / 200; + ibu = round((fmoment * ((mass * (alpha * 100) * boilfactor * 0.1) / (liters * (1 + sgfactor))) + whirlibus) * 100) / 100; + } + if (Method == 1) { // Rager + boilfactor = fmoment * 18.11 + 13.86 * tanh((time * 31.32) / 18.27); + if (gravity < 1050) + sgfactor = 0; + else + sgfactor = (gravity - 1050) / 200; + ibu = round(((mass * (alpha * 100) * boilfactor * 0.1) / (liters * (1 + sgfactor)) + whirlibus) * 100) / 100; + } + + return ibu; +} + + +double Utils::hopFlavourContribution(double bt, double vol, int use, double amount) +{ + double result; + + if (use == 4 || use == 5) // Whirlpool or Dry-hop + return 0; + if (use == 1) { // First wort + result = 0.15; // assume 15% flavourcontribution for fwh + } else if (bt > 50) { + result = 0.10; // assume 10% flavourcontribution as a minimum + } else { + result = 15.25 / (6 * sqrt(2 * 3.1416)) * exp(-0.5 * pow((bt - 21.0) / 6.0, 2.0)); + if (result < 0.10) + result = 0.10; // assume 10% flavourcontribution as a minimum + } + return (result * amount * 1000.0) / vol; +} + + +double Utils::hopAromaContribution(double bt, double vol, int use, double amount) +{ + double result = 0.0; + + if (use == 5) { // Dry hop + result = 1.33; + } else if (use == 4) { // Whirlpool + if (bt > 30) + bt = 30; // Max 30 minutes + result = 0.62 * bt / 30.0; + } else if (bt > 20) { + result = 0.0; + } else if (bt > 7.5) { + result = 10.03 / (4 * sqrt(2 * 3.1416)) * exp(-0.5 * pow((bt - 7.5) / 4.0, 2.0)); + } else if (use == 2) { // Boil + result = 1; + } else if (use == 3) { // Aroma + result = 1.2; + } + return (result * amount * 1000.0) / vol; +} + + diff -r ba26b19572ab -r 2e79e0975e58 src/Utils.h --- a/src/Utils.h Sat Apr 09 13:34:45 2022 +0200 +++ b/src/Utils.h Sat Apr 09 21:50:19 2022 +0200 @@ -28,6 +28,10 @@ double kw_to_srm(int colormethod, double c); double kw_to_ebc(int colormethod, double c); double abvol(double og, double fg); + double toIBU(int Use, int Form, double SG, double Volume, double Amount, double Boiltime, double Alpha, + int Method, double Whirlpool9, double Whirlpool7, double Whirlpool6); + double hopFlavourContribution(double bt, double vol, int use, double amount); + double hopAromaContribution(double bt, double vol, int use, double amount); QString hours_to_string(int hours); /** diff -r ba26b19572ab -r 2e79e0975e58 ui/EditRecipe.ui --- a/ui/EditRecipe.ui Sat Apr 09 13:34:45 2022 +0200 +++ b/ui/EditRecipe.ui Sat Apr 09 21:50:19 2022 +0200 @@ -1400,7 +1400,7 @@ 140 10 - 71 + 81 24 @@ -1509,12 +1509,29 @@ 10 - 50 + 100 1101 - 411 + 361 + + + + 140 + 70 + 80 + 23 + + + + Add + + + + :/icons/bms/hop.png:/icons/bms/hop.png + +