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(®ion, 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, ®ion))
389 {
390 if (painter != nullptr)
391 painter->drawSkyPolygon(®ion, 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, ®ion))
400 {
401 if (painter != nullptr)
402 painter->drawSkyPolygon(®ion, 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, ®ions);
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 ®ionName)
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 ®ionName, 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 ®ionName, 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 ®ionName, 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 ®ionName, 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