swift
radarcomponent.cpp
1 // SPDX-FileCopyrightText: Copyright (C) 2019 swift Project Community / Contributors
2 // SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-swift-pilot-client-1
3 
5 
6 #include <QStringBuilder>
7 #include <QtMath>
8 
9 #include "ui_radarcomponent.h"
10 
13 #include "gui/guiapplication.h"
14 #include "gui/infoarea.h"
16 
17 using namespace swift::misc;
18 using namespace swift::misc::aviation;
19 using namespace swift::misc::simulation;
20 using namespace swift::misc::geo;
21 using namespace swift::misc::physical_quantities;
22 using namespace swift::gui::views;
23 
24 namespace swift::gui::components
25 {
26  CRadarComponent::CRadarComponent(QWidget *parent)
27  : QFrame(parent), ui(new Ui::CRadarComponent), m_tagFont(QApplication::font())
28  {
29  ui->setupUi(this);
30 
31  ui->gv_RadarView->setScene(&m_scene);
32 
33  ui->cb_RadarRange->addItem(QString::number(0.5) % u" nm", 0.5);
34  for (int r = 1; r <= 9; ++r) { ui->cb_RadarRange->addItem(QString::number(r) % u" nm", r); }
35  for (int r = 10; r <= 90; r += 10) { ui->cb_RadarRange->addItem(QString::number(r) % u" nm", r); }
36 
37  ui->cb_RadarRange->setCurrentText(QString::number(m_rangeNM) % u" nm");
38  ui->sb_FontSize->setRange(1, 100);
39  ui->sb_FontSize->setValue(QApplication::font().pointSize());
40 
41  connect(ui->gv_RadarView, &CRadarView::radarViewResized, this, &CRadarComponent::fitInView);
42  connect(ui->gv_RadarView, &CRadarView::zoomEvent, this, &CRadarComponent::changeRangeInSteps);
43  connect(&m_updateTimer, &QTimer::timeout, this, &CRadarComponent::refreshTargets);
44  connect(&m_headingTimer, &QTimer::timeout, this, &CRadarComponent::rotateView);
45 
46  connect(ui->cb_RadarRange, qOverload<int>(&QComboBox::currentIndexChanged), this,
47  &CRadarComponent::changeRangeFromUserSelection);
48  connect(ui->sb_FontSize, qOverload<int>(&QSpinBox::valueChanged), this, &CRadarComponent::updateFont);
49  connect(ui->cb_Callsign, &QCheckBox::toggled, this, &CRadarComponent::refreshTargets);
50  connect(ui->cb_Heading, &QCheckBox::toggled, this, &CRadarComponent::refreshTargets);
51  connect(ui->cb_Altitude, &QCheckBox::toggled, this, &CRadarComponent::refreshTargets);
52  connect(ui->cb_GroundSpeed, &QCheckBox::toggled, this, &CRadarComponent::refreshTargets);
53  connect(ui->cb_Grid, &QCheckBox::toggled, this, &CRadarComponent::toggleGrid);
54 
55  prepareScene();
56 
57  m_updateTimer.start(5000);
58  m_headingTimer.start(50);
59  }
60 
62 
64  {
66  const bool c = connect(this->getParentInfoArea(), &CInfoArea::changedInfoAreaTabBarIndex, this,
67  &CRadarComponent::onInfoAreaTabBarChanged);
68  Q_ASSERT_X(c, Q_FUNC_INFO, "failed connect");
69  Q_ASSERT_X(parentDockableWidget, Q_FUNC_INFO, "missing parent");
70  return c && parentDockableWidget;
71  }
72 
73  void CRadarComponent::prepareScene()
74  {
75  m_scene.addItem(&m_center);
76  m_scene.addItem(&m_macroGraticule);
77  m_scene.addItem(&m_microGraticule);
78  m_scene.addItem(&m_radials);
79  m_scene.addItem(&m_radarTargets);
80  m_radarTargetPen.setCosmetic(true);
81  addCenter();
82  addGraticules();
83  addRadials();
84  }
85 
86  void CRadarComponent::addCenter()
87  {
88  QPen pen(Qt::white, 1);
89  pen.setCosmetic(true);
90  QGraphicsLineItem *lix = new QGraphicsLineItem { QLineF(-5.0, 0.0, 5.0, 0.0), &m_center };
91  lix->setFlags(QGraphicsItem::ItemIgnoresTransformations);
92  lix->setPen(pen);
93 
94  QGraphicsLineItem *liy = new QGraphicsLineItem(QLineF(0.0, -5.0, 0.0, 5.0), &m_center);
95  liy->setFlags(QGraphicsItem::ItemIgnoresTransformations);
96  liy->setPen(pen);
97  }
98 
99  void CRadarComponent::addGraticules()
100  {
101  QPen pen(Qt::white, 1);
102  pen.setCosmetic(true);
103 
104  // Macro graticule, drawn as full line at every 10 nm
105  for (int range = 10; range <= 100; range += 10)
106  {
107  QGraphicsEllipseItem *circle =
108  new QGraphicsEllipseItem(-range, -range, 2.0 * range, 2.0 * range, &m_macroGraticule);
109  circle->setPen(pen);
110  }
111  pen = QPen(Qt::gray, 1, Qt::DashLine);
112  pen.setCosmetic(true);
113 
114  // Micro graticule, drawn as dash line at every 2.5 nm
115  for (qreal range = 1; range <= 3; ++range)
116  {
117  QGraphicsEllipseItem *circle =
118  new QGraphicsEllipseItem(-range * 2.5, -range * 2.5, 5.0 * range, 5.0 * range, &m_microGraticule);
119  circle->setPen(pen);
120  }
121  }
122 
123  void CRadarComponent::addRadials()
124  {
125  QPen pen(Qt::gray, 1, Qt::DashDotDotLine);
126  pen.setCosmetic(true);
127 
128  for (int angle = 0; angle < 360; angle += 30)
129  {
130  const QLineF line({ 0.0, 0.0 }, polarPoint(1000.0, qDegreesToRadians(static_cast<qreal>(angle))));
131  QGraphicsLineItem *li = new QGraphicsLineItem(line, &m_radials);
132  li->setFlags(QGraphicsItem::ItemIgnoresTransformations);
133  li->setPen(pen);
134  }
135  }
136 
137  void CRadarComponent::refreshTargets()
138  {
139  if (!sGui || sGui->isShuttingDown()) { return; }
140 
141  qDeleteAll(m_radarTargets.childItems());
142 
144  {
145  if (isVisibleWidget())
146  {
148  for (const CSimulatedAircraft &sa : aircraft)
149  {
150  const double distanceNM = sa.getRelativeDistance().value(CLengthUnit::NM());
151  const double bearingRad = sa.getRelativeBearing().value(CAngleUnit::rad());
152  const int groundSpeedKts = sa.getGroundSpeed().valueInteger(CSpeedUnit::kts());
153 
154  QPointF position(polarPoint(distanceNM, bearingRad));
155 
156  QGraphicsEllipseItem *dot = new QGraphicsEllipseItem(-2.0, -2.0, 4.0, 4.0, &m_radarTargets);
157  dot->setPos(position);
158  dot->setPen(m_radarTargetPen);
159  dot->setBrush(m_radarTargetPen.color());
160  dot->setFlags(QGraphicsItem::ItemIgnoresTransformations);
161 
162  QGraphicsTextItem *tag = new QGraphicsTextItem(&m_radarTargets);
163  QString tagText;
164  if (ui->cb_Callsign->isChecked()) { tagText += sa.getCallsignAsString() % u"\n"; }
165  if (ui->cb_Altitude->isChecked())
166  {
167  int flightLeveL = sa.getAltitude().valueInteger(CLengthUnit::ft()) / 100;
168  tagText += u"FL" % QStringLiteral("%1").arg(flightLeveL, 3, 10, QChar('0'));
169  }
170  if (ui->cb_GroundSpeed->isChecked())
171  {
172  if (!tagText.isEmpty()) tagText += QStringLiteral(" ");
173  tagText += QString::number(groundSpeedKts) % u" kt";
174  }
175 
176  tag->setPlainText(tagText);
177  tag->setFont(m_tagFont);
178  tag->setPos(position);
179  tag->setDefaultTextColor(Qt::green);
180  tag->setFlags(QGraphicsItem::ItemIgnoresTransformations);
181 
182  if (ui->cb_Heading->isChecked() && groundSpeedKts > 3.0)
183  {
184  const double headingRad = sa.getHeading().value(CAngleUnit::rad());
185  QPen pen(Qt::green, 1);
186  pen.setCosmetic(true);
187  QGraphicsLineItem *li =
188  new QGraphicsLineItem(QLineF({ 0.0, 0.0 }, polarPoint(5.0, headingRad)), &m_radarTargets);
189  li->setPos(position);
190  li->setPen(pen);
191  }
192  }
193  }
194  }
195  }
196 
197  void CRadarComponent::rotateView()
198  {
200  {
201  if (isVisibleWidget())
202  {
203  int headingDegree = 0;
204  if (!ui->cb_LockNorth->isChecked())
205  {
207  CAngleUnit::deg());
208  }
209 
210  if (m_rotatenAngle != headingDegree)
211  {
212  // Rotations are summed up, hence rotate back before applying the new rotation.
213  // Doing a global transformation reset will not work as it resets also zooming.
214  ui->gv_RadarView->rotate(m_rotatenAngle);
215  ui->gv_RadarView->rotate(-headingDegree);
216  m_rotatenAngle = headingDegree;
217  }
218  }
219  }
220  }
221 
222  void CRadarComponent::toggleGrid(bool checked)
223  {
224  m_macroGraticule.setVisible(checked);
225  m_microGraticule.setVisible(checked);
226  m_radials.setVisible(checked);
227  }
228 
229  void CRadarComponent::fitInView()
230  {
231  ui->gv_RadarView->fitInView(-m_rangeNM, -m_rangeNM, 2.0 * m_rangeNM, 2.0 * m_rangeNM, Qt::KeepAspectRatio);
232  }
233 
234  void CRadarComponent::changeRangeInSteps(bool zoomIn)
235  {
236  qreal direction = zoomIn ? 1.0 : -1.0;
237  double factor = 10.0;
238  if (m_rangeNM < 10.0 || (qFuzzyCompare(m_rangeNM, 10.0) && zoomIn)) { factor = 1.0; }
239 
240  if (m_rangeNM < 1.0 || (qFuzzyCompare(m_rangeNM, 1.0) && zoomIn)) { factor = 0.5; }
241 
242  m_rangeNM = m_rangeNM - direction * factor;
243  m_rangeNM = qMin(90.0, qMax(0.5, m_rangeNM));
244  ui->cb_RadarRange->setCurrentText(QString::number(m_rangeNM) % u" nm");
245  fitInView();
246  }
247 
248  void CRadarComponent::changeRangeFromUserSelection(int index)
249  {
250  double range = ui->cb_RadarRange->itemData(index).toDouble();
251  if (!qFuzzyCompare(m_rangeNM, range))
252  {
253  m_rangeNM = range;
254  fitInView();
255  }
256  }
257 
258  void CRadarComponent::updateFont(int pointSize)
259  {
260  m_tagFont.setPointSize(pointSize);
261  this->refreshTargets();
262  }
263 
264  void CRadarComponent::onInfoAreaTabBarChanged(int index)
265  {
266  Q_UNUSED(index)
267 
268  // ignore in those cases
269  if (!this->isVisibleWidget()) return;
270  if (this->isParentDockWidgetFloating()) return;
271  if (!sGui->getIContextNetwork()->isConnected()) return;
272 
273  // here I know I am the selected widget, update, but keep GUI responsive (hence I use a timer)
274  QPointer<CRadarComponent> myself(this);
275  QTimer::singleShot(1000, this, [=] {
276  if (!myself) { return; }
277  myself->refreshTargets();
278  });
279  }
280 
281  QPointF CRadarComponent::polarPoint(double distance, double angleRadians)
282  {
283  angleRadians = -angleRadians; // conversion assumes angles are counterclockwise
284 
285  // standard conversion from https://en.wikipedia.org/wiki/Polar_coordinate_system
286  QPointF p(distance * qCos(angleRadians), distance * qSin(angleRadians));
287 
288  // conversion yields a coordinate system
289  // in which North=(1,0) and East=(-1,0)
290  // but we want North=(0,-1) and East=(0,1)
291  // (QGraphicsView y axis increases downwards)
292  std::swap(p.rx(), p.ry());
293  p.setX(-p.x());
294  p.setY(-p.y());
295  return p;
296  }
297 } // namespace swift::gui::components
const context::IContextOwnAircraft * getIContextOwnAircraft() const
Direct access to contexts if a CCoreFacade has been initialized.
const context::IContextNetwork * getIContextNetwork() const
Direct access to contexts if a CCoreFacade has been initialized.
bool isShuttingDown() const
Is application shutting down?
virtual swift::misc::simulation::CSimulatedAircraftList getAircraftInRange() const =0
Aircraft list.
virtual bool isConnected() const =0
Network connected?
virtual swift::misc::aviation::CAircraftSituation getOwnAircraftSituation() const =0
Get own aircraft.
Specialized class for dock widgets serving as info area.
CInfoArea * getParentInfoArea() const
The parent info area.
bool isParentDockWidgetFloating() const
Is the parent dockable widget floating?
virtual bool setParentDockWidgetInfoArea(CDockWidgetInfoArea *parentDockableWidget)
Corresponding dockable widget in info area.
void changedInfoAreaTabBarIndex(int index)
Tab bar changed.
GUI displaying a radar like view with aircrafts nearby.
virtual bool setParentDockWidgetInfoArea(swift::gui::CDockWidgetInfoArea *parentDockableWidget)
Corresponding dockable widget in info area.
const CHeading & getHeading() const
Get heading.
int valueInteger(MU unit) const
As integer value.
Comprehensive information of an aircraft.
Value object encapsulating a list of aircraft.
SWIFT_GUI_EXPORT swift::gui::CGuiApplication * sGui
Single instance of GUI application object.
High level reusable GUI components.
Definition: aboutdialog.cpp:13
Views, mainly QTableView.
Free functions in swift::misc.
void swap(Optional< T > &a, Optional< T > &b) noexcept(std::is_nothrow_swappable_v< T >)
Efficient swap for two Optional objects.
Definition: optional.h:132
auto singleShot(int msec, QObject *target, F &&task)
Starts a single-shot timer which will call a task in the thread of the given object when it times out...
Definition: threadutils.h:30