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
105 // There can be issues with GreatCircle below if altitudes are at the zenith.
106 const double alt = std::min(89.999, p->alt().Degrees());
107
108 if (qIsNaN(az.Degrees()) || qIsNaN(alt)) continue;
109 if (!firstOne && inBetween(desiredAzimuth, lastAz, az))
110 {
111 *constraintExists = true;
112 // If the input angle is in the interval between the last two points,
113 // interpolate the altitude constraint, and use that value.
114 // If there are other line segments which also contain the point,
115 // we use the max constraint.
116 const double totalDelta = fabs(lastAz.deltaAngle(az).Degrees());
117 if (totalDelta <= 0)
118 {
119 if (!m_Ceiling)
120 constraint = std::max(constraint, alt);
121 else
122 constraint = std::min(constraint, alt);
123 }
124 else
125 {
126 GreatCircle gc(lastAz.Degrees(), lastAlt, az.Degrees(), alt);
127 const double newConstraint = gc.altAtAz(azimuthDegrees);
128 if (!m_Ceiling)
129 constraint = std::max(constraint, newConstraint);
130 else
131 constraint = std::min(constraint, newConstraint);
132 }
133 }
134 firstOne = false;
135 lastAz = az;
136 lastAlt = alt;
137 }
138 return constraint;
139}
140
141ArtificialHorizonComponent::ArtificialHorizonComponent(SkyComposite *parent)
142 : NoPrecessIndex(parent, i18n("Artificial Horizon"))
143{
144 load();
145}
146
147ArtificialHorizonComponent::~ArtificialHorizonComponent()
148{
149}
150
151ArtificialHorizon::~ArtificialHorizon()
152{
153 qDeleteAll(m_HorizonList);
154 m_HorizonList.clear();
155}
156
157void ArtificialHorizon::load(const QList<ArtificialHorizonEntity *> &list)
158{
159 m_HorizonList = list;
160 resetPrecomputeConstraints();
161 checkForCeilings();
162}
163
164bool ArtificialHorizonComponent::load()
165{
167 KStarsData::Instance()->userdb()->GetAllHorizons(list);
168 horizon.load(list);
169
170 foreach (ArtificialHorizonEntity *horizon, *horizon.horizonList())
171 appendLine(horizon->list());
172
173 return true;
174}
175
176void ArtificialHorizonComponent::save()
177{
178 KStarsData::Instance()->userdb()->DeleteAllHorizons();
179
180 foreach (ArtificialHorizonEntity *horizon, *horizon.horizonList())
181 KStarsData::Instance()->userdb()->AddHorizon(horizon);
182}
183
184bool ArtificialHorizonComponent::selected()
185{
186 return true;
187}
188
189void ArtificialHorizonComponent::preDraw(SkyPainter *skyp)
190{
191 QColor color(KStarsData::Instance()->colorScheme()->colorNamed("ArtificialHorizonColor"));
192 color.setAlpha(40);
193 skyp->setBrush(QBrush(color));
194 skyp->setPen(Qt::NoPen);
195}
196
197namespace
198{
199
200// Returns an equivalent degrees in the range 0 <= 0 < 360
201double normalizeDegrees(double degrees)
202{
203 while (degrees < 0)
204 degrees += 360;
205 while (degrees >= 360.0)
206 degrees -= 360.0;
207 return degrees;
208}
209
210// Draws a "round polygon", sampling a circle every 45 degrees, with the given radius,
211// centered on the SkyPoint.
212void drawHorizonPoint(const SkyPoint &pt, double radius, SkyPainter *painter)
213
214{
215 LineList region;
216 double az = pt.az().Degrees(), alt = pt.alt().Degrees();
217
218 for (double angle = 0; angle < 360; angle += 45)
219 {
220 double radians = angle * 2 * M_PI / 360.0;
221 double az1 = az + radius * cos(radians);
222 double alt1 = alt + radius * sin(radians);
223 std::shared_ptr<SkyPoint> sp(new SkyPoint());
224 sp->setAz(az1);
225 sp->setAlt(alt1);
226 sp->HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
227 region.append(sp);
228 }
229 // Repeat the first point.
230 double az1 = az + radius * cos(0);
231 double alt1 = alt + radius * sin(0);
232 std::shared_ptr<SkyPoint> sp(new SkyPoint());
233 sp->setAz(az1);
234 sp->setAlt(alt1);
235 sp->HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
236 region.append(sp);
237
238 painter->drawSkyPolygon(&region, false);
239}
240
241// Draws a series of points whose coordinates are given by the LineList.
242void drawHorizonPoints(LineList *lineList, SkyPainter *painter)
243{
244 const SkyList &points = *(lineList->points());
245 for (int i = 0; i < points.size(); ++i)
246 {
247 const SkyPoint &pt = *points[i];
248 if (qIsNaN(pt.az().Degrees()) || qIsNaN(pt.alt().Degrees()))
249 continue;
250 drawHorizonPoint(pt, .5, painter);
251 }
252}
253
254// Draws a points that is larger than the one drawn by drawHorizonPoint().
255// The point's coordinates are the ith (index) point in the LineList.
256void drawSelectedPoint(LineList *lineList, int index, SkyPainter *painter)
257{
258 if (index >= 0 && index < lineList->points()->size())
259 {
260 const SkyList &points = *(lineList->points());
261 const SkyPoint &pt = *points[index];
262 if (qIsNaN(pt.az().Degrees()) || qIsNaN(pt.alt().Degrees()))
263 return;
264 drawHorizonPoint(pt, 1.0, painter);
265 }
266}
267
268// This creates a set of connected line segments from az1,alt1 to az2,alt2, sampling
269// points on the great circle between az1,alt1 and az2,alt2 every 2 degrees or so.
270// The errors would be obvious for longer lines if we just drew a standard line.
271// If testing is true, HorizontalToEquatorial is not called.
272void appendGreatCirclePoints(double az1, double alt1, double az2, double alt2, LineList *region, bool testing)
273{
274 constexpr double sampling = 2.0; // degrees
275 const double maxAngleDiff = std::max(fabs(az1 - az2), fabs(alt1 - alt2));
276 const int numSamples = maxAngleDiff / sampling;
277
278 // Hy 9/25/22: These 4 lines cause rendering issues in equatorial mode (horizon mode is ok).
279 // Not sure why--though I suspect the initial conditions computed in drawPolygons().
280 // Without them there are some jagged lines near the horizon, but much better than with them.
281 // std::shared_ptr<SkyPoint> sp0(new SkyPoint());
282 // sp0->setAz(az1);
283 // sp0->setAlt(alt1);
284 // region->append(sp0);
285
286 if (numSamples > 1)
287 {
288 GreatCircle gc(az1, alt1, az2, alt2);
289 for (int i = 1; i < numSamples; ++i)
290 {
291 const double fraction = i / static_cast<double>(numSamples);
292 double az, alt;
293 gc.waypoint(fraction, &az, &alt);
294 std::shared_ptr<SkyPoint> sp(new SkyPoint());
295 sp->setAz(az);
296 sp->setAlt(alt);
297 if (!testing)
298 sp->HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
299 region->append(sp);
300 }
301 }
302 std::shared_ptr<SkyPoint> sp(new SkyPoint());
303 sp->setAz(az2);
304 sp->setAlt(alt2);
305 // Is HorizontalToEquatorial necessary in any case?
306 if (!testing)
307 sp->HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
308 region->append(sp);
309}
310
311} // namespace
312
313// Draws a polygon, where one of the sides is az1,alt1 --> az2,alt2 (except that's implemented as series
314// of connected line segments along a great circle).
315// It figures out the opposite side depending on the type of the constraint for this entity
316// (horizon line or ceiling) and the other contraints that are enabled.
317bool ArtificialHorizon::computePolygon(int entity, double az1, double alt1, double az2, double alt2,
318 double sampling, LineList *region)
319{
320 // There can be issues with GreatCircle below if both altitudes are at the zenith.
321 if (alt1 >= 90 && alt2 >= 90)
322 {
323 alt1 = 89.99;
324 alt2 = 89.99;
325 }
326 const bool ceiling = horizonList()->at(entity)->ceiling();
327 const ArtificialHorizonEntity *thisOne = horizonList()->at(entity);
328 double alt1b = 0, alt2b = 0;
329 bool exists = false;
330 LineList left, top, right, bottom;
331
332 double lastAz = az1;
333 double lastAlt = alt1;
334
335 if (az1 >= az2)
336 return false;
337
338 if (az1 + sampling > az2)
339 sampling = (az2 - az1) - 1e-6;
340
341 GreatCircle gc(az1, alt1, az2, alt2);
342 double numSamples = (az2 - az1) / sampling;
343 for (int i = 0; i < numSamples; ++i)
344 {
345 double fraction = i / numSamples;
346 if (fraction + (1.0 / numSamples) > (1 + .0001))
347 fraction = 1.0;
348
349 double az, alt;
350 gc.waypoint(fraction, &az, &alt);
351 double alt1b = 0, alt2b = 0;
352
353 if (!ceiling)
354 {
355 // For standard horizon lines, the polygon is drawn down to the next lower-altitude
356 // enabled line, or to the horizon if a lower line doesn't exist.
357 const ArtificialHorizonEntity *constraint = getConstraintBelow(lastAz, lastAlt, thisOne);
358 if (constraint != nullptr)
359 {
360 double altTemp = constraint->altitudeConstraint(lastAz, &exists);
361 if (exists)
362 alt1b = altTemp;
363 }
364 constraint = getConstraintBelow(az, alt, thisOne);
365 if (constraint != nullptr)
366 {
367 double altTemp = constraint->altitudeConstraint(az, &exists);
368 if (exists)
369 alt2b = altTemp;
370 }
371 appendGreatCirclePoints(lastAz, lastAlt, az, alt, &top, testing);
372 appendGreatCirclePoints(lastAz, alt1b, az, alt2b, &bottom, testing);
373 }
374 else
375 {
376 // For ceiling lines, the polygon is drawn up to the next higher-altitude enabled line
377 // but only if that line is another cieling, otherwise it not drawn at all (because that
378 // horizon line will do the drawing).
379 const ArtificialHorizonEntity *constraint = getConstraintAbove(lastAz, lastAlt, thisOne);
380 alt1b = 90;
381 alt2b = 90;
382 if (constraint != nullptr)
383 {
384 if (constraint->ceiling()) return false;
385 double altTemp = constraint->altitudeConstraint(lastAz, &exists);
386 if (exists) alt1b = altTemp;
387 }
388 constraint = getConstraintAbove(az, alt, thisOne);
389 if (constraint != nullptr)
390 {
391 if (constraint->ceiling()) return false;
392 double altTemp = constraint->altitudeConstraint(az, &exists);
393 if (exists) alt2b = altTemp;
394 }
395 appendGreatCirclePoints(lastAz, lastAlt, az, alt, &top, testing);
396 // Note that "bottom" for a ceiling is above.
397 appendGreatCirclePoints(lastAz, alt1b, az, alt2b, &bottom, testing);
398 }
399 lastAz = az;
400 lastAlt = alt;
401 }
402
403 if (!ceiling)
404 {
405 // For standard horizon lines, the polygon is drawn down to the next lower-altitude
406 // enabled line, or to the horizon if a lower line doesn't exist.
407 const ArtificialHorizonEntity *constraint = getConstraintBelow(az1, alt1, thisOne);
408 if (constraint != nullptr)
409 {
410 double altTemp = constraint->altitudeConstraint(az1, &exists);
411 if (exists)
412 alt1b = altTemp;
413 }
414 appendGreatCirclePoints(az1, alt1b, az1, alt1, &left, testing);
415
416 const ArtificialHorizonEntity *constraint2 = getConstraintBelow(az2, alt2, thisOne);
417 if (constraint2 != nullptr)
418 {
419 double altTemp = constraint2->altitudeConstraint(az2, &exists);
420 if (exists)
421 alt2b = altTemp;
422 }
423 appendGreatCirclePoints(az2, alt2, az2, alt2b, &right, testing);
424 }
425 else
426 {
427 // For ceiling lines, the polygon is drawn up to the next higher-altitude enabled line
428 // but only if that line is another cieling, otherwise it not drawn at all (because that
429 // horizon line will do the drawing).
430 const ArtificialHorizonEntity *constraint = getConstraintAbove(az1, alt1, thisOne);
431 alt1b = 90;
432 alt2b = 90;
433 if (constraint != nullptr)
434 {
435 if (!constraint->ceiling()) return false;
436 double altTemp = constraint->altitudeConstraint(az1, &exists);
437 if (exists) alt1b = altTemp;
438 }
439 appendGreatCirclePoints(az1, alt1b, az1, alt1, &left, testing);
440
441 const ArtificialHorizonEntity *constraint2 = getConstraintAbove(az2, alt2, thisOne);
442 if (constraint2 != nullptr)
443 {
444 if (!constraint2->ceiling()) return false;
445 double altTemp = constraint2->altitudeConstraint(az2, &exists);
446 if (exists) alt2b = altTemp;
447 }
448 appendGreatCirclePoints(az2, alt2, az2, alt2b, &right, testing);
449 }
450
451 // Now we have all the sides: left, top, right, bottom, but the order of bottom is reversed.
452 // Make a polygon with all the points.
453 for (const auto &p : * (left.points()))
454 region->append(p);
455 for (const auto &p : * (top.points()))
456 region->append(p);
457 for (const auto &p : * (right.points()))
458 region->append(p);
459 for (int i = bottom.points()->size() - 1; i >= 0; i--)
460 region->append(bottom.points()->at(i));
461
462 return true;
463}
464
465// Draws a series of polygons of width in azimuth of "sampling degrees".
466// Drawing a single polygon would have "great-circle issues". This looks a lot better.
467// Assumes az1 and az2 in range 0-360 and az1 < az2.
468// regions is only not nullptr during testing. In this wasy we can test
469// whether the appropriate regions are drawn.
470void ArtificialHorizon::drawSampledPolygons(int entity, double az1, double alt1, double az2, double alt2,
471 double sampling, SkyPainter *painter, QList<LineList> *regions)
472{
473 if (az1 > az2)
474 {
475 // Should not generally happen. Possibility e.g. az 0 -> 0.01 in a wrap around.
476 // OK to ignore.
477 return;
478 }
479
480 LineList region;
481 if (computePolygon(entity, az1, alt1, az2, alt2, sampling, &region))
482 {
483 if (painter != nullptr)
484 painter->drawSkyPolygon(&region, false);
485 if (regions != nullptr)
486 regions->append(region);
487 }
488}
489
490// This draws a series of polygons that fill the area that the horizon entity with index "entity"
491// is responsible for. If that is a horizon line, it draws it down to the horizon, or to the next
492// lower line. It draws the polygons one pair of points at a time, and deals with complications
493// of when the azimuth angle wraps around 360 degrees.
494void ArtificialHorizon::drawPolygons(int entity, SkyPainter *painter, QList<LineList> *regions)
495{
496 const ArtificialHorizonEntity &ah = *(horizonList()->at(entity));
497 const SkyList &points = *(ah.list()->points());
498
499 // The skylist shouldn't contain NaN values, but, it has in the past,
500 // and, to be cautious, this checks for them and removes points with NaNs.
501 int start = 0;
502 for (; start < points.size(); ++start)
503 {
504 const SkyPoint &p = *points[start];
505 if (!qIsNaN(p.az().Degrees()) && !qIsNaN(p.alt().Degrees()))
506 break;
507 }
508
509 for (int i = start + 1; i < points.size(); ++i)
510 {
511 const SkyPoint &p2 = *points[i];
512 if (qIsNaN(p2.az().Degrees()) || qIsNaN(p2.alt().Degrees()))
513 continue;
514 const SkyPoint &p1 = *points[start];
515 start = i;
516
517 const double az1 = normalizeDegrees(p1.az().Degrees());
518 const double az2 = normalizeDegrees(p2.az().Degrees());
519
520 double minAz, maxAz, minAzAlt, maxAzAlt;
521 if (az1 < az2)
522 {
523 minAz = az1;
524 minAzAlt = p1.alt().Degrees();
525 maxAz = az2;
526 maxAzAlt = p2.alt().Degrees();
527 }
528 else
529 {
530 minAz = az2;
531 minAzAlt = p2.alt().Degrees();
532 maxAz = az1;
533 maxAzAlt = p1.alt().Degrees();
534 }
535 const bool wrapAround = !inBetween(dms((minAz + maxAz) / 2.0), dms(minAz), dms(maxAz));
536 constexpr double sampling = 1.0; // Draw a polygon for every degree in Azimuth
537 if (wrapAround)
538 {
539 // We've detected that the line segment crosses 0 degrees.
540 // Draw one polygon on one side of 0 degrees, and another on the other side.
541 // Compute the altitude at wrap-around.
542 GreatCircle gc(maxAz, maxAzAlt, minAz, minAzAlt);
543 const double midAlt = gc.altAtAz(0);
544 // Draw polygons form maxAz upto 0 degrees, then again from 0 to minAz.
545 drawSampledPolygons(entity, maxAz, maxAzAlt, 360, midAlt, sampling, painter, regions);
546 drawSampledPolygons(entity, 0, midAlt, minAz, minAzAlt, sampling, painter, regions);
547 }
548 else
549 {
550 // Draw the polygons without wraparound
551 drawSampledPolygons(entity, minAz, minAzAlt, maxAz, maxAzAlt, sampling, painter, regions);
552 }
553 }
554}
555
556void ArtificialHorizon::drawPolygons(SkyPainter *painter, QList<LineList> *regions)
557{
558 for (int i = 0; i < horizonList()->size(); i++)
559 {
560 if (enabled(i))
561 drawPolygons(i, painter, regions);
562 }
563}
564
565// This samples the lines in the input list by 0.1 degrees.
566// In this way they will be drawn to approximate a great-circle curve.
567void sampleLineList(std::shared_ptr<LineList> *list, std::shared_ptr<LineList> *tempPoints)
568{
569 constexpr double sampling = 0.1; // degrees
570 const auto points = list->get()->points();
571 const int size = points->size();
572 (*tempPoints)->points()->clear();
573 for (int upto = 0; upto < size - 1; ++upto)
574 {
575 const auto p1 = points->at(upto);
576 const auto p2 = points->at(upto + 1);
577 GreatCircle gc(p1->az().Degrees(), std::min(89.999, p1->alt().Degrees()), p2->az().Degrees(), std::min(89.999,
578 p2->alt().Degrees()));
579 const double maxDelta = std::max(fabs(p2->az().Degrees() - p1->az().Degrees()),
580 fabs(p2->alt().Degrees() - p1->alt().Degrees()));
581 if (maxDelta == 0) continue;
582 int numP = maxDelta / sampling;
583 if (numP == 0) numP = 2;
584 for (int i = 0; i < numP; ++i)
585 {
586 double newAz = 0, newAlt = 0;
587 gc.waypoint(i * 1.0 / numP, &newAz, &newAlt);
588 SkyPoint *newPt = new SkyPoint;
589 newPt->setAz(newAz);
590 newPt->setAlt(newAlt);
591 newPt->HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
592 (*tempPoints)->append(std::shared_ptr<SkyPoint>(newPt));
593 }
594 // Do the last point.
595 if (upto == (size - 2))
596 {
597 SkyPoint *newPt = new SkyPoint;
598 newPt->setAz(p2->az().Degrees());
599 newPt->setAlt(p2->alt().Degrees());
600 newPt->HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
601 (*tempPoints)->append(std::shared_ptr<SkyPoint>(newPt));
602 }
603 }
604}
605
606void ArtificialHorizonComponent::draw(SkyPainter *skyp)
607{
608 if (!selected())
609 return;
610
611 bool showPolygons = Options::showGround();
612 if (livePreview.get())
613 {
614 if ((livePreview->points() != nullptr) && (livePreview->points()->size() > 0))
615 {
616 // Draws a series of line segments, overlayed by the vertices.
617 // One vertex (the current selection) is emphasized.
618 skyp->setPen(QPen(Qt::white, 2));
619
620 // Sample the points so that the line renders as an approximation to a great-circle curve.
621 auto tempLineList = std::shared_ptr<LineList>(new LineList);
622 *tempLineList = *livePreview;
623 sampleLineList(&livePreview, &tempLineList);
624 skyp->drawSkyPolyline(tempLineList.get());
625 skyp->setBrush(QBrush(Qt::yellow));
626 drawSelectedPoint(livePreview.get(), selectedPreviewPoint, skyp);
627 skyp->setBrush(QBrush(Qt::red));
628 drawHorizonPoints(livePreview.get(), skyp);
629 showPolygons = true;
630 }
631 }
632
633 if (showPolygons)
634 {
635 preDraw(skyp);
636 QList<LineList> regions;
637 horizon.drawPolygons(skyp, &regions);
638 }
639}
640
641bool ArtificialHorizon::enabled(int i) const
642{
643 return m_HorizonList.at(i)->enabled();
644}
645
646ArtificialHorizonEntity *ArtificialHorizon::findRegion(const QString &regionName)
647{
648 ArtificialHorizonEntity *regionHorizon = nullptr;
649
650 foreach (ArtificialHorizonEntity *horizon, m_HorizonList)
651 {
652 if (horizon->region() == regionName)
653 {
654 regionHorizon = horizon;
655 break;
656 }
657 }
658
659 return regionHorizon;
660}
661
662void ArtificialHorizon::removeRegion(const QString &regionName, bool lineOnly)
663{
664 ArtificialHorizonEntity *regionHorizon = findRegion(regionName);
665
666 if (regionHorizon == nullptr)
667 return;
668
669 if (lineOnly)
670 regionHorizon->clearList();
671 else
672 {
673 m_HorizonList.removeOne(regionHorizon);
674 delete (regionHorizon);
675 }
676 resetPrecomputeConstraints();
677 checkForCeilings();
678}
679
680void ArtificialHorizonComponent::removeRegion(const QString &regionName, bool lineOnly)
681{
682 ArtificialHorizonEntity *regionHorizon = horizon.findRegion(regionName);
683 if (regionHorizon != nullptr && regionHorizon->list())
684 removeLine(regionHorizon->list());
685 horizon.removeRegion(regionName, lineOnly);
686}
687
688void ArtificialHorizon::checkForCeilings()
689{
690 noCeilingConstraints = true;
691 for (const auto &r : m_HorizonList)
692 {
693 if (r->ceiling() && r->enabled())
694 {
695 noCeilingConstraints = false;
696 break;
697 }
698 }
699}
700
701void ArtificialHorizon::addRegion(const QString &regionName, bool enabled, const std::shared_ptr<LineList> &list,
702 bool ceiling)
703{
704 ArtificialHorizonEntity *horizon = new ArtificialHorizonEntity;
705
706 horizon->setRegion(regionName);
707 horizon->setEnabled(enabled);
708 horizon->setCeiling(ceiling);
709 horizon->setList(list);
710
711 m_HorizonList.append(horizon);
712 resetPrecomputeConstraints();
713 checkForCeilings();
714}
715
716void ArtificialHorizonComponent::addRegion(const QString &regionName, bool enabled, const std::shared_ptr<LineList> &list,
717 bool ceiling)
718{
719 horizon.addRegion(regionName, enabled, list, ceiling);
720 appendLine(list);
721}
722
723bool ArtificialHorizon::altitudeConstraintsExist() const
724{
725 foreach (ArtificialHorizonEntity *horizon, m_HorizonList)
726 {
727 if (horizon->enabled())
728 return true;
729 }
730 return false;
731}
732
733const ArtificialHorizonEntity *ArtificialHorizon::getConstraintAbove(double azimuthDegrees, double altitudeDegrees,
734 const ArtificialHorizonEntity *ignore) const
735{
736 double closestAbove = 1e6;
737 const ArtificialHorizonEntity *entity = nullptr;
738
739 foreach (ArtificialHorizonEntity *horizon, m_HorizonList)
740 {
741 if (!horizon->enabled()) continue;
742 if (horizon == ignore) continue;
743 bool constraintExists = false;
744 double constraint = horizon->altitudeConstraint(azimuthDegrees, &constraintExists);
745 // This horizon doesn't constrain this azimuth.
746 if (!constraintExists) continue;
747
748 double altitudeDiff = constraint - altitudeDegrees;
749 if (altitudeDiff > 0 && constraint < closestAbove)
750 {
751 closestAbove = constraint;
752 entity = horizon;
753 }
754 }
755 return entity;
756}
757
758// Estimate the horizon contraint to .1 degrees.
759// This significantly speeds up computation.
760constexpr int PRECOMPUTED_RESOLUTION = 10;
761
762double ArtificialHorizon::altitudeConstraint(double azimuthDegrees) const
763{
764 if (precomputedConstraints.size() != 360 * PRECOMPUTED_RESOLUTION)
765 precomputeConstraints();
766 return precomputedConstraint(azimuthDegrees);
767}
768
769double ArtificialHorizon::altitudeConstraintInternal(double azimuthDegrees) const
770{
771 const ArtificialHorizonEntity *horizonBelow = getConstraintBelow(azimuthDegrees, 90.0, nullptr);
772 if (horizonBelow == nullptr)
773 return UNDEFINED_ALTITUDE;
774 bool ignore = false;
775 return horizonBelow->altitudeConstraint(azimuthDegrees, &ignore);
776}
777
778// Quantize the constraints to within .1 degrees (so there are 360*10=3600
779// precomputed values).
780void ArtificialHorizon::precomputeConstraints() const
781{
782 precomputedConstraints.clear();
783 precomputedConstraints.fill(0, 360 * PRECOMPUTED_RESOLUTION);
784 for (int i = 0; i < 360 * PRECOMPUTED_RESOLUTION; ++i)
785 {
786 const double az = i / static_cast<double>(PRECOMPUTED_RESOLUTION);
787 precomputedConstraints[i] = altitudeConstraintInternal(az);
788 }
789}
790
791void ArtificialHorizon::resetPrecomputeConstraints() const
792{
793 precomputedConstraints.clear();
794}
795
796double ArtificialHorizon::precomputedConstraint(double azimuth) const
797{
798 constexpr int maxval = 360 * PRECOMPUTED_RESOLUTION;
799 int index = azimuth * PRECOMPUTED_RESOLUTION + 0.5;
800 if (index == maxval)
801 index = 0;
802 if (index < 0 || index >= precomputedConstraints.size())
803 return UNDEFINED_ALTITUDE;
804 return precomputedConstraints[index];
805}
806
807const ArtificialHorizonEntity *ArtificialHorizon::getConstraintBelow(double azimuthDegrees, double altitudeDegrees,
808 const ArtificialHorizonEntity *ignore) const
809{
810 double closestBelow = -1e6;
811 const ArtificialHorizonEntity *entity = nullptr;
812
813 foreach (ArtificialHorizonEntity *horizon, m_HorizonList)
814 {
815 if (!horizon->enabled()) continue;
816 if (horizon == ignore) continue;
817 bool constraintExists = false;
818 double constraint = horizon->altitudeConstraint(azimuthDegrees, &constraintExists);
819 // This horizon doesn't constrain this azimuth.
820 if (!constraintExists) continue;
821
822 double altitudeDiff = constraint - altitudeDegrees;
823 if (altitudeDiff < 0 && constraint > closestBelow)
824 {
825 closestBelow = constraint;
826 entity = horizon;
827 }
828 }
829 return entity;
830}
831
832bool ArtificialHorizon::isAltitudeOK(double azimuthDegrees, double altitudeDegrees, QString *reason) const
833{
834 if (noCeilingConstraints)
835 {
836 const double constraint = altitudeConstraint(azimuthDegrees);
837 if (altitudeDegrees >= constraint)
838 return true;
839 if (reason != nullptr)
840 *reason = QString("altitude %1 < horizon %2").arg(altitudeDegrees, 0, 'f', 1).arg(constraint, 0, 'f', 1);
841 return false;
842 }
843 else
844 return isVisible(azimuthDegrees, altitudeDegrees, reason);
845}
846
847// An altitude is blocked (not visible) if either:
848// - there are constraints above and the closest above constraint is not a ceiling, or
849// - there are constraints below and the closest below constraint is a ceiling.
850bool ArtificialHorizon::isVisible(double azimuthDegrees, double altitudeDegrees, QString *reason) const
851{
852 const ArtificialHorizonEntity *above = getConstraintAbove(azimuthDegrees, altitudeDegrees);
853 if (above != nullptr && !above->ceiling())
854 {
855 if (reason != nullptr)
856 {
857 bool ignoreMe;
858 double constraint = above->altitudeConstraint(azimuthDegrees, &ignoreMe);
859 *reason = QString("altitude %1 < horizon %2").arg(altitudeDegrees, 0, 'f', 1).arg(constraint, 0, 'f', 1);
860 }
861 return false;
862 }
863 const ArtificialHorizonEntity *below = getConstraintBelow(azimuthDegrees, altitudeDegrees);
864 if (below != nullptr && below->ceiling())
865 {
866 if (reason != nullptr)
867 {
868 bool ignoreMe;
869 double constraint = below->altitudeConstraint(azimuthDegrees, &ignoreMe);
870 *reason = QString("altitude %1 > ceiling %2").arg(altitudeDegrees, 0, 'f', 1).arg(constraint, 0, 'f', 1);
871 }
872 return false;
873 }
874 return true;
875}
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:217
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
void setAlt(dms alt)
Sets Alt, the Altitude.
Definition skypoint.h:194
const dms & alt() const
Definition skypoint.h:281
void HorizontalToEquatorial(const dms *LST, const dms *lat)
Determine the (RA, Dec) coordinates of the SkyPoint from its (Altitude, Azimuth) coordinates,...
Definition skypoint.cpp:143
void setAz(dms az)
Sets Az, the Azimuth.
Definition skypoint.h:230
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 Sat Dec 21 2024 17:04:47 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.