Rev 1182 | Rev 1217 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
1 | |||
671 | werner | 2 | /******************************************************************************************** |
3 | ** iLand - an individual based forest landscape and disturbance model |
||
4 | ** http://iland.boku.ac.at |
||
5 | ** Copyright (C) 2009- Werner Rammer, Rupert Seidl |
||
6 | ** |
||
7 | ** This program is free software: you can redistribute it and/or modify |
||
8 | ** it under the terms of the GNU General Public License as published by |
||
9 | ** the Free Software Foundation, either version 3 of the License, or |
||
10 | ** (at your option) any later version. |
||
11 | ** |
||
12 | ** This program is distributed in the hope that it will be useful, |
||
13 | ** but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
14 | ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
15 | ** GNU General Public License for more details. |
||
16 | ** |
||
17 | ** You should have received a copy of the GNU General Public License |
||
18 | ** along with this program. If not, see <http://www.gnu.org/licenses/>. |
||
19 | ********************************************************************************************/ |
||
20 | |||
21 | |||
247 | werner | 22 | /** @class Climate |
23 | Climate handles climate input data and performs some basic related calculations on that data. |
||
24 | @ingroup core |
||
25 | @sa http://iland.boku.ac.at/ClimateData |
||
193 | werner | 26 | */ |
27 | #include "global.h" |
||
28 | #include "climate.h" |
||
201 | werner | 29 | #include "model.h" |
977 | werner | 30 | #include "timeevents.h" |
193 | werner | 31 | |
226 | werner | 32 | double ClimateDay::co2 = 350.; // base value of ambient CO2-concentration (ppm) |
894 | werner | 33 | QVector<int> sampled_years; // list of sampled years to use |
226 | werner | 34 | |
35 | |||
894 | werner | 36 | |
210 | werner | 37 | void Sun::setup(double latitude_rad) |
38 | { |
||
39 | mLatitude = latitude_rad; |
||
211 | werner | 40 | if (mLatitude>0) |
41 | mDayWithMaxLength = 182-10; // 21.juni |
||
42 | else |
||
43 | mDayWithMaxLength = 365-10; //southern hemisphere |
||
210 | werner | 44 | // calculate length of day using the approximation formulae of: http://herbert.gandraxa.com/length_of_day.aspx |
45 | const double j = M_PI / 182.625; |
||
46 | const double ecliptic = RAD(23.439); |
||
47 | double m; |
||
48 | for (int day=0;day<366;day++) { |
||
211 | werner | 49 | m = 1. - tan(latitude_rad)*tan(ecliptic*cos(j*(day+10))); // day=0: winter solstice => subtract 10 days |
210 | werner | 50 | m = limit(m, 0., 2.); |
211 | werner | 51 | mDaylength_h[day] = acos(1-m)/M_PI * 24.; // result in hours [0..24] |
210 | werner | 52 | } |
440 | werner | 53 | mDayWith10_5hrs = 0; |
54 | for (int day=mDayWithMaxLength;day<366;day++) { |
||
55 | if (mDaylength_h[day]<10.5) { |
||
56 | mDayWith10_5hrs = day; |
||
57 | break; |
||
58 | } |
||
1008 | werner | 59 | } |
60 | mDayWith14_5hrs = 0; |
||
61 | for (int day=mDayWithMaxLength;day<366;day++) { |
||
62 | if (mDaylength_h[day]<14.5) { |
||
63 | mDayWith14_5hrs = day; |
||
64 | break; |
||
65 | } |
||
66 | } |
||
210 | werner | 67 | } |
68 | |||
69 | QString Sun::dump() |
||
70 | { |
||
211 | werner | 71 | QString result=QString("lat: %1, longest day: %2\ndoy;daylength").arg(mLatitude).arg(mDayWithMaxLength); |
210 | werner | 72 | for (int i=0;i<366;i++) |
73 | result+=QString("\n%1;%2").arg(i).arg(mDaylength_h[i]); |
||
74 | return result; |
||
75 | } |
||
76 | |||
193 | werner | 77 | Climate::Climate() |
78 | { |
||
79 | mLoadYears = 1; |
||
552 | werner | 80 | mInvalidDay.dayOfMonth=mInvalidDay.month=mInvalidDay.year=-1; |
214 | werner | 81 | mBegin = mEnd = 0; |
280 | werner | 82 | mIsSetup = false; |
193 | werner | 83 | } |
84 | |||
85 | |||
198 | werner | 86 | // access functions |
214 | werner | 87 | |
255 | werner | 88 | const ClimateDay *Climate::day(const int month, const int day) const |
198 | werner | 89 | { |
201 | werner | 90 | if (mDayIndices.isEmpty()) |
91 | return &mInvalidDay; |
||
209 | werner | 92 | return mStore.constBegin() + mDayIndices[mCurrentYear*12 + month] + day; |
198 | werner | 93 | } |
255 | werner | 94 | void Climate::monthRange(const int month, const ClimateDay **rBegin, const ClimateDay **rEnd) const |
198 | werner | 95 | { |
209 | werner | 96 | *rBegin = mStore.constBegin() + mDayIndices[mCurrentYear*12 + month]; |
97 | *rEnd = mStore.constBegin() + mDayIndices[mCurrentYear*12 + month+1]; |
||
229 | werner | 98 | //qDebug() << "monthRange returning: begin:"<< (*rBegin)->toString() << "end-1:" << (*rEnd-1)->toString(); |
198 | werner | 99 | } |
100 | |||
255 | werner | 101 | double Climate::days(const int month) const |
198 | werner | 102 | { |
208 | werner | 103 | return (double) mDayIndices[mCurrentYear*12 + month + 1]-mDayIndices[mCurrentYear*12 + month]; |
198 | werner | 104 | } |
255 | werner | 105 | int Climate::daysOfYear() const |
201 | werner | 106 | { |
107 | if (mDayIndices.isEmpty()) |
||
108 | return -1; |
||
214 | werner | 109 | return mEnd - mBegin; |
201 | werner | 110 | } |
198 | werner | 111 | |
255 | werner | 112 | void Climate::toDate(const int yearday, int *rDay, int *rMonth, int *rYear) const |
214 | werner | 113 | { |
114 | const ClimateDay *d = dayOfYear(yearday); |
||
552 | werner | 115 | if (rDay) *rDay = d->dayOfMonth-1; |
214 | werner | 116 | if (rMonth) *rMonth = d->month-1; |
117 | if (rYear) *rYear = d->year; |
||
118 | } |
||
198 | werner | 119 | |
193 | werner | 120 | void Climate::setup() |
121 | { |
||
122 | GlobalSettings *g=GlobalSettings::instance(); |
||
123 | XmlHelper xml(g->settings().node("model.climate")); |
||
124 | QString tableName =xml.value("tableName"); |
||
318 | werner | 125 | mName = tableName; |
894 | werner | 126 | QString filter = xml.value("filter"); |
127 | |||
859 | werner | 128 | mLoadYears = (int) qMax(xml.valueDouble("batchYears", 1.),1.); |
348 | werner | 129 | mDoRandomSampling = xml.valueBool("randomSamplingEnabled", false); |
130 | mRandomYearList.clear(); |
||
131 | mRandomListIndex = -1; |
||
132 | QString list = xml.value("randomSamplingList"); |
||
133 | if (mDoRandomSampling) { |
||
356 | werner | 134 | if (!list.isEmpty()) { |
135 | QStringList strlist = list.split(QRegExp("\\W+"), QString::SkipEmptyParts); |
||
136 | foreach(const QString &s,strlist) |
||
137 | mRandomYearList.push_back(s.toInt()); |
||
138 | // check for validity |
||
139 | foreach(int year, mRandomYearList) |
||
140 | if (year < 0 || year>=mLoadYears) |
||
981 | werner | 141 | throw IException("Invalid randomSamplingList! Year numbers are 0-based and must to between 0 and batchYears-1 (check value of batchYears)!!!"); |
356 | werner | 142 | } |
143 | |||
348 | werner | 144 | if (mRandomYearList.count()>0) |
1204 | werner | 145 | qDebug() << "Climate: Random sampling enabled with fixed list" << mRandomYearList.count() << "of years. climate:" << name(); |
348 | werner | 146 | else |
1204 | werner | 147 | qDebug() << "Climate: Random sampling enabled (without a fixed list). climate:" << name(); |
348 | werner | 148 | } |
313 | werner | 149 | mTemperatureShift = xml.valueDouble("temperatureShift", 0.); |
150 | mPrecipitationShift = xml.valueDouble("precipitationShift", 1.); |
||
335 | werner | 151 | if (mTemperatureShift!=0. || mPrecipitationShift!=1.) |
152 | qDebug() << "Climate modifaction: add temperature:" << mTemperatureShift << ". Multiply precipitation: " << mPrecipitationShift; |
||
153 | |||
735 | werner | 154 | mStore.resize(mLoadYears * 366 + 1); // reserve enough space (1 more than used at max) |
193 | werner | 155 | mCurrentYear=0; |
156 | mMinYear = 0; |
||
157 | mMaxYear = 0; |
||
158 | |||
894 | werner | 159 | // add a where-clause |
160 | if (!filter.isEmpty()) { |
||
161 | filter = QString("where %1").arg(filter); |
||
162 | qDebug() << "adding climate table where-clause:" << filter; |
||
163 | } |
||
164 | |||
165 | QString query=QString("select year,month,day,min_temp,max_temp,prec,rad,vpd from %1 %2 order by year, month, day").arg(tableName).arg(filter); |
||
193 | werner | 166 | // here add more options... |
167 | mClimateQuery = QSqlQuery(g->dbclimate()); |
||
168 | mClimateQuery.exec(query); |
||
653 | werner | 169 | mTMaxAvailable = true; |
193 | werner | 170 | if (mClimateQuery.lastError().isValid()){ |
653 | werner | 171 | // fallback: if there is no max_temp try the older format: |
754 | werner | 172 | query=QString("select year,month,day,temp,min_temp,prec,rad,vpd from %1 order by year, month, day").arg(tableName); |
653 | werner | 173 | mClimateQuery.exec(query); |
174 | mTMaxAvailable = false; |
||
175 | if (mClimateQuery.lastError().isValid()){ |
||
176 | throw IException(QString("Error setting up climate: %1 \n %2").arg(query, mClimateQuery.lastError().text()) ); |
||
177 | } |
||
193 | werner | 178 | } |
653 | werner | 179 | // setup query |
198 | werner | 180 | // load first chunk... |
181 | load(); |
||
214 | werner | 182 | setupPhenology(); // load phenology |
183 | // setup sun |
||
184 | mSun.setup(Model::settings().latitude); |
||
246 | werner | 185 | mCurrentYear--; // go to "-1" -> the first call to next year will go to year 0. |
894 | werner | 186 | sampled_years.clear(); |
280 | werner | 187 | mIsSetup = true; |
193 | werner | 188 | } |
189 | |||
190 | |||
191 | void Climate::load() |
||
192 | { |
||
193 | if (!mClimateQuery.isActive()) |
||
194 | throw IException(QString("Error loading climate file - query not active.")); |
||
195 | |||
201 | werner | 196 | ClimateDay lastDay = *day(11,30); // 31.december |
194 | werner | 197 | mMinYear = mMaxYear; |
193 | werner | 198 | QVector<ClimateDay>::iterator store=mStore.begin(); |
201 | werner | 199 | |
193 | werner | 200 | mDayIndices.clear(); |
201 | ClimateDay *cday = store; |
||
202 | int lastmon = -1; |
||
203 | int lastyear = -1; |
||
204 | int yeardays; |
||
205 | for (int i=0;i<mLoadYears;i++) { |
||
206 | yeardays = 0; |
||
977 | werner | 207 | if (GlobalSettings::instance()->model()->timeEvents()) { |
208 | QVariant val_temp = GlobalSettings::instance()->model()->timeEvents()->value(GlobalSettings::instance()->currentYear() + i, "model.climate.temperatureShift"); |
||
209 | QVariant val_prec = GlobalSettings::instance()->model()->timeEvents()->value(GlobalSettings::instance()->currentYear() + i, "model.climate.precipitationShift"); |
||
210 | if (val_temp.isValid()) |
||
211 | mTemperatureShift = val_temp.toDouble(); |
||
212 | if (val_prec.isValid()) |
||
213 | mPrecipitationShift = val_prec.toDouble(); |
||
214 | |||
981 | werner | 215 | if (mTemperatureShift!=0. || mPrecipitationShift!=1.) { |
216 | qDebug() << "Climate modification: add temperature:" << mTemperatureShift << ". Multiply precipitation: " << mPrecipitationShift; |
||
217 | if (mDoRandomSampling) { |
||
1182 | werner | 218 | qWarning() << "WARNING - Climate: using a randomSamplingList and temperatureShift/precipitationShift at the same time. The same offset is applied for *every instance* of a year!!"; |
1081 | werner | 219 | //throw IException("Climate: cannot use a randomSamplingList and temperatureShift/precipitationShift at the same time. Sorry."); |
981 | werner | 220 | } |
221 | } |
||
977 | werner | 222 | } |
223 | |||
198 | werner | 224 | //qDebug() << "loading year" << lastyear+1; |
194 | werner | 225 | while(1==1) { |
226 | if(!mClimateQuery.next()) { |
||
227 | // rewind to start |
||
228 | qDebug() << "restart of climate table"; |
||
229 | lastyear=-1; |
||
230 | if (!mClimateQuery.first()) |
||
231 | throw IException("Error rewinding climate file!"); |
||
232 | } |
||
193 | werner | 233 | yeardays++; |
234 | if (yeardays>366) |
||
235 | throw IException("Error in reading climate file: yeardays>366!"); |
||
236 | |||
201 | werner | 237 | |
193 | werner | 238 | cday = store++; // store values directly in the QVector |
201 | werner | 239 | |
193 | werner | 240 | cday->year = mClimateQuery.value(0).toInt(); |
241 | cday->month = mClimateQuery.value(1).toInt(); |
||
552 | werner | 242 | cday->dayOfMonth = mClimateQuery.value(2).toInt(); |
653 | werner | 243 | if (mTMaxAvailable) { |
244 | //References for calculation the temperature of the day: |
||
245 | //Floyd, R. B., Braddock, R. D. 1984. A simple method for fitting average diurnal temperature curves. Agricultural and Forest Meteorology 32: 107-119. |
||
246 | //Landsberg, J. J. 1986. Physiological ecology of forest production. Academic Press Inc., 197 S. |
||
247 | |||
248 | cday->min_temperature = mClimateQuery.value(3).toDouble() + mTemperatureShift; |
||
249 | cday->max_temperature = mClimateQuery.value(4).toDouble() + mTemperatureShift; |
||
250 | cday->temperature = 0.212*(cday->max_temperature - cday->mean_temp()) + cday->mean_temp(); |
||
251 | |||
252 | } else { |
||
253 | // for compatibility: the old method |
||
254 | cday->temperature = mClimateQuery.value(3).toDouble() + mTemperatureShift; |
||
255 | cday->min_temperature = mClimateQuery.value(4).toDouble() + mTemperatureShift; |
||
256 | cday->max_temperature = cday->temperature; |
||
257 | } |
||
414 | werner | 258 | cday->preciptitation = mClimateQuery.value(5).toDouble() * mPrecipitationShift; |
259 | cday->radiation = mClimateQuery.value(6).toDouble(); |
||
260 | cday->vpd = mClimateQuery.value(7).toDouble(); |
||
319 | werner | 261 | // sanity checks |
552 | werner | 262 | if (cday->month<1 || cday->dayOfMonth<1 || cday->month>12 || cday->dayOfMonth>31) |
263 | qDebug() << QString("Invalid dates in climate table %1: year %2 month %3 day %4!").arg(name()).arg(cday->year).arg(cday->month).arg(cday->dayOfMonth); |
||
264 | DBG_IF(cday->month<1 || cday->dayOfMonth<1 || cday->month>12 || cday->dayOfMonth>31,"Climate:load", "invalid dates"); |
||
802 | werner | 265 | DBG_IF(cday->temperature<-70 || cday->temperature>50,"Climate:load", "temperature out of range (-70..+50 degree C)"); |
255 | werner | 266 | DBG_IF(cday->preciptitation<0 || cday->preciptitation>200,"Climate:load", "precipitation out of range (0..200mm)"); |
241 | werner | 267 | DBG_IF(cday->radiation<0 || cday->radiation>50,"Climate:load", "radiation out of range (0..50 MJ/m2/day)"); |
268 | DBG_IF(cday->vpd<0 || cday->vpd>10,"Climate:load", "vpd out of range (0..10 kPa)"); |
||
269 | |||
193 | werner | 270 | if (cday->month != lastmon) { |
271 | // new month... |
||
272 | lastmon = cday->month; |
||
273 | // save relative position of the beginning of the new month |
||
209 | werner | 274 | mDayIndices.push_back( cday - mStore.constBegin() ); |
193 | werner | 275 | } |
276 | if (yeardays==1) { |
||
277 | // check on first day of the year |
||
278 | if (lastyear!=-1 && cday->year!=lastyear+1) |
||
552 | werner | 279 | throw IException(QString("Error in reading climate file: invalid year break at y-m-d: %1-%2-%3!").arg(cday->year).arg(cday->month).arg(cday->dayOfMonth)); |
193 | werner | 280 | } |
552 | werner | 281 | if (cday->month==12 && cday->dayOfMonth==31) |
193 | werner | 282 | break; |
283 | |||
284 | if (cday==mStore.end()) |
||
285 | throw IException("Error in reading climate file: read across the end!"); |
||
286 | } |
||
287 | lastyear = cday->year; |
||
288 | } |
||
201 | werner | 289 | while (store!=mStore.end()) |
290 | *store++ = mInvalidDay; // save a invalid day at the end... |
||
291 | |||
198 | werner | 292 | mDayIndices.push_back(cday- mStore.begin()); // the absolute last day... |
193 | werner | 293 | mMaxYear = mMinYear+mLoadYears; |
294 | mCurrentYear = 0; |
||
214 | werner | 295 | mBegin = mStore.begin() + mDayIndices[mCurrentYear*12]; |
296 | mEnd = mStore.begin() + mDayIndices[(mCurrentYear+1)*12];; // point to the 1.1. of the next year |
||
297 | |||
212 | werner | 298 | climateCalculations(lastDay); // perform additional calculations based on the climate data loaded from the database |
198 | werner | 299 | |
193 | werner | 300 | } |
301 | |||
302 | |||
303 | void Climate::nextYear() |
||
304 | { |
||
201 | werner | 305 | |
348 | werner | 306 | if (!mDoRandomSampling) { |
307 | // default behaviour: simply advance to next year, call load() if end reached |
||
894 | werner | 308 | if (mCurrentYear >= mLoadYears-1) // need to load more data |
348 | werner | 309 | load(); |
310 | else |
||
311 | mCurrentYear++; |
||
312 | } else { |
||
313 | // random sampling |
||
314 | if (mRandomYearList.isEmpty()) { |
||
1164 | werner | 315 | // random without list |
894 | werner | 316 | // make sure that the sequence of years is the same for the full landscape |
317 | if (sampled_years.size()<GlobalSettings::instance()->currentYear()) { |
||
318 | while (sampled_years.size()-1 < GlobalSettings::instance()->currentYear()) |
||
1164 | werner | 319 | sampled_years.append(irandom(0,mLoadYears)); |
894 | werner | 320 | } |
321 | |||
322 | mCurrentYear = sampled_years[GlobalSettings::instance()->currentYear()]; |
||
323 | |||
348 | werner | 324 | } else { |
894 | werner | 325 | // random with fixed list |
348 | werner | 326 | mRandomListIndex++; |
327 | if (mRandomListIndex>=mRandomYearList.count()) |
||
328 | mRandomListIndex=0; |
||
329 | mCurrentYear = mRandomYearList[mRandomListIndex]; |
||
330 | if (mCurrentYear >= mLoadYears) |
||
331 | throw IException(QString("Climate: load year with random sampling: the actual year %1 is invalid. Only %2 years are loaded from the climate database.").arg(mCurrentYear).arg(mLoadYears) ); |
||
332 | } |
||
721 | werner | 333 | if (logLevelDebug()) |
334 | qDebug() << "Climate: current year (randomized):" << mCurrentYear; |
||
348 | werner | 335 | } |
214 | werner | 336 | |
342 | werner | 337 | ClimateDay::co2 = GlobalSettings::instance()->settings().valueDouble("model.climate.co2concentration", 380.); |
754 | werner | 338 | if (logLevelDebug()) |
339 | qDebug() << "CO2 concentration" << ClimateDay::co2 << "ppm."; |
||
214 | werner | 340 | mBegin = mStore.begin() + mDayIndices[mCurrentYear*12]; |
341 | mEnd = mStore.begin() + mDayIndices[(mCurrentYear+1)*12];; // point to the 1.1. of the next year |
||
553 | werner | 342 | |
343 | // some aggregates: |
||
344 | // calculate radiation sum of the year and monthly precipitation |
||
485 | werner | 345 | mAnnualRadiation = 0.; |
721 | werner | 346 | mMeanAnnualTemperature = 0.; |
347 | for (int i=0;i<12;i++) { |
||
348 | mPrecipitationMonth[i]=0.; |
||
349 | mTemperatureMonth[i]=0.; |
||
350 | } |
||
553 | werner | 351 | |
352 | for (const ClimateDay *d=begin();d!=end();++d) { |
||
485 | werner | 353 | mAnnualRadiation+=d->radiation; |
721 | werner | 354 | mMeanAnnualTemperature += d->temperature; |
553 | werner | 355 | mPrecipitationMonth[d->month-1]+= d->preciptitation; |
721 | werner | 356 | mTemperatureMonth[d->month-1] += d->temperature; |
553 | werner | 357 | } |
721 | werner | 358 | for (int i=0;i<12;++i) { |
359 | mTemperatureMonth[i] /= days(i); |
||
360 | } |
||
361 | mMeanAnnualTemperature /= daysOfYear(); |
||
485 | werner | 362 | |
363 | // calculate phenology |
||
214 | werner | 364 | for(int i=0;i<mPhenology.count(); ++i) |
365 | mPhenology[i].calculate(); |
||
193 | werner | 366 | } |
198 | werner | 367 | |
720 | werner | 368 | void Climate::climateCalculations(const ClimateDay &lastDay) |
198 | werner | 369 | { |
201 | werner | 370 | ClimateDay *c = mStore.begin(); |
371 | const double tau = Model::settings().temperatureTau; |
||
372 | // handle first day: use tissue temperature of the last day of the last year (if available) |
||
373 | if (lastDay.isValid()) |
||
374 | c->temp_delayed = lastDay.temp_delayed + 1./tau * (c->temperature - lastDay.temp_delayed); |
||
375 | else |
||
376 | c->temp_delayed = c->temperature; |
||
377 | c++; |
||
378 | while (c->isValid()) { |
||
1033 | werner | 379 | // first order dynamic delayed model (Maekela 2008) |
201 | werner | 380 | c->temp_delayed=(c-1)->temp_delayed + 1./tau * (c->temperature - (c-1)->temp_delayed); |
381 | ++c; |
||
382 | } |
||
198 | werner | 383 | |
384 | } |
||
214 | werner | 385 | |
386 | |||
387 | void Climate::setupPhenology() |
||
388 | { |
||
389 | mPhenology.clear(); |
||
436 | werner | 390 | mPhenology.push_back(Phenology(this)); // id=0 |
214 | werner | 391 | XmlHelper xml(GlobalSettings::instance()->settings().node("model.species.phenology")); |
392 | int i=0; |
||
393 | do { |
||
394 | QDomElement n = xml.node(QString("type[%1]").arg(i)); |
||
395 | if (n.isNull()) |
||
396 | break; |
||
397 | i++; |
||
398 | int id; |
||
399 | id = n.attribute("id", "-1").toInt(); |
||
400 | if (id<0) throw IException(QString("Error setting up phenology: id invalid\ndump: %1").arg(xml.dump(""))); |
||
401 | xml.setCurrentNode(n); |
||
402 | Phenology item( id, |
||
403 | this, |
||
404 | xml.valueDouble(".vpdMin",0.5), // use relative access to node (".x") |
||
405 | xml.valueDouble(".vpdMax", 5), |
||
406 | xml.valueDouble(".dayLengthMin",10), |
||
407 | xml.valueDouble(".dayLengthMax",11), |
||
408 | xml.valueDouble(".tempMin", 2), |
||
409 | xml.valueDouble(".tempMax", 9) ); |
||
410 | |||
411 | mPhenology.push_back(item); |
||
412 | } while(true); |
||
413 | } |
||
414 | |||
415 | /** return the phenology of the group... */ |
||
238 | werner | 416 | const Phenology &Climate::phenology(const int phenologyGroup) const |
214 | werner | 417 | { |
418 | const Phenology &p = mPhenology.at(phenologyGroup); |
||
419 | if (p.id() == phenologyGroup) |
||
420 | return p; |
||
421 | // search... |
||
422 | for (int i=0;i<mPhenology.count();i++) |
||
423 | if (mPhenology[i].id()==phenologyGroup) |
||
424 | return mPhenology[i]; |
||
435 | werner | 425 | throw IException(QString("Error at SpeciesSet::phenology(): invalid group: %1").arg(phenologyGroup)); |
214 | werner | 426 | } |
436 | werner | 427 |