Kstars

artificialhorizoncomponent.cpp
1 /*
2  SPDX-FileCopyrightText: 2015 Jasem Mutlaq <[email protected]>
3 
4  SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "artificialhorizoncomponent.h"
8 
9 #include "greatcircle.h"
10 #include "kstarsdata.h"
11 #include "linelist.h"
12 #include "Options.h"
13 #include "skymap.h"
14 #include "skymapcomposite.h"
15 #include "skypainter.h"
16 #include "projections/projector.h"
17 
18 #define UNDEFINED_ALTITUDE -90
19 
20 ArtificialHorizonEntity::~ArtificialHorizonEntity()
21 {
22  clearList();
23 }
24 
25 QString ArtificialHorizonEntity::region() const
26 {
27  return m_Region;
28 }
29 
30 void ArtificialHorizonEntity::setRegion(const QString &Region)
31 {
32  m_Region = Region;
33 }
34 
35 bool ArtificialHorizonEntity::enabled() const
36 {
37  return m_Enabled;
38 }
39 
40 void ArtificialHorizonEntity::setEnabled(bool Enabled)
41 {
42  m_Enabled = Enabled;
43 }
44 
45 bool ArtificialHorizonEntity::ceiling() const
46 {
47  return m_Ceiling;
48 }
49 
50 void ArtificialHorizonEntity::setCeiling(bool value)
51 {
52  m_Ceiling = value;
53 }
54 
55 void ArtificialHorizonEntity::setList(const std::shared_ptr<LineList> &list)
56 {
57  m_List = list;
58 }
59 
60 std::shared_ptr<LineList> ArtificialHorizonEntity::list() const
61 {
62  return m_List;
63 }
64 
65 void ArtificialHorizonEntity::clearList()
66 {
67  m_List.reset();
68 }
69 
70 namespace
71 {
72 
73 // Returns true if angle is "in between" range1 and range2, two other angles,
74 // where in-between means "the short way".
75 bool inBetween(const dms &angle, const dms &range1, const dms &range2)
76 {
77  const double rangeDelta = fabs(range1.deltaAngle(range2).Degrees());
78  const double delta1 = fabs(range1.deltaAngle(angle).Degrees());
79  const double delta2 = fabs(range2.deltaAngle(angle).Degrees());
80  // The angle is between range1 and range2 if its two distances to each are both
81  // less than the range distance.
82  return delta1 <= rangeDelta && delta2 <= rangeDelta;
83 }
84 } // namespace
85 
86 double ArtificialHorizonEntity::altitudeConstraint(double azimuthDegrees, bool *constraintExists) const
87 {
88  *constraintExists = false;
89  if (m_List == nullptr)
90  return UNDEFINED_ALTITUDE;
91 
92  SkyList *points = m_List->points();
93  if (points == nullptr)
94  return UNDEFINED_ALTITUDE;
95 
96  double constraint = !m_Ceiling ? UNDEFINED_ALTITUDE : 90.0;
97  dms desiredAzimuth(azimuthDegrees);
98  dms lastAz;
99  double lastAlt = 0;
100  bool firstOne = true;
101  for (auto &p : *points)
102  {
103  const dms az = p->az();
104  const double alt = p->alt().Degrees();
105  if (qIsNaN(az.Degrees()) || qIsNaN(alt)) continue;
106  if (!firstOne && inBetween(desiredAzimuth, lastAz, az))
107  {
108  *constraintExists = true;
109  // If the input angle is in the interval between the last two points,
110  // interpolate the altitude constraint, and use that value.
111  // If there are other line segments which also contain the point,
112  // we use the max constraint. Convert to GreatCircle?
113  const double totalDelta = fabs(lastAz.deltaAngle(az).Degrees());
114  if (totalDelta <= 0)
115  {
116  if (!m_Ceiling)
117  constraint = std::max(constraint, alt);
118  else
119  constraint = std::min(constraint, alt);
120  }
121  else
122  {
123  const double deltaToLast = fabs(lastAz.deltaAngle(desiredAzimuth).Degrees());
124  const double weight = deltaToLast / totalDelta;
125  const double newConstraint = (1.0 - weight) * lastAlt + weight * alt;
126  if (!m_Ceiling)
127  constraint = std::max(constraint, newConstraint);
128  else
129  constraint = std::min(constraint, newConstraint);
130  }
131  }
132  firstOne = false;
133  lastAz = az;
134  lastAlt = alt;
135  }
136  return constraint;
137 }
138 
139 ArtificialHorizonComponent::ArtificialHorizonComponent(SkyComposite *parent)
140  : NoPrecessIndex(parent, i18n("Artificial Horizon"))
141 {
142  load();
143 }
144 
145 ArtificialHorizonComponent::~ArtificialHorizonComponent()
146 {
147 }
148 
149 ArtificialHorizon::~ArtificialHorizon()
150 {
151  qDeleteAll(m_HorizonList);
152  m_HorizonList.clear();
153 }
154 
155 void ArtificialHorizon::load(const QList<ArtificialHorizonEntity *> &list)
156 {
157  m_HorizonList = list;
158  resetPrecomputeConstraints();
159  checkForCeilings();
160 }
161 
162 bool ArtificialHorizonComponent::load()
163 {
165  KStarsData::Instance()->userdb()->GetAllHorizons(list);
166  horizon.load(list);
167 
168  foreach (ArtificialHorizonEntity *horizon, *horizon.horizonList())
169  appendLine(horizon->list());
170 
171  return true;
172 }
173 
174 void ArtificialHorizonComponent::save()
175 {
176  KStarsData::Instance()->userdb()->DeleteAllHorizons();
177 
178  foreach (ArtificialHorizonEntity *horizon, *horizon.horizonList())
179  KStarsData::Instance()->userdb()->AddHorizon(horizon);
180 }
181 
182 bool ArtificialHorizonComponent::selected()
183 {
184  return Options::showGround();
185 }
186 
187 void ArtificialHorizonComponent::preDraw(SkyPainter *skyp)
188 {
189  QColor color(KStarsData::Instance()->colorScheme()->colorNamed("ArtificialHorizonColor"));
190  color.setAlpha(40);
191  skyp->setBrush(QBrush(color));
192  skyp->setPen(Qt::NoPen);
193 }
194 
195 namespace
196 {
197 
198 // Returns an equivalent degrees in the range 0 <= 0 < 360
199 double normalizeDegrees(double degrees)
200 {
201  while (degrees < 0)
202  degrees += 360;
203  while (degrees >= 360.0)
204  degrees -= 360.0;
205  return degrees;
206 }
207 
208 // Draws a "round polygon", sampling a circle every 45 degrees, with the given radius,
209 // centered on the SkyPoint.
210 void drawHorizonPoint(const SkyPoint &pt, double radius, SkyPainter *painter)
211 
212 {
213  LineList region;
214  double az = pt.az().Degrees(), alt = pt.alt().Degrees();
215 
216  for (double angle = 0; angle < 360; angle += 45)
217  {
218  double radians = angle * 2 * M_PI / 360.0;
219  double az1 = az + radius * cos(radians);
220  double alt1 = alt + radius * sin(radians);
221  std::shared_ptr<SkyPoint> sp(new SkyPoint());
222  sp->setAz(az1);
223  sp->setAlt(alt1);
224  sp->HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
225  region.append(sp);
226  }
227  // Repeat the first point.
228  double az1 = az + radius * cos(0);
229  double alt1 = alt + radius * sin(0);
230  std::shared_ptr<SkyPoint> sp(new SkyPoint());
231  sp->setAz(az1);
232  sp->setAlt(alt1);
233  sp->HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
234  region.append(sp);
235 
236  painter->drawSkyPolygon(&region, false);
237 }
238 
239 // Draws a series of points whose coordinates are given by the LineList.
240 void drawHorizonPoints(LineList *lineList, SkyPainter *painter)
241 {
242  const SkyList &points = *(lineList->points());
243  for (int i = 0; i < points.size(); ++i)
244  {
245  const SkyPoint &pt = *points[i];
246  if (qIsNaN(pt.az().Degrees()) || qIsNaN(pt.alt().Degrees()))
247  continue;
248  drawHorizonPoint(pt, .5, painter);
249  }
250 }
251 
252 // Draws a points that is larger than the one drawn by drawHorizonPoint().
253 // The point's coordinates are the ith (index) point in the LineList.
254 void drawSelectedPoint(LineList *lineList, int index, SkyPainter *painter)
255 {
256  if (index >= 0 && index < lineList->points()->size())
257  {
258  const SkyList &points = *(lineList->points());
259  const SkyPoint &pt = *points[index];
260  if (qIsNaN(pt.az().Degrees()) || qIsNaN(pt.alt().Degrees()))
261  return;
262  drawHorizonPoint(pt, 1.0, painter);
263  }
264 }
265 
266 // This creates a set of connected line segments from az1,alt1 to az2,alt2, sampling
267 // points on the great circle between az1,alt1 and az2,alt2 every 2 degrees or so.
268 // The errors would be obvious for longer lines if we just drew a standard line.
269 // If testing is true, HorizontalToEquatorial is not called.
270 void appendGreatCirclePoints(double az1, double alt1, double az2, double alt2, LineList *region, bool testing)
271 {
272  constexpr double sampling = 2.0; // degrees
273  const double maxAngleDiff = std::max(fabs(az1 - az2), fabs(alt1 - alt2));
274  const int numSamples = maxAngleDiff / sampling;
275 
276  // Hy 9/25/22: These 4 lines cause rendering issues in equatorial mode (horizon mode is ok).
277  // Not sure why--though I suspect the initial conditions computed in drawPolygons().
278  // Without them there are some jagged lines near the horizon, but much better than with them.
279  // std::shared_ptr<SkyPoint> sp0(new SkyPoint());
280  // sp0->setAz(az1);
281  // sp0->setAlt(alt1);
282  // region->append(sp0);
283 
284  if (numSamples > 1)
285  {
286  GreatCircle gc(az1, alt1, az2, alt2);
287  for (int i = 1; i < numSamples; ++i)
288  {
289  const double fraction = i / static_cast<double>(numSamples);
290  double az, alt;
291  gc.waypoint(fraction, &az, &alt);
292  std::shared_ptr<SkyPoint> sp(new SkyPoint());
293  sp->setAz(az);
294  sp->setAlt(alt);
295  if (!testing)
296  sp->HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
297  region->append(sp);
298  }
299  }
300  std::shared_ptr<SkyPoint> sp(new SkyPoint());
301  sp->setAz(az2);
302  sp->setAlt(alt2);
303  // Is HorizontalToEquatorial necessary in any case?
304  if (!testing)
305  sp->HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
306  region->append(sp);
307 }
308 
309 } // namespace
310 
311 // Draws a polygon, where one of the sides is az1,alt1 --> az2,alt2 (except that's implemented as series
312 // of connected line segments along a great circle).
313 // It figures out the opposite side depending on the type of the constraint for this entity
314 // (horizon line or ceiling) and the other contraints that are enabled.
315 bool ArtificialHorizon::computePolygon(int entity, double az1, double alt1, double az2, double alt2,
316  double sampling, LineList *region)
317 {
318  const bool ceiling = horizonList()->at(entity)->ceiling();
319  const ArtificialHorizonEntity *thisOne = horizonList()->at(entity);
320  double alt1b = 0, alt2b = 0;
321  bool exists = false;
322  LineList left, top, right, bottom;
323 
324  double lastAz = az1;
325  double lastAlt = alt1;
326  const double azRange = az2 - az1, altRange = alt2 - alt1;
327 
328  if (az1 >= az2)
329  return false;
330 
331  if (az1 + sampling > az2)
332  sampling = (az2 - az1) - 1e-6;
333 
334  for (double az = az1 + sampling; az <= az2; az += sampling)
335  {
336  // Put it at the end, if we're close.
337  if (az + sampling > az2)
338  az = az2;
339 
340  double alt = alt1 + altRange * (az - az1) / azRange;
341  double alt1b = 0, alt2b = 0;
342 
343  if (!ceiling)
344  {
345  // For standard horizon lines, the polygon is drawn down to the next lower-altitude
346  // enabled line, or to the horizon if a lower line doesn't exist.
347  const ArtificialHorizonEntity *constraint = getConstraintBelow(lastAz, lastAlt, thisOne);
348  if (constraint != nullptr)
349  {
350  double altTemp = constraint->altitudeConstraint(lastAz, &exists);
351  if (exists)
352  alt1b = altTemp;
353  }
354  constraint = getConstraintBelow(az, alt, thisOne);
355  if (constraint != nullptr)
356  {
357  double altTemp = constraint->altitudeConstraint(az, &exists);
358  if (exists)
359  alt2b = altTemp;
360  }
361  appendGreatCirclePoints(lastAz, lastAlt, az, alt, &top, testing);
362  appendGreatCirclePoints(lastAz, alt1b, az, alt2b, &bottom, testing);
363  }
364  else
365  {
366  // For ceiling lines, the polygon is drawn up to the next higher-altitude enabled line
367  // but only if that line is another cieling, otherwise it not drawn at all (because that
368  // horizon line will do the drawing).
369  const ArtificialHorizonEntity *constraint = getConstraintAbove(lastAz, lastAlt, thisOne);
370  alt1b = 90;
371  alt2b = 90;
372  if (constraint != nullptr)
373  {
374  if (constraint->ceiling()) return false;
375  double altTemp = constraint->altitudeConstraint(lastAz, &exists);
376  if (exists) alt1b = altTemp;
377  }
378  constraint = getConstraintAbove(az, alt, thisOne);
379  if (constraint != nullptr)
380  {
381  if (constraint->ceiling()) return false;
382  double altTemp = constraint->altitudeConstraint(az, &exists);
383  if (exists) alt2b = altTemp;
384  }
385  appendGreatCirclePoints(lastAz, lastAlt, az, alt, &top, testing);
386  // Note that "bottom" for a ceiling is above.
387  appendGreatCirclePoints(lastAz, alt1b, az, alt2b, &bottom, testing);
388  }
389  lastAz = az;
390  lastAlt = alt;
391  }
392 
393  if (!ceiling)
394  {
395  // For standard horizon lines, the polygon is drawn down to the next lower-altitude
396  // enabled line, or to the horizon if a lower line doesn't exist.
397  const ArtificialHorizonEntity *constraint = getConstraintBelow(az1, alt1, thisOne);
398  if (constraint != nullptr)
399  {
400  double altTemp = constraint->altitudeConstraint(az1, &exists);
401  if (exists)
402  alt1b = altTemp;
403  }
404  appendGreatCirclePoints(az1, alt1b, az1, alt1, &left, testing);
405 
406  const ArtificialHorizonEntity *constraint2 = getConstraintBelow(az2, alt2, thisOne);
407  if (constraint2 != nullptr)
408  {
409  double altTemp = constraint2->altitudeConstraint(az2, &exists);
410  if (exists)
411  alt2b = altTemp;
412  }
413  appendGreatCirclePoints(az2, alt2, az2, alt2b, &right, testing);
414  }
415  else
416  {
417  // For ceiling lines, the polygon is drawn up to the next higher-altitude enabled line
418  // but only if that line is another cieling, otherwise it not drawn at all (because that
419  // horizon line will do the drawing).
420  const ArtificialHorizonEntity *constraint = getConstraintAbove(az1, alt1, thisOne);
421  alt1b = 90;
422  alt2b = 90;
423  if (constraint != nullptr)
424  {
425  if (!constraint->ceiling()) return false;
426  double altTemp = constraint->altitudeConstraint(az1, &exists);
427  if (exists) alt1b = altTemp;
428  }
429  appendGreatCirclePoints(az1, alt1b, az1, alt1, &left, testing);
430 
431  const ArtificialHorizonEntity *constraint2 = getConstraintAbove(az2, alt2, thisOne);
432  if (constraint2 != nullptr)
433  {
434  if (!constraint2->ceiling()) return false;
435  double altTemp = constraint2->altitudeConstraint(az2, &exists);
436  if (exists) alt2b = altTemp;
437  }
438  appendGreatCirclePoints(az2, alt2, az2, alt2b, &right, testing);
439  }
440 
441  // Now we have all the sides: left, top, right, bottom, but the order of bottom is reversed.
442  // Make a polygon with all the points.
443  for (const auto &p : * (left.points()))
444  region->append(p);
445  for (const auto &p : * (top.points()))
446  region->append(p);
447  for (const auto &p : * (right.points()))
448  region->append(p);
449  for (int i = bottom.points()->size() - 1; i >= 0; i--)
450  region->append(bottom.points()->at(i));
451 
452  return true;
453 }
454 
455 // Draws a series of polygons of width in azimuth of "sampling degrees".
456 // Drawing a single polygon would have "great-circle issues". This looks a lot better.
457 // Assumes az1 and az2 in range 0-360 and az1 < az2.
458 // regions is only not nullptr during testing. In this wasy we can test
459 // whether the appropriate regions are drawn.
460 void ArtificialHorizon::drawSampledPolygons(int entity, double az1, double alt1, double az2, double alt2,
461  double sampling, SkyPainter *painter, QList<LineList> *regions)
462 {
463  if (az1 > az2)
464  {
465  // Should not generally happen. Possibility e.g. az 0 -> 0.01 in a wrap around.
466  // OK to ignore.
467  return;
468  }
469 
470  LineList region;
471  if (computePolygon(entity, az1, alt1, az2, alt2, sampling, &region))
472  {
473  if (painter != nullptr)
474  painter->drawSkyPolygon(&region, false);
475  if (regions != nullptr)
476  regions->append(region);
477  }
478 }
479 
480 // This draws a series of polygons that fill the area that the horizon entity with index "entity"
481 // is responsible for. If that is a horizon line, it draws it down to the horizon, or to the next
482 // lower line. It draws the polygons one pair of points at a time, and deals with complications
483 // of when the azimuth angle wraps around 360 degrees.
484 void ArtificialHorizon::drawPolygons(int entity, SkyPainter *painter, QList<LineList> *regions)
485 {
486  const ArtificialHorizonEntity &ah = *(horizonList()->at(entity));
487  const SkyList &points = *(ah.list()->points());
488 
489  // The skylist shouldn't contain NaN values, but, it has in the past,
490  // and, to be cautious, this checks for them and removes points with NaNs.
491  int start = 0;
492  for (; start < points.size(); ++start)
493  {
494  const SkyPoint &p = *points[start];
495  if (!qIsNaN(p.az().Degrees()) && !qIsNaN(p.alt().Degrees()))
496  break;
497  }
498 
499  for (int i = start + 1; i < points.size(); ++i)
500  {
501  const SkyPoint &p2 = *points[i];
502  if (qIsNaN(p2.az().Degrees()) || qIsNaN(p2.alt().Degrees()))
503  continue;
504  const SkyPoint &p1 = *points[start];
505  start = i;
506 
507  const double az1 = normalizeDegrees(p1.az().Degrees());
508  const double az2 = normalizeDegrees(p2.az().Degrees());
509 
510  double minAz, maxAz, minAzAlt, maxAzAlt;
511  if (az1 < az2)
512  {
513  minAz = az1;
514  minAzAlt = p1.alt().Degrees();
515  maxAz = az2;
516  maxAzAlt = p2.alt().Degrees();
517  }
518  else
519  {
520  minAz = az2;
521  minAzAlt = p2.alt().Degrees();
522  maxAz = az1;
523  maxAzAlt = p1.alt().Degrees();
524  }
525  const bool wrapAround = !inBetween(dms((minAz + maxAz) / 2.0), dms(minAz), dms(maxAz));
526  constexpr double sampling = 1.0; // Draw a polygon for every degree in Azimuth
527  if (wrapAround)
528  {
529  // We've detected that the line segment crosses 0 degrees.
530  // Draw one polygon on one side of 0 degrees, and another on the other side.
531  // Compute the altitude at wrap-around.
532  const double fraction = fabs(dms(360.0).deltaAngle(dms(maxAz)).Degrees() /
533  p1.az().deltaAngle(p2.az()).Degrees());
534  const double midAlt = minAzAlt + fraction * (maxAzAlt - minAzAlt);
535  // Draw polygons form maxAz upto 0 degrees, then again from 0 to minAz.
536  drawSampledPolygons(entity, maxAz, maxAzAlt, 360, midAlt, sampling, painter, regions);
537  drawSampledPolygons(entity, 0, midAlt, minAz, minAzAlt, sampling, painter, regions);
538  }
539  else
540  {
541  // Draw the polygons without wraparound
542  drawSampledPolygons(entity, minAz, minAzAlt, maxAz, maxAzAlt, sampling, painter, regions);
543  }
544  }
545 }
546 
547 void ArtificialHorizon::drawPolygons(SkyPainter *painter, QList<LineList> *regions)
548 {
549  for (int i = 0; i < horizonList()->size(); i++)
550  {
551  if (enabled(i))
552  drawPolygons(i, painter, regions);
553  }
554 }
555 
556 void ArtificialHorizonComponent::draw(SkyPainter *skyp)
557 {
558  if (!selected())
559  return;
560 
561  if (livePreview.get())
562  {
563  if ((livePreview->points() != nullptr) && (livePreview->points()->size() > 0))
564  {
565  // Draws a series of line segments, overlayed by the vertices.
566  // One vertex (the current selection) is emphasized.
567  skyp->setPen(QPen(Qt::white, 2));
568  skyp->drawSkyPolyline(livePreview.get());
569  skyp->setBrush(QBrush(Qt::yellow));
570  drawSelectedPoint(livePreview.get(), selectedPreviewPoint, skyp);
571  skyp->setBrush(QBrush(Qt::red));
572  drawHorizonPoints(livePreview.get(), skyp);
573  }
574  }
575 
576  preDraw(skyp);
577 
578  QList<LineList> regions;
579  horizon.drawPolygons(skyp, &regions);
580 }
581 
582 bool ArtificialHorizon::enabled(int i) const
583 {
584  return m_HorizonList.at(i)->enabled();
585 }
586 
587 ArtificialHorizonEntity *ArtificialHorizon::findRegion(const QString &regionName)
588 {
589  ArtificialHorizonEntity *regionHorizon = nullptr;
590 
591  foreach (ArtificialHorizonEntity *horizon, m_HorizonList)
592  {
593  if (horizon->region() == regionName)
594  {
595  regionHorizon = horizon;
596  break;
597  }
598  }
599 
600  return regionHorizon;
601 }
602 
603 void ArtificialHorizon::removeRegion(const QString &regionName, bool lineOnly)
604 {
605  ArtificialHorizonEntity *regionHorizon = findRegion(regionName);
606 
607  if (regionHorizon == nullptr)
608  return;
609 
610  if (lineOnly)
611  regionHorizon->clearList();
612  else
613  {
614  m_HorizonList.removeOne(regionHorizon);
615  delete (regionHorizon);
616  }
617  resetPrecomputeConstraints();
618  checkForCeilings();
619 }
620 
621 void ArtificialHorizonComponent::removeRegion(const QString &regionName, bool lineOnly)
622 {
623  ArtificialHorizonEntity *regionHorizon = horizon.findRegion(regionName);
624  if (regionHorizon != nullptr && regionHorizon->list())
625  removeLine(regionHorizon->list());
626  horizon.removeRegion(regionName, lineOnly);
627 }
628 
629 void ArtificialHorizon::checkForCeilings()
630 {
631  noCeilingConstraints = true;
632  for (const auto &r : m_HorizonList)
633  {
634  if (r->ceiling() && r->enabled())
635  {
636  noCeilingConstraints = false;
637  break;
638  }
639  }
640 }
641 
642 void ArtificialHorizon::addRegion(const QString &regionName, bool enabled, const std::shared_ptr<LineList> &list,
643  bool ceiling)
644 {
645  ArtificialHorizonEntity *horizon = new ArtificialHorizonEntity;
646 
647  horizon->setRegion(regionName);
648  horizon->setEnabled(enabled);
649  horizon->setCeiling(ceiling);
650  horizon->setList(list);
651 
652  m_HorizonList.append(horizon);
653  resetPrecomputeConstraints();
654  checkForCeilings();
655 }
656 
657 void ArtificialHorizonComponent::addRegion(const QString &regionName, bool enabled, const std::shared_ptr<LineList> &list,
658  bool ceiling)
659 {
660  horizon.addRegion(regionName, enabled, list, ceiling);
661  appendLine(list);
662 }
663 
664 bool ArtificialHorizon::altitudeConstraintsExist() const
665 {
666  foreach (ArtificialHorizonEntity *horizon, m_HorizonList)
667  {
668  if (horizon->enabled())
669  return true;
670  }
671  return false;
672 }
673 
674 const ArtificialHorizonEntity *ArtificialHorizon::getConstraintAbove(double azimuthDegrees, double altitudeDegrees,
675  const ArtificialHorizonEntity *ignore) const
676 {
677  double closestAbove = 1e6;
678  const ArtificialHorizonEntity *entity = nullptr;
679 
680  foreach (ArtificialHorizonEntity *horizon, m_HorizonList)
681  {
682  if (!horizon->enabled()) continue;
683  if (horizon == ignore) continue;
684  bool constraintExists = false;
685  double constraint = horizon->altitudeConstraint(azimuthDegrees, &constraintExists);
686  // This horizon doesn't constrain this azimuth.
687  if (!constraintExists) continue;
688 
689  double altitudeDiff = constraint - altitudeDegrees;
690  if (altitudeDiff > 0 && constraint < closestAbove)
691  {
692  closestAbove = constraint;
693  entity = horizon;
694  }
695  }
696  return entity;
697 }
698 
699 // Estimate the horizon contraint to .1 degrees.
700 // This significantly speeds up computation.
701 constexpr int PRECOMPUTED_RESOLUTION = 10;
702 
703 double ArtificialHorizon::altitudeConstraint(double azimuthDegrees) const
704 {
705  if (precomputedConstraints.size() != 360 * PRECOMPUTED_RESOLUTION)
706  precomputeConstraints();
707  return precomputedConstraint(azimuthDegrees);
708 }
709 
710 double ArtificialHorizon::altitudeConstraintInternal(double azimuthDegrees) const
711 {
712  const ArtificialHorizonEntity *horizonBelow = getConstraintBelow(azimuthDegrees, 90.0, nullptr);
713  if (horizonBelow == nullptr)
714  return UNDEFINED_ALTITUDE;
715  bool ignore = false;
716  return horizonBelow->altitudeConstraint(azimuthDegrees, &ignore);
717 }
718 
719 // Quantize the constraints to within .1 degrees (so there are 360*10=3600
720 // precomputed values).
721 void ArtificialHorizon::precomputeConstraints() const
722 {
723  precomputedConstraints.clear();
724  precomputedConstraints.fill(0, 360 * PRECOMPUTED_RESOLUTION);
725  for (int i = 0; i < 360 * PRECOMPUTED_RESOLUTION; ++i)
726  {
727  const double az = i / static_cast<double>(PRECOMPUTED_RESOLUTION);
728  precomputedConstraints[i] = altitudeConstraintInternal(az);
729  }
730 }
731 
732 void ArtificialHorizon::resetPrecomputeConstraints() const
733 {
734  precomputedConstraints.clear();
735 }
736 
737 double ArtificialHorizon::precomputedConstraint(double azimuth) const
738 {
739  constexpr int maxval = 360 * PRECOMPUTED_RESOLUTION;
740  int index = azimuth * PRECOMPUTED_RESOLUTION + 0.5;
741  if (index == maxval)
742  index = 0;
743  if (index < 0 || index >= precomputedConstraints.size())
744  return UNDEFINED_ALTITUDE;
745  return precomputedConstraints[index];
746 }
747 
748 const ArtificialHorizonEntity *ArtificialHorizon::getConstraintBelow(double azimuthDegrees, double altitudeDegrees,
749  const ArtificialHorizonEntity *ignore) const
750 {
751  double closestBelow = -1e6;
752  const ArtificialHorizonEntity *entity = nullptr;
753 
754  foreach (ArtificialHorizonEntity *horizon, m_HorizonList)
755  {
756  if (!horizon->enabled()) continue;
757  if (horizon == ignore) continue;
758  bool constraintExists = false;
759  double constraint = horizon->altitudeConstraint(azimuthDegrees, &constraintExists);
760  // This horizon doesn't constrain this azimuth.
761  if (!constraintExists) continue;
762 
763  double altitudeDiff = constraint - altitudeDegrees;
764  if (altitudeDiff < 0 && constraint > closestBelow)
765  {
766  closestBelow = constraint;
767  entity = horizon;
768  }
769  }
770  return entity;
771 }
772 
773 bool ArtificialHorizon::isAltitudeOK(double azimuthDegrees, double altitudeDegrees, QString *reason) const
774 {
775  if (noCeilingConstraints)
776  {
777  const double constraint = altitudeConstraint(azimuthDegrees);
778  if (altitudeDegrees >= constraint)
779  return true;
780  if (reason != nullptr)
781  *reason = QString("altitude %1 < horizon %2").arg(altitudeDegrees, 0, 'f', 1).arg(constraint, 0, 'f', 1);
782  return false;
783  }
784  else
785  return isVisible(azimuthDegrees, altitudeDegrees, reason);
786 }
787 
788 // An altitude is blocked (not visible) if either:
789 // - there are constraints above and the closest above constraint is not a ceiling, or
790 // - there are constraints below and the closest below constraint is a ceiling.
791 bool ArtificialHorizon::isVisible(double azimuthDegrees, double altitudeDegrees, QString *reason) const
792 {
793  const ArtificialHorizonEntity *above = getConstraintAbove(azimuthDegrees, altitudeDegrees);
794  if (above != nullptr && !above->ceiling())
795  {
796  if (reason != nullptr)
797  {
798  bool ignoreMe;
799  double constraint = above->altitudeConstraint(azimuthDegrees, &ignoreMe);
800  *reason = QString("altitude %1 < horizon %2").arg(altitudeDegrees, 0, 'f', 1).arg(constraint, 0, 'f', 1);
801  }
802  return false;
803  }
804  const ArtificialHorizonEntity *below = getConstraintBelow(azimuthDegrees, altitudeDegrees);
805  if (below != nullptr && below->ceiling())
806  {
807  if (reason != nullptr)
808  {
809  bool ignoreMe;
810  double constraint = below->altitudeConstraint(azimuthDegrees, &ignoreMe);
811  *reason = QString("altitude %1 > ceiling %2").arg(altitudeDegrees, 0, 'f', 1).arg(constraint, 0, 'f', 1);
812  }
813  return false;
814  }
815  return true;
816 }
void append(const T &value)
const dms & alt() const
Definition: skypoint.h:281
QTextStream & right(QTextStream &stream)
QVector< T > & fill(const T &value, int size)
Stores dms coordinates for a point in the sky. for converting between coordinate systems.
Definition: skypoint.h:44
QTextStream & left(QTextStream &stream)
virtual void setPen(const QPen &pen)=0
Set the pen of the painter.
A class to compute points along a great circle from one az/alt to another.
Definition: greatcircle.h:27
QAction * load(const QObject *recvr, const char *slot, QObject *parent)
KSUserDB * userdb()
Definition: kstarsdata.h:215
KIOFILEWIDGETS_EXPORT QStringList list(const QString &fileClass)
int size() const const
void clear()
QString i18n(const char *text, const TYPE &arg...)
bool AddHorizon(ArtificialHorizonEntity *horizon)
Adds a new artificial horizon row into the database.
Definition: ksuserdb.cpp:2281
const T & at(int i) const const
bool removeOne(const T &value)
const T & at(int i) const const
virtual void setBrush(const QBrush &brush)=0
Set the brush of the painter.
GeoCoordinates geo(const QVariant &location)
Draws things on the sky, without regard to backend.
Definition: skypainter.h:39
virtual void drawSkyPolygon(LineList *list, bool forceClip=true)=0
Draw a polygon in the sky.
SkyList * points()
return the list of points for iterating or appending (or whatever).
Definition: linelist.h:33
bool GetAllHorizons(QList< ArtificialHorizonEntity * > &horizonList)
Gets all the artificial horizon rows from the database.
Definition: ksuserdb.cpp:2190
An angle, stored as degrees, but expressible in many ways.
Definition: dms.h:37
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
const double & Degrees() const
Definition: dms.h:141
const dms deltaAngle(dms angle) const
deltaAngle Return the shortest difference (path) between this angle and the supplied angle.
Definition: dms.cpp:259
void clear()
int size() const const
virtual void drawSkyPolyline(LineList *list, SkipHashList *skipList=nullptr, LineListLabel *label=nullptr)=0
Draw a polyline in the sky.
bool DeleteAllHorizons()
Deletes all artificial horizon rows from the database.
Definition: ksuserdb.cpp:2250
const dms & az() const
Definition: skypoint.h:275
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Sat Sep 30 2023 04:02:41 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.