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