1 /***************************************************************************
2 recipe.cpp
3 -------------------
4 Recipe (document) class
5 -------------------
6 Copyright 2001-2008, David Johnson
7 Please see the header file for copyright and license information
8 ***************************************************************************/
9
10 #include <cmath>
11
12 #include <QApplication>
13 #include <QFile>
14 #include <QMessageBox>
15 #include <QTextDocument>
16 #include <QTextStream>
17
18 #include "beerxmlreader.h"
19 #include "data.h"
20 #include "recipereader.h"
21 #include "resource.h"
22 #include "textprinter.h"
23
24 #include "recipe.h"
25
26 using namespace Resource;
27
28 const QByteArray Recipe::EXTRACT_STRING = QT_TRANSLATE_NOOP("recipe", "Extract");
29 const QByteArray Recipe::PARTIAL_STRING = QT_TRANSLATE_NOOP("recipe", "Partial Mash");
30 const QByteArray Recipe::ALLGRAIN_STRING = QT_TRANSLATE_NOOP("recipe", "All Grain");
31
32 //////////////////////////////////////////////////////////////////////////////
33 // Construction, destruction //
34 //////////////////////////////////////////////////////////////////////////////
35
36 //////////////////////////////////////////////////////////////////////////////
37 // Recipe()
38 // --------
39 // Default constructor
40
Recipe(QObject * parent)41 Recipe::Recipe(QObject *parent)
42 : QObject(parent), modified_(false), title_(), brewer_(),
43 size_(5.0, Volume::gallon), style_(), grains_(), hops_(), miscs_(),
44 recipenotes_(), batchnotes_(), og_(0.0), ibu_(0), srm_(0)
45 { ; }
46
47 //////////////////////////////////////////////////////////////////////////////
48 // Recipe()
49 // --------
50 // Copy constructor
51
Recipe(const Recipe & r)52 Recipe::Recipe(const Recipe &r)
53 : QObject(0), modified_(r.modified_), title_(r.title_), brewer_(r.brewer_),
54 size_(r.size_), style_(r.style_), grains_(r.grains_), hops_(r.hops_),
55 miscs_(r.miscs_), recipenotes_(r.recipenotes_),
56 batchnotes_(r.batchnotes_), og_(r.og_), ibu_(r.ibu_), srm_(r.srm_)
57 {
58 }
59
60 //////////////////////////////////////////////////////////////////////////////
61 // operator=()
62 // -----------
63 // Assignment operator
operator =(const Recipe & r)64 Recipe Recipe::operator=(const Recipe &r)
65 {
66 if (this != &r) {
67 modified_ = r.modified_;
68 title_ = r.title_;
69 brewer_ = r.brewer_;
70 size_ = r.size_;
71 style_ = r.style_;
72 grains_ = r.grains_;
73 hops_ = r.hops_;
74 miscs_ = r.miscs_;
75 recipenotes_ = r.recipenotes_;
76 batchnotes_ = r.batchnotes_;
77 og_ = r.og_;
78 ibu_ = r.ibu_;
79 srm_ = r.srm_;
80 }
81 return *this;
82 }
83
~Recipe()84 Recipe::~Recipe()
85 { ; }
86
87 //////////////////////////////////////////////////////////////////////////////
88 // Serialization //
89 //////////////////////////////////////////////////////////////////////////////
90
91 //////////////////////////////////////////////////////////////////////////////
92 // newRecipe()
93 // -------------
94 // Clears recipe (new document)
95
newRecipe()96 void Recipe::newRecipe()
97 {
98 title_.clear();
99 brewer_.clear();
100 size_ = Data::instance()->defaultSize();
101 style_ = Data::instance()->defaultStyle();
102 grains_.clear();
103 hops_.clear();
104 miscs_.clear();
105 recipenotes_.clear();
106 batchnotes_.clear();
107
108 og_ = 0.0;
109 ibu_ = 0;
110 srm_ = 0;
111
112 setModified(false); // new documents are not in a modified state
113 emit recipeChanged();
114 }
115
116 /////////////////////////////////////////////////////////////////////////////
117 // nativeFormat()
118 // -------------
119 // Is the recipe in native format?
120
nativeFormat(const QString & filename)121 bool Recipe::nativeFormat(const QString &filename)
122 {
123 QFile datafile(filename);
124 if (!datafile.open(QFile::ReadOnly | QFile::Text)) {
125 // error opening file
126 qWarning() << "Error: Cannot open" << filename;
127 return false;
128 }
129
130 RecipeReader reader(&datafile);
131 return reader.isRecipeFormat();
132 }
133
134 /////////////////////////////////////////////////////////////////////////////
135 // beerXmlFormat()
136 // ---------------
137 // Is the recipe in BeerXML format?
138
beerXmlFormat(const QString & filename)139 bool Recipe::beerXmlFormat(const QString &filename)
140 {
141 QFile datafile(filename);
142 if (!datafile.open(QFile::ReadOnly | QFile::Text)) {
143 // error opening file
144 qWarning() << "Error: Cannot open" << filename;
145 return false;
146 }
147
148 BeerXmlReader reader(&datafile);
149 return reader.isBeerXmlFormat();
150 }
151
152 //////////////////////////////////////////////////////////////////////////////
153 // loadRecipe()
154 // --------------
155 // Load a recipe. Assumes file has been checked with nativeFormat()
156
loadRecipe(const QString & filename)157 bool Recipe::loadRecipe(const QString &filename)
158 {
159 // open file
160 QFile datafile(filename);
161 if (!datafile.open(QFile::ReadOnly | QFile::Text)) {
162 // error opening file
163 qWarning() << "Error: Cannot open" << filename;
164 QMessageBox::warning(0, TITLE,
165 tr("Cannot read file %1:\n%2")
166 .arg(filename)
167 .arg(datafile.errorString()));
168 datafile.close();
169 return false;
170 }
171
172 QApplication::setOverrideCursor(Qt::WaitCursor);
173 title_.clear();
174 brewer_.clear();
175 size_ = Data::instance()->defaultSize();
176 style_ = Data::instance()->defaultStyle();
177 grains_.clear();
178 hops_.clear();
179 miscs_.clear();
180 recipenotes_.clear();
181 batchnotes_.clear();
182
183 // parse file
184 RecipeReader reader(&datafile);
185 bool status = reader.readRecipe(this);
186 datafile.close();
187
188 if (!status) {
189 qWarning() << "Error: Problem reading file" << filename;
190 qWarning() << reader.errorString();
191 QMessageBox::warning(0, TITLE,
192 tr("Error reading file %1").arg(filename));
193 QApplication::restoreOverrideCursor();
194 return false;
195 }
196
197 // calculate the numbers
198 recalc();
199
200 // just loaded recipes are not modified
201 setModified(false);
202 emit recipeChanged();
203
204 QApplication::restoreOverrideCursor();
205 return true;
206 }
207
208 //////////////////////////////////////////////////////////////////////////////
209 // saveRecipe()
210 // ---------------
211 // Save a recipe
212
saveRecipe(const QString & filename)213 bool Recipe::saveRecipe(const QString &filename)
214 {
215 // open file
216 QFile datafile(filename);
217 if (!datafile.open(QFile::WriteOnly | QFile::Text)) {
218 // error opening file
219 qWarning() << "Error: Cannot open file" << filename;
220 QMessageBox::warning(0, TITLE,
221 tr("Cannot write file %1:\n%2")
222 .arg(filename)
223 .arg(datafile.errorString()));
224 datafile.close();
225 return false;
226 }
227
228 QApplication::setOverrideCursor(Qt::WaitCursor);
229
230 // write out xml
231 RecipeWriter writer(&datafile);
232 writer.writeRecipe(this);
233 datafile.close();
234
235 // recipe is saved, so set flags accordingly
236 setModified(false);
237 QApplication::restoreOverrideCursor();
238 return true;
239 }
240
241 //////////////////////////////////////////////////////////////////////////////
242 // previewRecipe()
243 // ---------------
244 // Preview the recipe (assumes textprinter has been setup)
245
previewRecipe(TextPrinter * textprinter)246 void Recipe::previewRecipe(TextPrinter *textprinter)
247 {
248 if (!textprinter) return;
249 QTextDocument document;
250 document.setHtml(recipeHTML());
251 textprinter->preview(&document);
252 }
253
254 void previewRecipe(TextPrinter *textprinter, QWidget *wparent);
255 //////////////////////////////////////////////////////////////////////////////
256 // printRecipe()
257 // ---------------
258 // Print the recipe (assumes textprinter has been setup)
259
printRecipe(TextPrinter * textprinter)260 void Recipe::printRecipe(TextPrinter *textprinter)
261 {
262 if (!textprinter) return;
263 QTextDocument document;
264 document.setHtml(recipeHTML());
265 textprinter->print(&document);
266 }
267
268 //////////////////////////////////////////////////////////////////////////////
269 // Miscellaneous //
270 //////////////////////////////////////////////////////////////////////////////
271
272 /////////////////////////////////////////////////////////////////////////////
273 // setStyle()
274 // ----------
275 // Set style from string
276
setStyle(const QString & s)277 void Recipe::setStyle(const QString &s)
278 {
279 if (Data::instance()->hasStyle(s))
280 style_ = Data::instance()->style(s);
281 else
282 style_ = Style();
283 setModified(true);
284 }
285
286 //////////////////////////////////////////////////////////////////////////////
287 // addGrain()
288 // ----------
289 // Add a grain ingredient to the recipe
290
addGrain(const Grain & g)291 void Recipe::addGrain(const Grain &g)
292 {
293 grains_.append(g);
294 recalc();
295 setModified(true);
296 }
297
298 //////////////////////////////////////////////////////////////////////////////
299 // addHop()
300 // ----------
301 // Add a hop ingredient to the recipe
302
addHop(const Hop & h)303 void Recipe::addHop(const Hop &h)
304 {
305 hops_.append(h);
306 recalc();
307 setModified(true);
308 }
309
310 //////////////////////////////////////////////////////////////////////////////
311 // addMisc()
312 // ----------
313 // Add a misc ingredient to the recipe
314
addMisc(const Misc & m)315 void Recipe::addMisc(const Misc &m)
316 {
317 miscs_.append(m);
318 recalc();
319 setModified(true);
320 }
321
322 //////////////////////////////////////////////////////////////////////////////
323 // recipeType()
324 // ------------
325 // Return type of recipe
326
method()327 QString Recipe::method()
328 {
329 int extract = 0;
330 int mash = 0;
331
332 foreach(Grain grain, grains_) {
333 if (grain.use().toLower() == Grain::MASHED_STRING.toLower()) mash++;
334 else if (grain.use().toLower() == Grain::EXTRACT_STRING.toLower()) extract++;
335 }
336
337 if (mash > 0) {
338 if (extract > 0) return PARTIAL_STRING;
339 else return ALLGRAIN_STRING;
340 }
341 return EXTRACT_STRING;
342 }
343
344 //////////////////////////////////////////////////////////////////////////////
345 // Calculations //
346 //////////////////////////////////////////////////////////////////////////////
347
348 //////////////////////////////////////////////////////////////////////////////
349 // recalc()
350 // -------
351 // Recalculate recipe values
352
recalc()353 void Recipe::recalc()
354 {
355 og_ = calcOG();
356 ibu_ = calcIBU();
357 srm_ = calcSRM();
358 }
359
360 //////////////////////////////////////////////////////////////////////////////
361 // calcOG()
362 // --------
363 // Calculate the original gravity
364
calcOG()365 double Recipe::calcOG()
366 {
367 double yield;
368 double est = 0.0;
369 foreach(Grain grain, grains_) {
370 yield = grain.yield();
371 if (grain.use().toLower() == Grain::MASHED_STRING.toLower()) {
372 // adjust for mash efficiency
373 yield *= Data::instance()->efficiency();
374 } else if (grain.use().toLower() == Grain::STEEPED_STRING.toLower()) {
375 // steeped grains don't yield nearly as much as mashed grains
376 yield *= Data::instance()->steepYield();
377 }
378 est += yield;
379 }
380 if (size_.amount()) {
381 est /= size_.amount(Volume::gallon);
382 } else {
383 est = 0.0;
384 }
385 return est + 1.0;
386 }
387
388 //////////////////////////////////////////////////////////////////////////////
389 // calcIBU()
390 // ---------
391 // Calculate the bitterness
392
calcIBU()393 int Recipe::calcIBU()
394 {
395 // switch between two possible calculations
396 if (Data::instance()->tinseth())
397 return calcTinsethIBU();
398 else
399 return calcRagerIBU();
400 }
401
402 //////////////////////////////////////////////////////////////////////////////
403 // calcRagerIBU()
404 // --------------
405 // Calculate the bitterness based on Rager's method (table method)
406
calcRagerIBU()407 int Recipe::calcRagerIBU()
408 {
409 // TODO: update this (and other) hop calculations
410 double bitterness = 0.0;
411 foreach(Hop hop, hops_) {
412 bitterness += hop.HBU() * Data::instance()->utilization(hop.time());
413 // TODO: we should also correct for hop type
414 }
415 if (size_.amount()) {
416 bitterness /= size_.amount(Volume::gallon);
417 } else {
418 bitterness = 0.0;
419 }
420 // correct for boil gravity
421 if (og_ > 1.050) bitterness /= 1.0 + ((og_ - 1.050) / 0.2);
422 return qRound(bitterness);
423 }
424
425 //////////////////////////////////////////////////////////////////////////////
426 // calcTinsethIBU()
427 // ----------------
428 // Calculate the bitterness based on Tinseth's method (formula method)
429 // The formula used is:
430 // (1.65*0.000125^(gravity-1))*(1-EXP(-0.04*time))*alpha*mass*1000
431 // ---------------------------------------------------------------
432 // (volume*4.15)
433
434 // TODO: recheck this formula
435
calcTinsethIBU()436 int Recipe::calcTinsethIBU()
437 {
438 const double GPO = 28.3495; // grams per ounce
439 const double LPG = 3.785; // liters per gallon
440
441 const double COEFF1 = 1.65;
442 const double COEFF2 = 0.000125;
443 const double COEFF3 = 0.04;
444 const double COEFF4 = 4.15;
445
446 double ibu;
447 double bitterness = 0.0;
448 foreach(Hop hop, hops_) {
449 ibu = (COEFF1 * pow(COEFF2, (og_ - 1.0))) *
450 (1.0 - exp(-COEFF3 * hop.time())) *
451 (hop.alpha()) * hop.weight().amount(Weight::ounce) * 1000.0;
452 if (size_.amount()) {
453 ibu /= (size_.amount(Volume::gallon) * COEFF4);
454 } else {
455 ibu = 0.0;
456 }
457 bitterness += ibu;
458 }
459 bitterness *= (GPO / LPG) / 100.0;
460 return qRound(bitterness);
461 }
462
463 //////////////////////////////////////////////////////////////////////////////
464 // calcSRM()
465 // ---------
466 // Calculate the color
467
calcSRM()468 int Recipe::calcSRM()
469 {
470 double srm = 0.0;
471 foreach(Grain grain, grains_) {
472 srm += grain.HCU();
473 }
474 if (size_.amount()) {
475 srm /= size_.amount(Volume::gallon);
476 } else {
477 srm = 0.0;
478 }
479
480 // switch between two possible calculations
481 if (Data::instance()->morey()) {
482 // power model (morey) [courtesy Rob Hudson <rob@tastybrew.com>]
483 srm = (pow(srm, 0.6859)) * 1.4922;
484 if (srm > 50) srm = 50;
485 } else {
486 // linear model (daniels)
487 if (srm > 8.0) {
488 srm *= 0.2;
489 srm += 8.4;
490 }
491 }
492 return qRound(srm);
493 }
494
495 // TODO: following formulas need to use constants
496
497 //////////////////////////////////////////////////////////////////////////////
498 // FGEstimate()
499 // ------------
500 // Return estimated final gravity
501
FGEstimate()502 double Recipe::FGEstimate()
503 {
504 if (og_ <= 0.0) return 0.0;
505 return (((og_ - 1.0) * 0.25) + 1.0);
506 }
507
508 //////////////////////////////////////////////////////////////////////////////
509 // ABV()
510 // -----
511 // Calculate alcohol by volume
512
ABV()513 double Recipe::ABV() // recipe version
514 {
515 return (ABV(og_, FGEstimate()));
516 }
517
ABV(double og,double fg)518 double Recipe::ABV(double og, double fg) // static version
519 {
520 return (og - fg) * 1.29;
521 }
522
523 //////////////////////////////////////////////////////////////////////////////
524 // ABW()
525 // -----
526 // Calculate alcohol by weight
527 // NOTE: Calculations were taken from http://hbd.org/ensmingr/
528
ABW()529 double Recipe::ABW() // recipe version
530 {
531 return (ABW(og_, FGEstimate()));
532 }
533
ABW(double og,double fg)534 double Recipe::ABW(double og, double fg) // static version
535 {
536 return (ABV(og, fg) * 0.785);
537 }
538
539 //////////////////////////////////////////////////////////////////////////////
540 // SgToP()
541 // -------
542 // Convert specific gravity to degrees plato
543
SgToP(double sg)544 double Recipe::SgToP(double sg)
545 {
546 return ((-463.37) + (668.72*sg) - (205.35 * sg * sg));
547 }
548
549 //////////////////////////////////////////////////////////////////////////////
550 // extractToYield()
551 // ----------------
552 // Convert extract potential to percent yield
553
554 // TODO: need to double check this, as well as terms
extractToYield(double extract)555 double Recipe::extractToYield(double extract)
556 {
557 const double SUCROSE = 46.21415;
558 return (((extract-1.0)*1000.0) / SUCROSE);
559 }
560
561 //////////////////////////////////////////////////////////////////////////////
562 // yieldToExtract()
563 // ----------------
564 // Convert percent yield to extract potential
565
yieldToExtract(double yield)566 double Recipe::yieldToExtract(double yield)
567 {
568 const double SUCROSE = 46.21415;
569 return ((yield*SUCROSE)/1000.0)+1.0;
570 }
571
572 //////////////////////////////////////////////////////////////////////////////
573 // Miscellaneous //
574 //////////////////////////////////////////////////////////////////////////////
575
setModified(bool mod)576 void Recipe::setModified(bool mod)
577 {
578 modified_ = mod;
579 if (mod) emit recipeModified();
580 }
581
modified() const582 bool Recipe::modified() const
583 {
584 return modified_;
585 }
586