Kstars

calibrationprocess.cpp
1 #include "calibrationprocess.h"
2 
3 #include "gmath.h"
4 #include "ekos_guide_debug.h"
5 #include "gmath.h"
6 #include "Options.h"
7 
8 namespace Ekos
9 {
10 
11 QString stageString(Ekos::CalibrationProcess::CalibrationStage stage)
12 {
13  switch(stage)
14  {
15  case Ekos::CalibrationProcess::CAL_IDLE:
16  return ("CAL_IDLE");
17  case Ekos::CalibrationProcess::CAL_ERROR:
18  return("CAL_ERROR");
19  case Ekos::CalibrationProcess::CAL_CAPTURE_IMAGE:
20  return("CAL_CAPTURE_IMAGE");
21  case Ekos::CalibrationProcess::CAL_SELECT_STAR:
22  return("CAL_SELECT_STAR");
23  case Ekos::CalibrationProcess::CAL_START:
24  return("CAL_START");
25  case Ekos::CalibrationProcess::CAL_RA_INC:
26  return("CAL_RA_INC");
27  case Ekos::CalibrationProcess::CAL_RA_DEC:
28  return("CAL_RA_DEC");
29  case Ekos::CalibrationProcess::CAL_DEC_INC:
30  return("CAL_DEC_INC");
31  case Ekos::CalibrationProcess::CAL_DEC_DEC:
32  return("CAL_DEC_DEC");
33  case Ekos::CalibrationProcess::CAL_BACKLASH:
34  return("CAL_BACKLASH");
35  default:
36  return("???");
37  };
38 }
39 
40 CalibrationProcess::CalibrationProcess(double startX, double startY, bool raOnlyEnabled)
41 {
42  calibrationStage = CAL_START;
43  raOnly = raOnlyEnabled;
44  start_x1 = startX;
45  start_y1 = startY;
46 }
47 
48 void CalibrationProcess::useCalibration(Calibration *calibrationPtr)
49 {
50  calibration = calibrationPtr;
51  tempCalibration = *calibration;
52 }
53 
54 void CalibrationProcess::startup()
55 {
56  calibrationStage = CAL_START;
57 }
58 
59 void CalibrationProcess::setGuideLog(GuideLog *guideLogPtr)
60 {
61  guideLog = guideLogPtr;
62 }
63 
64 bool CalibrationProcess::inProgress() const
65 {
66  return calibrationStage > CAL_START;
67 }
68 
69 void CalibrationProcess::addCalibrationUpdate(
70  GuideInterface::CalibrationUpdateType type,
71  QString message, double x, double y)
72 {
73  updateType = type;
74  calibrationUpdate = message;
75  updateX = x;
76  updateY = y;
77 }
78 
79 void CalibrationProcess::getCalibrationUpdate(
80  GuideInterface::CalibrationUpdateType *type,
81  QString *message, double *x, double *y) const
82 {
83  *type = updateType;
84  *message = calibrationUpdate;
85  *x = updateX;
86  *y = updateY;
87 }
88 
89 QString CalibrationProcess::getLogStatus() const
90 {
91  return logString;
92 }
93 
94 void CalibrationProcess::addLogStatus(const QString &message)
95 {
96  logString = message;
97 }
98 
99 void CalibrationProcess::addPulse(GuideDirection dir, int msecs)
100 {
101  pulseDirection = dir;
102  pulseMsecs = msecs;
103 }
104 
105 void CalibrationProcess::getPulse(GuideDirection *dir, int *msecs) const
106 {
107  *dir = pulseDirection;
108  *msecs = pulseMsecs;
109 }
110 
111 void CalibrationProcess::addStatus(Ekos::GuideState s)
112 {
113  status = s;
114 }
115 
116 Ekos::GuideState CalibrationProcess::getStatus() const
117 {
118  return status;
119 }
120 
121 void CalibrationProcess::initializeIteration()
122 {
123  axisCalibrationComplete = false;
124 
125  logString.clear();
126 
127  calibrationUpdate.clear();
128  updateType = GuideInterface::CALIBRATION_MESSAGE_ONLY;
129  updateX = 0;
130  updateY = 0;
131 
132  addStatus(Ekos::GUIDE_CALIBRATING);
133 
134  pulseDirection = NO_DIR;
135  pulseMsecs = 0;
136 }
137 
138 void CalibrationProcess::iterate(double x, double y)
139 {
140  initializeIteration();
141  switch (calibrationStage)
142  {
143  case CAL_START:
144  startState();
145  break;
146  case CAL_RA_INC:
147  raOutState(x, y);
148  break;
149  case CAL_RA_DEC:
150  raInState(x, y);
151  break;
152  case CAL_BACKLASH:
153  decBacklashState(x, y);
154  break;
155  case CAL_DEC_INC:
156  decOutState(x, y);
157  break;
158  case CAL_DEC_DEC:
159  decInState(x, y);
160  break;
161  default:
162  break;
163  }
164 }
165 
166 void CalibrationProcess::startState()
167 {
168  maximumSteps = Options::autoModeIterations();
169  turn_back_time = maximumSteps * 7;
170 
171  ra_iterations = 0;
172  dec_iterations = 0;
173  backlash_iterations = 0;
174  ra_total_pulse = de_total_pulse = 0;
175 
176  addLogStatus(i18n("RA drifting forward..."));
177 
178  last_pulse = Options::calibrationPulseDuration();
179 
180  addCalibrationUpdate(GuideInterface::RA_OUT, i18n("Guide Star found."), 0, 0);
181 
182  qCDebug(KSTARS_EKOS_GUIDE) << "Auto Iteration #" << maximumSteps << "Default pulse:" << last_pulse;
183  qCDebug(KSTARS_EKOS_GUIDE) << "Start X1 " << start_x1 << " Start Y1 " << start_y1;
184 
185  last_x = start_x1;
186  last_y = start_x2;
187 
188  addPulse(RA_INC_DIR, last_pulse);
189 
190  ra_iterations++;
191 
192  calibrationStage = CAL_RA_INC;
193  if (guideLog)
194  guideLog->addCalibrationData(RA_INC_DIR, start_x1, start_y1, start_x1, start_y1);
195 }
196 
197 
198 void CalibrationProcess::raOutState(double cur_x, double cur_y)
199 {
200  addCalibrationUpdate(GuideInterface::RA_OUT, i18n("Calibrating RA Out"),
201  cur_x - start_x1, cur_y - start_y1);
202 
203  qCDebug(KSTARS_EKOS_GUIDE) << "Iteration #" << ra_iterations << ": STAR " << cur_x << "," << cur_y;
204  qCDebug(KSTARS_EKOS_GUIDE) << "Iteration " << ra_iterations << " Direction: RA_INC_DIR" << " Duration: "
205  << last_pulse << " ms.";
206 
207  if (guideLog)
208  guideLog->addCalibrationData(RA_INC_DIR, cur_x, cur_y, start_x1, start_y1);
209 
210  // Must pass at least 1.5 pixels to move on to the next stage.
211  // If we've moved 15 pixels, we can cut short the requested number of iterations.
212  const double xDrift = cur_x - start_x1;
213  const double yDrift = cur_y - start_y1;
214  if (((ra_iterations >= maximumSteps) ||
215  (std::hypot(xDrift, yDrift) > Options::calibrationMaxMove()))
216  && (fabs(xDrift) > 1.5 || fabs(yDrift) > 1.5))
217  {
218  ra_total_pulse += last_pulse;
219  calibrationStage = CAL_RA_DEC;
220 
221  end_x1 = cur_x;
222  end_y1 = cur_y;
223 
224  last_x = cur_x;
225  last_y = cur_y;
226 
227  qCDebug(KSTARS_EKOS_GUIDE) << "End X1 " << end_x1 << " End Y1 " << end_y1;
228 
229  // This temporary calibration is just used to help find our way back to the origin.
230  // total_pulse is not used, but valid.
231  tempCalibration.calculate1D(start_x1, start_y1, end_x1, end_y1, ra_total_pulse);
232 
233  ra_distance = 0;
234  backlash = 0;
235 
236  addPulse(RA_DEC_DIR, last_pulse);
237  ra_iterations++;
238 
239  addLogStatus(i18n("RA drifting reverse..."));
240  if (guideLog)
241  guideLog->endCalibrationSection(RA_INC_DIR, tempCalibration.getAngle());
242  }
243  else if (ra_iterations > turn_back_time)
244  {
245  addLogStatus(i18n("Calibration rejected. Star drift is too short. Check for mount, cable, or backlash problems."));
246  calibrationStage = CAL_ERROR;
247  addStatus(Ekos::GUIDE_CALIBRATION_ERROR);
248  addCalibrationUpdate(GuideInterface::CALIBRATION_MESSAGE_ONLY, i18n("Calibration Failed: Drift too short."));
249  if (guideLog)
250  guideLog->endCalibration(0, 0);
251  }
252  else
253  {
254  // Aggressive pulse in case we're going slow
255  if (fabs(cur_x - last_x) < 0.5 && fabs(cur_y - last_y) < 0.5)
256  {
257  // 200%
258  last_pulse = Options::calibrationPulseDuration() * 2;
259  }
260  else
261  {
262  ra_total_pulse += last_pulse;
263  last_pulse = Options::calibrationPulseDuration();
264  }
265 
266  last_x = cur_x;
267  last_y = cur_y;
268 
269  addPulse(RA_INC_DIR, last_pulse);
270 
271  ra_iterations++;
272  }
273 }
274 
275 void CalibrationProcess::raInState(double cur_x, double cur_y)
276 {
277  addCalibrationUpdate(GuideInterface::RA_IN, i18n("Calibrating RA In"),
278  cur_x - start_x1, cur_y - start_y1);
279  qCDebug(KSTARS_EKOS_GUIDE) << "Iteration #" << ra_iterations << ": STAR " << cur_x << "," << cur_y;
280  qCDebug(KSTARS_EKOS_GUIDE) << "Iteration " << ra_iterations << " Direction: RA_DEC_DIR" << " Duration: "
281  << last_pulse << " ms.";
282 
283  double driftRA, driftDEC;
284  tempCalibration.computeDrift(GuiderUtils::Vector(cur_x, cur_y, 0), GuiderUtils::Vector(start_x1, start_y1, 0),
285  &driftRA, &driftDEC);
286 
287  qCDebug(KSTARS_EKOS_GUIDE) << "Star x pos is " << driftRA << " from original point.";
288 
289  if (ra_distance == 0.0)
290  ra_distance = driftRA;
291 
292  if (guideLog)
293  guideLog->addCalibrationData(RA_DEC_DIR, cur_x, cur_y, start_x1, start_y1);
294 
295  // start point reached... so exit
296  if (driftRA < 1.5)
297  {
298  last_pulse = Options::calibrationPulseDuration();
299  axisCalibrationComplete = true;
300  }
301  // If we'not moving much, try increasing pulse to 200% to clear any backlash
302  // Also increase pulse width if we are going FARTHER and not back to our original position
303  else if ( (fabs(cur_x - last_x) < 0.5 && fabs(cur_y - last_y) < 0.5)
304  || driftRA > ra_distance)
305  {
306  backlash++;
307 
308  // Increase pulse to 200% after we tried to fight against backlash 2 times at least
309  if (backlash > 2)
310  last_pulse = Options::calibrationPulseDuration() * 2;
311  else
312  last_pulse = Options::calibrationPulseDuration();
313  }
314  else
315  {
316  //ra_total_pulse += last_pulse;
317  last_pulse = Options::calibrationPulseDuration();
318  backlash = 0;
319  }
320  last_x = cur_x;
321  last_y = cur_y;
322 
323  if (axisCalibrationComplete == false)
324  {
325  if (ra_iterations < turn_back_time)
326  {
327  addPulse(RA_DEC_DIR, last_pulse);
328  ra_iterations++;
329  return;
330  }
331 
332  calibrationStage = CAL_ERROR;
333  addStatus(Ekos::GUIDE_CALIBRATION_ERROR);
334  addCalibrationUpdate(GuideInterface::CALIBRATION_MESSAGE_ONLY, i18n("Calibration Failed: couldn't reach start."));
335  addLogStatus(i18np("Guide RA: Scope cannot reach the start point after %1 iteration. Possible mount or "
336  "backlash problems...",
337  "GUIDE_RA: Scope cannot reach the start point after %1 iterations. Possible mount or "
338  "backlash problems...",
339  ra_iterations));
340  return;
341  }
342 
343  if (raOnly == false)
344  {
345  if (Options::guideCalibrationBacklash())
346  {
347  calibrationStage = CAL_BACKLASH;
348  last_x = cur_x;
349  last_y = cur_y;
350  start_backlash_x = cur_x;
351  start_backlash_y = cur_y;
352  addPulse(DEC_INC_DIR, Options::calibrationPulseDuration());
353  backlash_iterations++;
354  addLogStatus(i18n("DEC backlash..."));
355  }
356  else
357  {
358  calibrationStage = CAL_DEC_INC;
359  start_x2 = cur_x;
360  start_y2 = cur_y;
361  last_x = cur_x;
362  last_y = cur_y;
363 
364  qCDebug(KSTARS_EKOS_GUIDE) << "Start X2 " << start_x2 << " start Y2 " << start_y2;
365  addPulse(DEC_INC_DIR, Options::calibrationPulseDuration());
366  dec_iterations++;
367  addLogStatus(i18n("DEC drifting forward..."));
368  }
369  return;
370  }
371  // calc orientation
372  if (calibration->calculate1D(start_x1, start_y1, end_x1, end_y1, ra_total_pulse))
373  {
374  calibration->save();
375  calibrationStage = CAL_IDLE;
376  addStatus(Ekos::GUIDE_CALIBRATION_SUCCESS);
377  // Below converts from ms/arcsecond to arcseconds/second.
378  if (guideLog)
379  guideLog->endCalibration(1000.0 / calibration->raPulseMillisecondsPerArcsecond(), 0);
380  }
381  else
382  {
383  addLogStatus(i18n("Calibration rejected. Star drift is too short. Check for mount, cable, or backlash problems."));
384  calibrationStage = CAL_ERROR;
385  addStatus(Ekos::GUIDE_CALIBRATION_ERROR);
386  addCalibrationUpdate(GuideInterface::CALIBRATION_MESSAGE_ONLY, i18n("Calibration Failed: drift too short."));
387  if (guideLog)
388  guideLog->endCalibration(0, 0);
389  }
390 }
391 
392 void CalibrationProcess::decBacklashState(double cur_x, double cur_y)
393 {
394  double driftRA, driftDEC;
395  tempCalibration.computeDrift(
396  GuiderUtils::Vector(cur_x, cur_y, 0),
397  GuiderUtils::Vector(start_backlash_x, start_backlash_y, 0),
398  &driftRA, &driftDEC);
399 
400  // Exit the backlash phase either after 5 pulses, or after we've moved sufficiently in the
401  // DEC direction.
402  constexpr int MIN_DEC_BACKLASH_MOVE_PIXELS = 3;
403  if ((++backlash_iterations >= 5) ||
404  (fabs(driftDEC) > MIN_DEC_BACKLASH_MOVE_PIXELS))
405  {
406  addCalibrationUpdate(GuideInterface::BACKLASH, i18n("Calibrating DEC Backlash"),
407  cur_x - start_x1, cur_y - start_y1);
408  qCDebug(KSTARS_EKOS_GUIDE) << QString("Stopping dec backlash caibration after %1 iterations, offset %2")
409  .arg(backlash_iterations - 1)
410  .arg(driftDEC, 4, 'f', 2);
411  calibrationStage = CAL_DEC_INC;
412  start_x2 = cur_x;
413  start_y2 = cur_y;
414  last_x = cur_x;
415  last_y = cur_y;
416 
417  qCDebug(KSTARS_EKOS_GUIDE) << "Start X2 " << start_x2 << " start Y2 " << start_y2;
418  addPulse(DEC_INC_DIR, Options::calibrationPulseDuration());
419  dec_iterations++;
420  addLogStatus(i18n("DEC drifting forward..."));
421  return;
422  }
423  addCalibrationUpdate(GuideInterface::BACKLASH, i18n("Calibrating DEC Backlash"),
424  cur_x - start_x1, cur_y - start_y1);
425  qCDebug(KSTARS_EKOS_GUIDE) << "Backlash iter" << backlash_iterations << "position" << cur_x << cur_y;
426  addPulse(DEC_INC_DIR, Options::calibrationPulseDuration());
427 }
428 
429 
430 void CalibrationProcess::decOutState(double cur_x, double cur_y)
431 {
432  addCalibrationUpdate(GuideInterface::DEC_OUT, i18n("Calibrating DEC Out"),
433  cur_x - start_x1, cur_y - start_y1);
434 
435  qCDebug(KSTARS_EKOS_GUIDE) << "Iteration #" << dec_iterations << ": STAR " << cur_x << "," << cur_y;
436  qCDebug(KSTARS_EKOS_GUIDE) << "Iteration " << dec_iterations << " Direction: DEC_INC_DIR" <<
437  " Duration: " << last_pulse << " ms.";
438 
439  // Don't yet know how to tell NORTH vs SOUTH
440  if (guideLog)
441  guideLog->addCalibrationData(DEC_INC_DIR, cur_x, cur_y,
442  start_x2, start_y2);
443  const double xDrift = cur_x - start_x2;
444  const double yDrift = cur_y - start_y2;
445  if (((dec_iterations >= maximumSteps) ||
446  (std::hypot(xDrift, yDrift) > Options::calibrationMaxMove()))
447  && (fabs(xDrift) > 1.5 || fabs(yDrift) > 1.5))
448  {
449  calibrationStage = CAL_DEC_DEC;
450 
451  de_total_pulse += last_pulse;
452 
453  end_x2 = cur_x;
454  end_y2 = cur_y;
455 
456  last_x = cur_x;
457  last_y = cur_y;
458 
459  axisCalibrationComplete = false;
460 
461  qCDebug(KSTARS_EKOS_GUIDE) << "End X2 " << end_x2 << " End Y2 " << end_y2;
462 
463  tempCalibration.calculate1D(start_x2, start_y2, end_x2, end_y2, de_total_pulse);
464 
465  de_distance = 0;
466 
467  addPulse(DEC_DEC_DIR, last_pulse);
468  addLogStatus(i18n("DEC drifting reverse..."));
469  dec_iterations++;
470  if (guideLog)
471  guideLog->endCalibrationSection(DEC_INC_DIR, tempCalibration.getAngle());
472  }
473  else if (dec_iterations > turn_back_time)
474  {
475  calibrationStage = CAL_ERROR;
476 
477  addStatus(Ekos::GUIDE_CALIBRATION_ERROR);
478  addCalibrationUpdate(GuideInterface::CALIBRATION_MESSAGE_ONLY, i18n("Calibration Failed: couldn't reach start point."));
479  addLogStatus(i18np("Guide DEC: Scope cannot reach the start point after %1 iteration.\nPossible mount "
480  "or backlash problems...",
481  "GUIDE DEC: Scope cannot reach the start point after %1 iterations.\nPossible mount "
482  "or backlash problems...",
483  dec_iterations));
484 
485  if (guideLog)
486  guideLog->endCalibration(0, 0);
487  }
488  else
489  {
490  if (fabs(cur_x - last_x) < 0.5 && fabs(cur_y - last_y) < 0.5)
491  {
492  // Increase pulse by 200%
493  last_pulse = Options::calibrationPulseDuration() * 2;
494  }
495  else
496  {
497  de_total_pulse += last_pulse;
498  last_pulse = Options::calibrationPulseDuration();
499  }
500  last_x = cur_x;
501  last_y = cur_y;
502 
503  addPulse(DEC_INC_DIR, last_pulse);
504 
505  dec_iterations++;
506  }
507 }
508 
509 void CalibrationProcess::decInState(double cur_x, double cur_y)
510 {
511  addCalibrationUpdate(GuideInterface::DEC_IN, i18n("Calibrating DEC In"),
512  cur_x - start_x1, cur_y - start_y1);
513 
514  // Star position resulting from LAST guiding pulse to mount
515  qCDebug(KSTARS_EKOS_GUIDE) << "Iteration #" << dec_iterations << ": STAR " << cur_x << "," << cur_y;
516  qCDebug(KSTARS_EKOS_GUIDE) << "Iteration " << dec_iterations << " Direction: DEC_DEC_DIR" <<
517  " Duration: " << last_pulse << " ms.";
518 
519  // Note: the way this temp calibration was set up above, with the DEC drifts, the ra axis is really dec.
520  // This will help the dec find its way home. Could convert to a full RA/DEC calibration.
521  double driftRA, driftDEC;
522  tempCalibration.computeDrift(
523  GuiderUtils::Vector(cur_x, cur_y, 0),
524  GuiderUtils::Vector(start_x1, start_y1, 0),
525  &driftRA, &driftDEC);
526 
527  qCDebug(KSTARS_EKOS_GUIDE) << "Currently " << driftRA << driftDEC << " from original point.";
528 
529  // Keep track of distance
530  if (de_distance == 0.0)
531  de_distance = driftRA;
532 
533  if (guideLog)
534  guideLog->addCalibrationData(DEC_DEC_DIR, cur_x, cur_y, start_x2, start_y2);
535 
536  // start point reached... so exit
537  if (driftRA < 1.5)
538  {
539  last_pulse = Options::calibrationPulseDuration();
540  axisCalibrationComplete = true;
541  }
542  // Increase pulse if we're not moving much or if we are moving _away_ from target.
543  else if ( (fabs(cur_x - last_x) < 0.5 && fabs(cur_y - last_y) < 0.5)
544  || driftRA > de_distance)
545  {
546  // Increase pulse by 200%
547  last_pulse = Options::calibrationPulseDuration() * 2;
548  }
549  else
550  {
551  last_pulse = Options::calibrationPulseDuration();
552  }
553 
554  if (axisCalibrationComplete == false)
555  {
556  if (dec_iterations < turn_back_time)
557  {
558  addPulse(DEC_DEC_DIR, last_pulse);
559  dec_iterations++;
560  return;
561  }
562 
563  calibrationStage = CAL_ERROR;
564 
565  addStatus(Ekos::GUIDE_CALIBRATION_ERROR);
566  addCalibrationUpdate(GuideInterface::CALIBRATION_MESSAGE_ONLY, i18n("Calibration Failed: couldn't reach start point."));
567 
568  addLogStatus(i18np("Guide DEC: Scope cannot reach the start point after %1 iteration.\nPossible mount "
569  "or backlash problems...",
570  "Guide DEC: Scope cannot reach the start point after %1 iterations.\nPossible mount "
571  "or backlash problems...",
572  dec_iterations));
573  return;
574  }
575 
576  bool swap_dec = false;
577  // calc orientation
578  if (calibration->calculate2D(start_x1, start_y1, end_x1, end_y1, start_x2, start_y2, end_x2, end_y2,
579  &swap_dec, ra_total_pulse, de_total_pulse))
580  {
581  calibration->save();
582  calibrationStage = CAL_IDLE;
583  if (swap_dec)
584  addLogStatus(i18n("DEC swap enabled."));
585  else
586  addLogStatus(i18n("DEC swap disabled."));
587 
588  addStatus(Ekos::GUIDE_CALIBRATION_SUCCESS);
589 
590  addCalibrationUpdate(GuideInterface::CALIBRATION_MESSAGE_ONLY, i18n("Calibration Successful"));
591 
592  // Below converts from ms/arcsecond to arcseconds/second.
593  if (guideLog)
594  guideLog->endCalibration(
595  1000.0 / calibration->raPulseMillisecondsPerArcsecond(),
596  1000.0 / calibration->decPulseMillisecondsPerArcsecond());
597  return;
598  }
599  else
600  {
601  addLogStatus(i18n("Calibration rejected. Star drift is too short. Check for mount, cable, or backlash problems."));
602  addCalibrationUpdate(GuideInterface::CALIBRATION_MESSAGE_ONLY, i18n("Calibration Failed: drift too short."));
603  addStatus(Ekos::GUIDE_CALIBRATION_ERROR);
604  calibrationStage = CAL_ERROR;
605  if (guideLog)
606  guideLog->endCalibration(0, 0);
607  return;
608  }
609 }
610 
611 } // namespace Ekos
Ekos is an advanced Astrophotography tool for Linux. It is based on a modular extensible framework to...
Definition: align.cpp:70
QString i18n(const char *text, const TYPE &arg...)
NETWORKMANAGERQT_EXPORT NetworkManager::Status status()
QString i18np(const char *singular, const char *plural, const TYPE &arg...)
VehicleSection::Type type(QStringView coachNumber, QStringView coachClassification)
KIOFILEWIDGETS_EXPORT QString dir(const QString &fileClass)
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
QString message
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Mon Aug 8 2022 04:13:18 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.