1 /*
2     SPDX-FileCopyrightText: 2015 Jasem Mutlaq <mutlaqja@ikarustech.com>
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 
~ArtificialHorizonEntity()20 ArtificialHorizonEntity::~ArtificialHorizonEntity()
21 {
22     clearList();
23 }
24 
region() const25 QString ArtificialHorizonEntity::region() const
26 {
27     return m_Region;
28 }
29 
setRegion(const QString & Region)30 void ArtificialHorizonEntity::setRegion(const QString &Region)
31 {
32     m_Region = Region;
33 }
34 
enabled() const35 bool ArtificialHorizonEntity::enabled() const
36 {
37     return m_Enabled;
38 }
39 
setEnabled(bool Enabled)40 void ArtificialHorizonEntity::setEnabled(bool Enabled)
41 {
42     m_Enabled = Enabled;
43 }
44 
ceiling() const45 bool ArtificialHorizonEntity::ceiling() const
46 {
47     return m_Ceiling;
48 }
49 
setCeiling(bool value)50 void ArtificialHorizonEntity::setCeiling(bool value)
51 {
52     m_Ceiling = value;
53 }
54 
setList(const std::shared_ptr<LineList> & list)55 void ArtificialHorizonEntity::setList(const std::shared_ptr<LineList> &list)
56 {
57     m_List = list;
58 }
59 
list() const60 std::shared_ptr<LineList> ArtificialHorizonEntity::list() const
61 {
62     return m_List;
63 }
64 
clearList()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".
inBetween(const dms & angle,const dms & range1,const dms & range2)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 
altitudeConstraint(double azimuthDegrees,bool * constraintExists) const86 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 
ArtificialHorizonComponent(SkyComposite * parent)139 ArtificialHorizonComponent::ArtificialHorizonComponent(SkyComposite *parent)
140     : NoPrecessIndex(parent, i18n("Artificial Horizon"))
141 {
142     load();
143 }
144 
~ArtificialHorizonComponent()145 ArtificialHorizonComponent::~ArtificialHorizonComponent()
146 {
147 }
148 
~ArtificialHorizon()149 ArtificialHorizon::~ArtificialHorizon()
150 {
151     qDeleteAll(m_HorizonList);
152     m_HorizonList.clear();
153 }
154 
load(const QList<ArtificialHorizonEntity * > & list)155 void ArtificialHorizon::load(const QList<ArtificialHorizonEntity *> &list)
156 {
157     m_HorizonList = list;
158 }
159 
load()160 bool ArtificialHorizonComponent::load()
161 {
162     horizon.load(KStarsData::Instance()->userdb()->GetAllHorizons());
163 
164     foreach (ArtificialHorizonEntity *horizon, *horizon.horizonList())
165         appendLine(horizon->list());
166 
167     return true;
168 }
169 
save()170 void ArtificialHorizonComponent::save()
171 {
172     KStarsData::Instance()->userdb()->DeleteAllHorizons();
173 
174     foreach (ArtificialHorizonEntity *horizon, *horizon.horizonList())
175         KStarsData::Instance()->userdb()->AddHorizon(horizon);
176 }
177 
selected()178 bool ArtificialHorizonComponent::selected()
179 {
180     return Options::showGround();
181 }
182 
preDraw(SkyPainter * skyp)183 void ArtificialHorizonComponent::preDraw(SkyPainter *skyp)
184 {
185     QColor color(KStarsData::Instance()->colorScheme()->colorNamed("ArtificialHorizonColor"));
186     color.setAlpha(40);
187     skyp->setBrush(QBrush(color));
188     skyp->setPen(Qt::NoPen);
189 }
190 
191 namespace
192 {
193 
194 // Returns an equivalent degrees in the range 0 <= 0 < 360
normalizeDegrees(double degrees)195 double normalizeDegrees(double degrees)
196 {
197     while (degrees < 0)
198         degrees += 360;
199     while (degrees >= 360.0)
200         degrees -= 360.0;
201     return degrees;
202 }
203 
204 // Draws a "round polygon", sampling a circle every 45 degrees, with the given radius,
205 // centered on the SkyPoint.
drawHorizonPoint(const SkyPoint & pt,double radius,SkyPainter * painter)206 void drawHorizonPoint(const SkyPoint &pt, double radius, SkyPainter *painter)
207 
208 {
209     LineList region;
210     double az = pt.az().Degrees(), alt = pt.alt().Degrees();
211 
212     for (double angle = 0; angle < 360; angle += 45)
213     {
214         double radians = angle * 2 * M_PI / 360.0;
215         double az1 = az + radius * cos(radians);
216         double alt1 = alt + radius * sin(radians);
217         std::shared_ptr<SkyPoint> sp(new SkyPoint());
218         sp->setAz(az1);
219         sp->setAlt(alt1);
220         sp->HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
221         region.append(sp);
222     }
223     // Repeat the first point.
224     double az1 = az + radius * cos(0);
225     double alt1 = alt + radius * sin(0);
226     std::shared_ptr<SkyPoint> sp(new SkyPoint());
227     sp->setAz(az1);
228     sp->setAlt(alt1);
229     sp->HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
230     region.append(sp);
231 
232     painter->drawSkyPolygon(&region, false);
233 }
234 
235 // Draws a series of points whose coordinates are given by the LineList.
drawHorizonPoints(LineList * lineList,SkyPainter * painter)236 void drawHorizonPoints(LineList *lineList, SkyPainter *painter)
237 {
238     const SkyList &points = *(lineList->points());
239     for (int i = 0; i < points.size(); ++i)
240     {
241         const SkyPoint &pt = *points[i];
242         if (qIsNaN(pt.az().Degrees()) || qIsNaN(pt.alt().Degrees()))
243             continue;
244         drawHorizonPoint(pt, .5, painter);
245     }
246 }
247 
248 // Draws a points that is larger than the one drawn by drawHorizonPoint().
249 // The point's coordinates are the ith (index) point in the LineList.
drawSelectedPoint(LineList * lineList,int index,SkyPainter * painter)250 void drawSelectedPoint(LineList *lineList, int index, SkyPainter *painter)
251 {
252     if (index >= 0 && index < lineList->points()->size())
253     {
254         const SkyList &points = *(lineList->points());
255         const SkyPoint &pt = *points[index];
256         if (qIsNaN(pt.az().Degrees()) || qIsNaN(pt.alt().Degrees()))
257             return;
258         drawHorizonPoint(pt, 1.0, painter);
259     }
260 }
261 
262 // This creates a set of connected line segments from az1,alt1 to az2,alt2, sampling
263 // points on the great circle between az1,alt1 and az2,alt2 every 2 degrees or so.
264 // The errors would be obvious for longer lines if we just drew a standard line.
265 // If testing is true, HorizontalToEquatorial is not called.
appendGreatCirclePoints(double az1,double alt1,double az2,double alt2,LineList * region,bool testing)266 void appendGreatCirclePoints(double az1, double alt1, double az2, double alt2, LineList *region, bool testing)
267 {
268     constexpr double sampling = 2.0;  // degrees
269     const double maxAngleDiff = std::max(fabs(az1 - az2), fabs(alt1 - alt2));
270     const int numSamples = maxAngleDiff / sampling;
271 
272     if (numSamples > 1)
273     {
274         GreatCircle gc(az1, alt1, az2, alt2);
275         for (int i = 1; i < numSamples; ++i)
276         {
277             const double fraction = i / static_cast<double>(numSamples);
278             double az, alt;
279             gc.waypoint(fraction, &az, &alt);
280             std::shared_ptr<SkyPoint> sp(new SkyPoint());
281             sp->setAz(az);
282             sp->setAlt(alt);
283             if (!testing)
284                 sp->HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
285             region->append(sp);
286         }
287     }
288     std::shared_ptr<SkyPoint> sp(new SkyPoint());
289     sp->setAz(az2);
290     sp->setAlt(alt2);
291     // Is HorizontalToEquatorial necessary in any case?
292     if (!testing)
293         sp->HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
294     region->append(sp);
295 }
296 
297 }  // namespace
298 
299 // Draws a polygon, where one of the sides is az1,alt1 --> az2,alt2 (except that's implemented as series
300 // of connected line segments along a great circle).
301 // It figures out the opposite side depending on the type of the constraint for this entity
302 // (horizon line or ceiling) and the other contraints that are enabled.
computePolygon(int entity,double az1,double alt1,double az2,double alt2,LineList * region)303 bool ArtificialHorizon::computePolygon(int entity, double az1, double alt1, double az2, double alt2,
304                                        LineList *region)
305 {
306     const bool ceiling = horizonList()->at(entity)->ceiling();
307     const ArtificialHorizonEntity *thisOne = horizonList()->at(entity);
308     double alt1b = 0, alt2b = 0;
309     bool exists = false;
310     if (!ceiling)
311     {
312         // For standard horizon lines, the polygon is drawn down to the next lower-altitude
313         // enabled line, or to the horizon if a lower line doesn't exist.
314         const ArtificialHorizonEntity *constraint = getConstraintBelow(az1, alt1, thisOne);
315         if (constraint != nullptr)
316         {
317             double alt = constraint->altitudeConstraint(az1, &exists);
318             if (exists)
319                 alt1b = alt;
320         }
321         constraint = getConstraintBelow(az2, alt2, thisOne);
322         if (constraint != nullptr)
323         {
324             double alt = constraint->altitudeConstraint(az2, &exists);
325             if (exists)
326                 alt2b = alt;
327         }
328     }
329     else
330     {
331         // For ceiling lines, the polygon is drawn up to the next higher-altitude enabled line
332         // but only if that line is another cieling, otherwise it not drawn at all (because that
333         // horizon line will do the drawing).
334         const ArtificialHorizonEntity *constraint = getConstraintAbove(az1, alt1, thisOne);
335         alt1b = 90;
336         alt2b = 90;
337         if (constraint != nullptr)
338         {
339             if (!constraint->ceiling()) return false;
340             double alt = constraint->altitudeConstraint(az1, &exists);
341             if (exists) alt1b = alt;
342         }
343         constraint = getConstraintAbove(az2, alt2, thisOne);
344         if (constraint != nullptr)
345         {
346             if (!constraint->ceiling()) return false;
347             double alt = constraint->altitudeConstraint(az2, &exists);
348             if (exists) alt2b = alt;
349         }
350     }
351 
352     std::shared_ptr<SkyPoint> sp(new SkyPoint());
353     sp->setAz(az1);
354     sp->setAlt(alt1b);
355     if (!testing)
356         sp->HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
357     region->append(sp);
358 
359     appendGreatCirclePoints(az1, alt1b,  az1, alt1, region, testing);
360     appendGreatCirclePoints(az1, alt1,   az2, alt2, region, testing);
361     appendGreatCirclePoints(az2, alt2,   az2, alt2b, region, testing);
362     return true;
363 }
364 
365 // Draws a series of polygons of width in azimuth of "sampling degrees".
366 // Drawing a single polygon would have "great-circle issues". This looks a lot better.
367 // Assumes az1 and az2 in range 0-360 and az1 < az2.
368 // regions is only not nullptr during testing. In this wasy we can test
369 // whether the appropriate regions are drawn.
drawSampledPolygons(int entity,double az1,double alt1,double az2,double alt2,double sampling,SkyPainter * painter,QList<LineList> * regions)370 void ArtificialHorizon::drawSampledPolygons(int entity, double az1, double alt1, double az2, double alt2,
371         double sampling, SkyPainter *painter, QList<LineList> *regions)
372 {
373     if (az1 > az2)
374     {
375         // Should not happen.
376         fprintf(stderr, "Bad input to artificialhorizoncomponent.cpp::DrawSampledPolygons\n");
377         return;
378     }
379     double lastAz = az1;
380     double lastAlt = alt1;
381     const double azRange = az2 - az1, altRange = alt2 - alt1;
382     if (azRange == 0) return;
383     for (double az = az1 + sampling; az < az2; az += sampling)
384     {
385         double alt = alt1 + altRange * (az - az1) / azRange;
386 
387         LineList region;
388         if (computePolygon(entity, lastAz, lastAlt, az, alt, &region))
389         {
390             if (painter != nullptr)
391                 painter->drawSkyPolygon(&region, false);
392             if (regions != nullptr)
393                 regions->append(region);
394         }
395         lastAz = az;
396         lastAlt = alt;
397     }
398     LineList region;
399     if (computePolygon(entity, lastAz, lastAlt, az2, alt2, &region))
400     {
401         if (painter != nullptr)
402             painter->drawSkyPolygon(&region, false);
403         if (regions != nullptr)
404             regions->append(region);
405     }
406 }
407 
408 // This draws a series of polygons that fill the area that the horizon entity with index "entity"
409 // is responsible for.  If that is a horizon line, it draws it down to the horizon, or to the next
410 // lower line. It draws the polygons one pair of points at a time, and deals with complications
411 // of when the azimuth angle wraps around 360 degrees.
drawPolygons(int entity,SkyPainter * painter,QList<LineList> * regions)412 void ArtificialHorizon::drawPolygons(int entity, SkyPainter *painter, QList<LineList> *regions)
413 {
414     const ArtificialHorizonEntity &ah = *(horizonList()->at(entity));
415     const SkyList &points = *(ah.list()->points());
416 
417     // The skylist shouldn't contain NaN values, but, it has in the past,
418     // and, to be cautious, this checks for them and removes points with NaNs.
419     int start = 0;
420     for (; start < points.size(); ++start)
421     {
422         const SkyPoint &p = *points[start];
423         if (!qIsNaN(p.az().Degrees()) && !qIsNaN(p.alt().Degrees()))
424             break;
425     }
426     for (int i = start + 1; i < points.size(); ++i)
427     {
428         const SkyPoint &p2 = *points[i];
429         if (qIsNaN(p2.az().Degrees()) || qIsNaN(p2.alt().Degrees()))
430             continue;
431         const SkyPoint &p1 = *points[start];
432         start = i;
433 
434         const double az1 = normalizeDegrees(p1.az().Degrees());
435         const double az2 = normalizeDegrees(p2.az().Degrees());
436 
437         double minAz, maxAz, minAzAlt, maxAzAlt;
438         if (az1 < az2)
439         {
440             minAz = az1;
441             minAzAlt = p1.alt().Degrees();
442             maxAz = az2;
443             maxAzAlt = p2.alt().Degrees();
444         }
445         else
446         {
447             minAz = az2;
448             minAzAlt = p2.alt().Degrees();
449             maxAz = az1;
450             maxAzAlt = p1.alt().Degrees();
451         }
452         const bool wrapAround = !inBetween(dms((minAz + maxAz) / 2.0), dms(minAz), dms(maxAz));
453         constexpr double sampling = 0.1;  // Draw a polygon for every degree in Azimuth
454         if (wrapAround)
455         {
456             // We've detected that the line segment crosses 0 degrees.
457             // Draw one polygon on one side of 0 degrees, and another on the other side.
458             // Compute the altitude at wrap-around.
459             const double fraction = fabs(dms(360.0).deltaAngle(dms(maxAz)).Degrees() /
460                                          p1.az().deltaAngle(p2.az()).Degrees());
461             const double midAlt = minAzAlt + fraction * (maxAzAlt - minAzAlt);
462             // Draw polygons form maxAz upto 0 degrees, then again from 0 to minAz.
463             drawSampledPolygons(entity, maxAz, maxAzAlt, 360.0, midAlt, sampling, painter, regions);
464             drawSampledPolygons(entity, 0, midAlt, minAz, minAzAlt, sampling, painter, regions);
465         }
466         else
467         {
468             // Draw the polygons without wraparound
469             drawSampledPolygons(entity, minAz, minAzAlt, maxAz, maxAzAlt, sampling, painter, regions);
470         }
471     }
472 }
473 
drawPolygons(SkyPainter * painter,QList<LineList> * regions)474 void ArtificialHorizon::drawPolygons(SkyPainter *painter, QList<LineList> *regions)
475 {
476     for (int i = 0; i < horizonList()->size(); i++)
477     {
478         if (enabled(i))
479             drawPolygons(i, painter, regions);
480     }
481 }
482 
draw(SkyPainter * skyp)483 void ArtificialHorizonComponent::draw(SkyPainter *skyp)
484 {
485     if (!selected())
486         return;
487 
488     if (livePreview.get())
489     {
490         if ((livePreview->points() != nullptr) && (livePreview->points()->size() > 0))
491         {
492             // Draws a series of line segments, overlayed by the vertices.
493             // One vertex (the current selection) is emphasized.
494             skyp->setPen(QPen(Qt::white, 2));
495             skyp->drawSkyPolyline(livePreview.get());
496             skyp->setBrush(QBrush(Qt::yellow));
497             drawSelectedPoint(livePreview.get(), selectedPreviewPoint, skyp);
498             skyp->setBrush(QBrush(Qt::red));
499             drawHorizonPoints(livePreview.get(), skyp);
500         }
501     }
502 
503     preDraw(skyp);
504 
505     QList<LineList> regions;
506     horizon.drawPolygons(skyp, &regions);
507 }
508 
enabled(int i) const509 bool ArtificialHorizon::enabled(int i) const
510 {
511     return m_HorizonList.at(i)->enabled();
512 }
513 
findRegion(const QString & regionName)514 ArtificialHorizonEntity *ArtificialHorizon::findRegion(const QString &regionName)
515 {
516     ArtificialHorizonEntity *regionHorizon = nullptr;
517 
518     foreach (ArtificialHorizonEntity *horizon, m_HorizonList)
519     {
520         if (horizon->region() == regionName)
521         {
522             regionHorizon = horizon;
523             break;
524         }
525     }
526 
527     return regionHorizon;
528 }
529 
removeRegion(const QString & regionName,bool lineOnly)530 void ArtificialHorizon::removeRegion(const QString &regionName, bool lineOnly)
531 {
532     ArtificialHorizonEntity *regionHorizon = findRegion(regionName);
533 
534     if (regionHorizon == nullptr)
535         return;
536 
537     if (lineOnly)
538         regionHorizon->clearList();
539     else
540     {
541         m_HorizonList.removeOne(regionHorizon);
542         delete (regionHorizon);
543     }
544 }
545 
removeRegion(const QString & regionName,bool lineOnly)546 void ArtificialHorizonComponent::removeRegion(const QString &regionName, bool lineOnly)
547 {
548     ArtificialHorizonEntity *regionHorizon = horizon.findRegion(regionName);
549     if (regionHorizon != nullptr && regionHorizon->list())
550         removeLine(regionHorizon->list());
551     horizon.removeRegion(regionName, lineOnly);
552 }
553 
addRegion(const QString & regionName,bool enabled,const std::shared_ptr<LineList> & list,bool ceiling)554 void ArtificialHorizon::addRegion(const QString &regionName, bool enabled, const std::shared_ptr<LineList> &list,
555                                   bool ceiling)
556 {
557     ArtificialHorizonEntity *horizon = new ArtificialHorizonEntity;
558 
559     horizon->setRegion(regionName);
560     horizon->setEnabled(enabled);
561     horizon->setCeiling(ceiling);
562     horizon->setList(list);
563 
564     m_HorizonList.append(horizon);
565 }
566 
addRegion(const QString & regionName,bool enabled,const std::shared_ptr<LineList> & list,bool ceiling)567 void ArtificialHorizonComponent::addRegion(const QString &regionName, bool enabled, const std::shared_ptr<LineList> &list,
568         bool ceiling)
569 {
570     horizon.addRegion(regionName, enabled, list, ceiling);
571     appendLine(list);
572 }
573 
altitudeConstraintsExist() const574 bool ArtificialHorizon::altitudeConstraintsExist() const
575 {
576     foreach (ArtificialHorizonEntity *horizon, m_HorizonList)
577     {
578         if (horizon->enabled())
579             return true;
580     }
581     return false;
582 }
583 
getConstraintAbove(double azimuthDegrees,double altitudeDegrees,const ArtificialHorizonEntity * ignore) const584 const ArtificialHorizonEntity *ArtificialHorizon::getConstraintAbove(double azimuthDegrees, double altitudeDegrees,
585         const ArtificialHorizonEntity *ignore) const
586 {
587     double closestAbove = 1e6;
588     const ArtificialHorizonEntity *entity = nullptr;
589 
590     foreach (ArtificialHorizonEntity *horizon, m_HorizonList)
591     {
592         if (!horizon->enabled()) continue;
593         if (horizon == ignore) continue;
594         bool constraintExists = false;
595         double constraint = horizon->altitudeConstraint(azimuthDegrees, &constraintExists);
596         // This horizon doesn't constrain this azimuth.
597         if (!constraintExists) continue;
598 
599         double altitudeDiff = constraint - altitudeDegrees;
600         if (altitudeDiff > 0 && constraint < closestAbove)
601         {
602             closestAbove = constraint;
603             entity = horizon;
604         }
605     }
606     return entity;
607 }
608 
altitudeConstraint(double azimuthDegrees) const609 double ArtificialHorizon::altitudeConstraint(double azimuthDegrees) const
610 {
611     const ArtificialHorizonEntity *horizonBelow = getConstraintBelow(azimuthDegrees, 90.0, nullptr);
612     if (horizonBelow == nullptr)
613         return UNDEFINED_ALTITUDE;
614     bool ignore = false;
615     return horizonBelow->altitudeConstraint(azimuthDegrees, &ignore);
616 }
617 
getConstraintBelow(double azimuthDegrees,double altitudeDegrees,const ArtificialHorizonEntity * ignore) const618 const ArtificialHorizonEntity *ArtificialHorizon::getConstraintBelow(double azimuthDegrees, double altitudeDegrees,
619         const ArtificialHorizonEntity *ignore) const
620 {
621     double closestBelow = -1e6;
622     const ArtificialHorizonEntity *entity = nullptr;
623 
624     foreach (ArtificialHorizonEntity *horizon, m_HorizonList)
625     {
626 
627         if (!horizon->enabled()) continue;
628         if (horizon == ignore) continue;
629         bool constraintExists = false;
630         double constraint = horizon->altitudeConstraint(azimuthDegrees, &constraintExists);
631         // This horizon doesn't constrain this azimuth.
632         if (!constraintExists) continue;
633 
634         double altitudeDiff = constraint - altitudeDegrees;
635         if (altitudeDiff < 0 && constraint > closestBelow)
636         {
637             closestBelow = constraint;
638             entity = horizon;
639         }
640     }
641     return entity;
642 }
643 
644 // An altitude is blocked (not visible) if either:
645 // - there are constraints above and the closest above constraint is not a ceiling, or
646 // - there are constraints below and the closest below constraint is a ceiling.
isVisible(double azimuthDegrees,double altitudeDegrees) const647 bool ArtificialHorizon::isVisible(double azimuthDegrees, double altitudeDegrees) const
648 {
649     const ArtificialHorizonEntity *above = getConstraintAbove(azimuthDegrees, altitudeDegrees);
650     if (above != nullptr && !above->ceiling()) return false;
651     const ArtificialHorizonEntity *below = getConstraintBelow(azimuthDegrees, altitudeDegrees);
652     if (below != nullptr && below->ceiling()) return false;
653     return true;
654 }
655