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