]> git.mxchange.org Git - flightgear.git/blob - src/GUI/BaseDiagram.cxx
Work on launcher diagrams.
[flightgear.git] / src / GUI / BaseDiagram.cxx
1 // BaseDiagram.cxx - part of GUI launcher using Qt5
2 //
3 // Written by James Turner, started December 2014.
4 //
5 // Copyright (C) 2014 James Turner <zakalawe@mac.com>
6 //
7 // This program is free software; you can redistribute it and/or
8 // modify it under the terms of the GNU General Public License as
9 // published by the Free Software Foundation; either version 2 of the
10 // License, or (at your option) any later version.
11 //
12 // This program is distributed in the hope that it will be useful, but
13 // WITHOUT ANY WARRANTY; without even the implied warranty of
14 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 // General Public License for more details.
16 //
17 // You should have received a copy of the GNU General Public License
18 // along with this program; if not, write to the Free Software
19 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
20
21 #include "BaseDiagram.hxx"
22
23 #include <limits>
24
25 #include <QPainter>
26 #include <QDebug>
27 #include <QVector2D>
28 #include <QMouseEvent>
29
30 #include <Navaids/navrecord.hxx>
31 #include <Navaids/positioned.hxx>
32 #include <Airports/airport.hxx>
33
34 /* equatorial and polar earth radius */
35 const float rec  = 6378137;          // earth radius, equator (?)
36 const float rpol = 6356752.314f;      // earth radius, polar   (?)
37
38 //Returns Earth radius at a given latitude (Ellipsoide equation with two equal axis)
39 static float earth_radius_lat( float lat )
40 {
41     double a = cos(lat)/rec;
42     double b = sin(lat)/rpol;
43     return 1.0f / sqrt( a * a + b * b );
44 }
45
46 BaseDiagram::BaseDiagram(QWidget* pr) :
47     QWidget(pr),
48     m_autoScalePan(true),
49     m_wheelAngleDeltaAccumulator(0)
50 {
51     setSizePolicy(QSizePolicy::MinimumExpanding,
52                   QSizePolicy::MinimumExpanding);
53     setMinimumSize(100, 100);
54 }
55
56 QTransform BaseDiagram::transform() const
57 {
58     QTransform t;
59     t.translate(width() / 2, height() / 2); // center projection origin in the widget
60     t.scale(m_scale, m_scale);
61
62     // apply any pan offset that exists
63     t.translate(m_panOffset.x(), m_panOffset.y());
64     // center the bounding box (may not be at the origin)
65     t.translate(-m_bounds.center().x(), -m_bounds.center().y());
66     return t;
67 }
68
69 void BaseDiagram::clearIgnoredNavaids()
70 {
71     m_ignored.clear();
72 }
73
74 void BaseDiagram::addIgnoredNavaid(FGPositionedRef pos)
75 {
76     if (isNavaidIgnored(pos))
77         return;
78     m_ignored.push_back(pos);
79 }
80
81 void BaseDiagram::extendRect(QRectF &r, const QPointF &p)
82 {
83     if (p.x() < r.left()) {
84         r.setLeft(p.x());
85     } else if (p.x() > r.right()) {
86         r.setRight(p.x());
87     }
88
89     if (p.y() < r.top()) {
90         r.setTop(p.y());
91     } else if (p.y() > r.bottom()) {
92         r.setBottom(p.y());
93     }
94 }
95
96 void BaseDiagram::paintEvent(QPaintEvent* pe)
97 {
98     QPainter p(this);
99     p.setRenderHints(QPainter::Antialiasing);
100     p.fillRect(rect(), QColor(0x3f, 0x3f, 0x3f));
101
102     if (m_autoScalePan) {
103         // fit bounds within our available space, allowing for a margin
104         const int MARGIN = 32; // pixels
105         double ratioInX = (width() - MARGIN * 2) / m_bounds.width();
106         double ratioInY = (height() - MARGIN * 2) / m_bounds.height();
107         m_scale = std::min(ratioInX, ratioInY);
108     }
109
110     QTransform t(transform());
111     p.setTransform(t);
112
113     paintNavaids(&p);
114
115     paintContents(&p);
116 }
117
118 class MapFilter : public FGPositioned::TypeFilter
119 {
120 public:
121     MapFilter()
122     {
123       //  addType(FGPositioned::FIX);
124         addType(FGPositioned::AIRPORT);
125         addType(FGPositioned::NDB);
126         addType(FGPositioned::VOR);
127     }
128
129     virtual bool pass(FGPositioned* aPos) const
130     {
131         bool ok = TypeFilter::pass(aPos);
132         if (ok && (aPos->type() == FGPositioned::FIX)) {
133             // ignore fixes which end in digits
134             if (aPos->ident().length() > 4 && isdigit(aPos->ident()[3]) && isdigit(aPos->ident()[4])) {
135                 return false;
136             }
137         }
138
139         return ok;
140     }
141 };
142
143
144 void BaseDiagram::paintNavaids(QPainter* painter)
145 {
146     QTransform xf = painter->transform();
147     painter->setTransform(QTransform()); // reset to identity
148     QTransform invT = xf.inverted();
149     SGGeod topLeft = unproject(invT.map(QPointF(0,0)), m_projectionCenter);
150
151     double minRunwayLengthFt = (16 / m_scale) * SG_METER_TO_FEET;
152     // add 10nm fudge factor
153     double drawRangeNm = SGGeodesy::distanceNm(m_projectionCenter, topLeft) + 10.0;
154     //qDebug() << "draw range computed as:" << drawRangeNm;
155
156     MapFilter f;
157     FGPositionedList items = FGPositioned::findWithinRange(m_projectionCenter, drawRangeNm, &f);
158
159     // pass 0 - icons
160
161     FGPositionedList::const_iterator it;
162     for (it = items.begin(); it != items.end(); ++it) {
163         FGPositionedRef pos(*it);
164         bool drawAsIcon = true;
165         if (isNavaidIgnored(pos))
166             continue;
167
168         FGPositioned::Type ty(pos->type());
169         if (ty == FGPositioned::AIRPORT) {
170             FGAirport* apt = static_cast<FGAirport*>(pos.ptr());
171             if (apt->hasHardRunwayOfLengthFt(minRunwayLengthFt)) {
172
173                 drawAsIcon = false;
174                 painter->setTransform(xf);
175                 QVector<QLineF> lines = projectAirportRuwaysWithCenter(apt, m_projectionCenter);
176
177                 QPen pen(QColor(0x03, 0x83, 0xbf), 8);
178                 pen.setCosmetic(true);
179                 painter->setPen(pen);
180                 painter->drawLines(lines);
181
182                 QPen linePen(Qt::white, 2);
183                 linePen.setCosmetic(true);
184                 painter->setPen(linePen);
185                 painter->drawLines(lines);
186
187                 painter->resetTransform();
188             }
189         }
190
191         if (drawAsIcon) {
192             QPixmap pm = iconForPositioned(pos, false);
193             QPointF loc = xf.map(project(pos->geod()));
194
195             QPointF iconLoc = loc - QPointF(pm.width() >> 1, pm.height() >> 1);
196             painter->drawPixmap(iconLoc, pm);
197
198             painter->setPen(QColor(0x03, 0x83, 0xbf));
199
200             QString label;
201             if (FGAirport::isAirportType(pos.ptr())) {
202                 label = QString::fromStdString((*it)->name());
203             } else {
204                 label = QString::fromStdString((*it)->ident());
205             }
206
207             if (ty == FGPositioned::NDB) {
208                 FGNavRecord* nav = static_cast<FGNavRecord*>(pos.ptr());
209                 label.append("\n").append(QString::number(nav->get_freq() / 100));
210             } else if (ty == FGPositioned::VOR) {
211                 FGNavRecord* nav = static_cast<FGNavRecord*>(pos.ptr());
212                 label.append("\n").append(QString::number(nav->get_freq() / 100.0, 'f', 1));
213             }
214
215             QRect labelBox(loc.x() + (pm.width()/2) + 4, loc.y() - 50, 100, 100);
216             painter->drawText(labelBox, Qt::AlignVCenter | Qt::AlignLeft | Qt::TextWordWrap,
217                               label);
218         }
219     }
220
221     // restore transform
222     painter->setTransform(xf);
223 }
224
225 bool BaseDiagram::isNavaidIgnored(const FGPositionedRef &pos) const
226 {
227     return m_ignored.contains(pos);
228 }
229
230 void BaseDiagram::mousePressEvent(QMouseEvent *me)
231 {
232     m_lastMousePos = me->pos();
233     m_didPan = false;
234 }
235
236 void BaseDiagram::mouseMoveEvent(QMouseEvent *me)
237 {
238     m_autoScalePan = false;
239
240     QPointF delta = me->pos() - m_lastMousePos;
241     m_lastMousePos = me->pos();
242
243     // offset is stored in metres so we don't have to modify it when
244     // zooming
245     m_panOffset += (delta / m_scale);
246     m_didPan = true;
247
248     update();
249 }
250
251 int intSign(int v)
252 {
253     return (v == 0) ? 0 : (v < 0) ? -1 : 1;
254 }
255
256 void BaseDiagram::wheelEvent(QWheelEvent *we)
257 {
258     m_autoScalePan = false;
259
260     int delta = we->angleDelta().y();
261     if (delta == 0)
262         return;
263
264     if (intSign(m_wheelAngleDeltaAccumulator) != intSign(delta)) {
265         m_wheelAngleDeltaAccumulator = 0;
266     }
267
268     m_wheelAngleDeltaAccumulator += delta;
269     if (m_wheelAngleDeltaAccumulator > 120) {
270         m_wheelAngleDeltaAccumulator = 0;
271         m_scale *= 2.0;
272     } else if (m_wheelAngleDeltaAccumulator < -120) {
273         m_wheelAngleDeltaAccumulator = 0;
274         m_scale *= 0.5;
275     }
276
277     update();
278 }
279
280 void BaseDiagram::paintContents(QPainter* painter)
281 {
282 }
283
284 void BaseDiagram::recomputeBounds(bool resetZoom)
285 {
286     m_bounds = QRectF();
287     doComputeBounds();
288
289     if (resetZoom) {
290         m_autoScalePan = true;
291         m_scale = 1.0;
292         m_panOffset = QPointF();
293     }
294
295     update();
296 }
297
298 void BaseDiagram::doComputeBounds()
299 {
300     // no-op in the base class
301 }
302
303 void BaseDiagram::extendBounds(const QPointF& p)
304 {
305     extendRect(m_bounds, p);
306 }
307
308 QPointF BaseDiagram::project(const SGGeod& geod, const SGGeod& center)
309 {
310     double r = earth_radius_lat(geod.getLatitudeRad());
311     double ref_lat = center.getLatitudeRad(),
312     ref_lon = center.getLongitudeRad(),
313     lat = geod.getLatitudeRad(),
314     lon = geod.getLongitudeRad(),
315     lonDiff = lon - ref_lon;
316
317     double c = acos( sin(ref_lat) * sin(lat) + cos(ref_lat) * cos(lat) * cos(lonDiff) );
318     if (c == 0.0) {
319         // angular distance from center is 0
320         return QPointF(0.0, 0.0);
321     }
322
323     double k = c / sin(c);
324     double x, y;
325     if (ref_lat == (90 * SG_DEGREES_TO_RADIANS))
326     {
327         x = (SGD_PI / 2 - lat) * sin(lonDiff);
328         y = -(SGD_PI / 2 - lat) * cos(lonDiff);
329     }
330     else if (ref_lat == -(90 * SG_DEGREES_TO_RADIANS))
331     {
332         x = (SGD_PI / 2 + lat) * sin(lonDiff);
333         y = (SGD_PI / 2 + lat) * cos(lonDiff);
334     }
335     else
336     {
337         x = k * cos(lat) * sin(lonDiff);
338         y = k * ( cos(ref_lat) * sin(lat) - sin(ref_lat) * cos(lat) * cos(lonDiff) );
339     }
340
341     return QPointF(x, -y) * r;
342 }
343
344 SGGeod BaseDiagram::unproject(const QPointF& xy, const SGGeod& center)
345 {
346     double r = earth_radius_lat(center.getLatitudeRad());
347     double lat = 0,
348            lon = 0,
349            ref_lat = center.getLatitudeRad(),
350            ref_lon = center.getLongitudeRad(),
351            rho = QVector2D(xy).length(),
352            c = rho/r;
353
354     if (rho == 0) {
355         return center;
356     }
357
358     double x = xy.x(), y = xy.y();
359     lat = asin( cos(c) * sin(ref_lat) + (y * sin(c) * cos(ref_lat)) / rho);
360
361     if (ref_lat == (90 * SG_DEGREES_TO_RADIANS)) // north pole
362     {
363         lon = ref_lon + atan(-x/y);
364     }
365     else if (ref_lat == -(90 * SG_DEGREES_TO_RADIANS)) // south pole
366     {
367         lon = ref_lon + atan(x/y);
368     }
369     else
370     {
371         lon = ref_lon + atan(x* sin(c) / (rho * cos(ref_lat) * cos(c) - y * sin(ref_lat) * sin(c)));
372     }
373
374     return SGGeod::fromRad(lon, lat);
375 }
376
377 QPointF BaseDiagram::project(const SGGeod& geod) const
378 {
379     return project(geod, m_projectionCenter);
380 }
381
382 QPixmap BaseDiagram::iconForPositioned(const FGPositionedRef& pos, bool small)
383 {
384     // if airport type, check towered or untowered
385
386     bool isTowered = false;
387     if (FGAirport::isAirportType(pos)) {
388         FGAirport* apt = static_cast<FGAirport*>(pos.ptr());
389         isTowered = apt->hasTower();
390     }
391
392     switch (pos->type()) {
393     case FGPositioned::VOR:
394         if (static_cast<FGNavRecord*>(pos.ptr())->isVORTAC())
395             return QPixmap(":/vortac-icon");
396
397         if (static_cast<FGNavRecord*>(pos.ptr())->hasDME())
398             return QPixmap(":/vor-dme-icon");
399
400         return QPixmap(":/vor-icon");
401
402     case FGPositioned::AIRPORT:
403         return iconForAirport(static_cast<FGAirport*>(pos.ptr()));
404
405     case FGPositioned::HELIPORT:
406         return QPixmap(":/heliport-icon");
407     case FGPositioned::SEAPORT:
408         return QPixmap(isTowered ? ":/seaport-tower-icon" : ":/seaport-icon");
409     case FGPositioned::NDB:
410         return QPixmap(small ? ":/ndb-small-icon" : ":/ndb-icon");
411     case FGPositioned::FIX:
412         return QPixmap(":/waypoint-icon");
413
414     default:
415         break;
416     }
417
418     return QPixmap();
419 }
420
421 QPixmap BaseDiagram::iconForAirport(FGAirport* apt)
422 {
423     if (!apt->hasHardRunwayOfLengthFt(1500)) {
424         return QPixmap(apt->hasTower() ? ":/airport-tower-icon" : ":/airport-icon");
425     }
426
427     if (apt->hasHardRunwayOfLengthFt(8500)) {
428         QPixmap result(32, 32);
429         result.fill(Qt::transparent);
430         {
431             QPainter p(&result);
432             p.setRenderHint(QPainter::Antialiasing, true);
433             QRectF b = result.rect().adjusted(4, 4, -4, -4);
434             QVector<QLineF> lines = projectAirportRuwaysIntoRect(apt, b);
435
436             p.setPen(QPen(QColor(0x03, 0x83, 0xbf), 8));
437             p.drawLines(lines);
438
439             p.setPen(QPen(Qt::white, 2));
440             p.drawLines(lines);
441         }
442         return result;
443     }
444
445     QPixmap result(25, 25);
446     result.fill(Qt::transparent);
447
448     {
449         QPainter p(&result);
450         p.setRenderHint(QPainter::Antialiasing, true);
451         p.setPen(Qt::NoPen);
452
453         p.setBrush(apt->hasTower() ? QColor(0x03, 0x83, 0xbf) :
454                                      QColor(0x9b, 0x5d, 0xa2));
455         p.drawEllipse(QPointF(13, 13), 10, 10);
456
457         FGRunwayRef r = apt->longestRunway();
458
459         p.setPen(QPen(Qt::white, 2));
460         p.translate(13, 13);
461         p.rotate(r->headingDeg());
462         p.drawLine(0, -8, 0, 8);
463     }
464
465     return result;
466 }
467
468 QVector<QLineF> BaseDiagram::projectAirportRuwaysWithCenter(FGAirportRef apt, const SGGeod& c)
469 {
470     QVector<QLineF> r;
471
472     const FGRunwayList& runways(apt->getRunwaysWithoutReciprocals());
473     FGRunwayList::const_iterator it;
474
475     for (it = runways.begin(); it != runways.end(); ++it) {
476         FGRunwayRef rwy = *it;
477         QPointF p1 = project(rwy->geod(), c);
478         QPointF p2 = project(rwy->end(), c);
479         r.append(QLineF(p1, p2));
480     }
481
482     return r;
483 }
484
485 QVector<QLineF> BaseDiagram::projectAirportRuwaysIntoRect(FGAirportRef apt, const QRectF &bounds)
486 {
487     QVector<QLineF> r = projectAirportRuwaysWithCenter(apt, apt->geod());
488
489     QRectF extent;
490     Q_FOREACH(const QLineF& l, r) {
491         extendRect(extent, l.p1());
492         extendRect(extent, l.p2());
493     }
494
495  // find constraining scale factor
496     double ratioInX = bounds.width() / extent.width();
497     double ratioInY = bounds.height() / extent.height();
498
499     QTransform t;
500     t.translate(bounds.left(), bounds.top());
501     t.scale(std::min(ratioInX, ratioInY),
502             std::min(ratioInX, ratioInY));
503     t.translate(-extent.left(), -extent.top()); // move unscaled to 0,0
504
505     for (int i=0; i<r.size(); ++i) {
506         r[i] = t.map(r[i]);
507     }
508
509     return r;
510 }