Kstars

artificialhorizoncomponent.cpp
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
20ArtificialHorizonEntity::~ArtificialHorizonEntity()
21{
22 clearList();
23}
24
25QString ArtificialHorizonEntity::region() const
26{
27 return m_Region;
28}
29
30void ArtificialHorizonEntity::setRegion(const QString &Region)
31{
32 m_Region = Region;
33}
34
35bool ArtificialHorizonEntity::enabled() const
36{
37 return m_Enabled;
38}
39
40void ArtificialHorizonEntity::setEnabled(bool Enabled)
41{
42 m_Enabled = Enabled;
43}
44
45bool ArtificialHorizonEntity::ceiling() const
46{
47 return m_Ceiling;
48}
49
50void ArtificialHorizonEntity::setCeiling(bool value)
51{
52 m_Ceiling = value;
53}
54
55void ArtificialHorizonEntity::setList(const std::shared_ptr<LineList> &list)
56{
57 m_List = list;
58}
59
60std::shared_ptr<LineList> ArtificialHorizonEntity::list() const
61{
62 return m_List;
63}
64
65void ArtificialHorizonEntity::clearList()
66{
67 m_List.reset();
68}
69
70namespace
71{
72
73// Returns true if angle is "in between" range1 and range2, two other angles,
74// where in-between means "the short way".
75bool 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
86double 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
139ArtificialHorizonComponent::ArtificialHorizonComponent(SkyComposite *parent)
140 : NoPrecessIndex(parent, i18n("Artificial Horizon"))
141{
142 load();
143}
144
145ArtificialHorizonComponent::~ArtificialHorizonComponent()
146{
147}
148
149ArtificialHorizon::~ArtificialHorizon()
150{
151 qDeleteAll(m_HorizonList);
152 m_HorizonList.clear();
153}
154
155void ArtificialHorizon::load(const QList<ArtificialHorizonEntity *> &list)
156{
157 m_HorizonList = list;
158 resetPrecomputeConstraints();
159 checkForCeilings();
160}
161
162bool 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
174void ArtificialHorizonComponent::save()
175{
176 KStarsData::Instance()->userdb()->DeleteAllHorizons();
177
178 foreach (ArtificialHorizonEntity *horizon, *horizon.horizonList())
179 KStarsData::Instance()->userdb()->AddHorizon(horizon);
180}
181
182bool ArtificialHorizonComponent::selected()
183{
184 return Options::showGround();
185}
186
187void 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
195namespace
196{
197
198// Returns an equivalent degrees in the range 0 <= 0 < 360
199double 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.
210void 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.
240void 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.
254void 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.
270void 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.
315bool 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.
460void 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.
484void 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
547void 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
556void 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
582bool ArtificialHorizon::enabled(int i) const
583{
584 return m_HorizonList.at(i)->enabled();
585}
586
587ArtificialHorizonEntity *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
603void 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
621void 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
629void 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
642void 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
657void 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
664bool ArtificialHorizon::altitudeConstraintsExist() const
665{
666 foreach (ArtificialHorizonEntity *horizon, m_HorizonList)
667 {
668 if (horizon->enabled())
669 return true;
670 }
671 return false;
672}
673
674const 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.
701constexpr int PRECOMPUTED_RESOLUTION = 10;
702
703double ArtificialHorizon::altitudeConstraint(double azimuthDegrees) const
704{
705 if (precomputedConstraints.size() != 360 * PRECOMPUTED_RESOLUTION)
706 precomputeConstraints();
707 return precomputedConstraint(azimuthDegrees);
708}
709
710double 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).
721void 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
732void ArtificialHorizon::resetPrecomputeConstraints() const
733{
734 precomputedConstraints.clear();
735}
736
737double 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
748const 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
773bool 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.
791bool 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}
A class to compute points along a great circle from one az/alt to another.
Definition greatcircle.h:28
bool GetAllHorizons(QList< ArtificialHorizonEntity * > &horizonList)
Gets all the artificial horizon rows from the database.
bool AddHorizon(ArtificialHorizonEntity *horizon)
Adds a new artificial horizon row into the database.
bool DeleteAllHorizons()
Deletes all artificial horizon rows from the database.
KSUserDB * userdb()
Definition kstarsdata.h:215
void appendLine(const std::shared_ptr< LineList > &lineList)
Typically called from within a subclasses constructors.
A simple data container used by LineListIndex.
Definition linelist.h:25
SkyList * points()
return the list of points for iterating or appending (or whatever).
Definition linelist.h:33
SkyComposite is a kind of container class for SkyComponent objects.
Draws things on the sky, without regard to backend.
Definition skypainter.h:40
virtual void drawSkyPolyline(LineList *list, SkipHashList *skipList=nullptr, LineListLabel *label=nullptr)=0
Draw a polyline in the sky.
virtual void drawSkyPolygon(LineList *list, bool forceClip=true)=0
Draw a polygon in the sky.
virtual void setBrush(const QBrush &brush)=0
Set the brush of the painter.
virtual void setPen(const QPen &pen)=0
Set the pen of the painter.
The sky coordinates of a point in the sky.
Definition skypoint.h:45
const dms & az() const
Definition skypoint.h:275
const dms & alt() const
Definition skypoint.h:281
An angle, stored as degrees, but expressible in many ways.
Definition dms.h:38
const dms deltaAngle(dms angle) const
deltaAngle Return the shortest difference (path) between this angle and the supplied angle.
Definition dms.cpp:267
const double & Degrees() const
Definition dms.h:141
QString i18n(const char *text, const TYPE &arg...)
QAction * load(const QObject *recvr, const char *slot, QObject *parent)
GeoCoordinates geo(const QVariant &location)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
void append(QList< T > &&value)
const_reference at(qsizetype i) const const
void clear()
bool removeOne(const AT &t)
qsizetype size() const const
QString arg(Args &&... args) const const
QTextStream & left(QTextStream &stream)
QTextStream & right(QTextStream &stream)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Jul 26 2024 11:59:52 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.