]> git.mxchange.org Git - flightgear.git/blob - src/GUI/BaseDiagram.cxx
Fix unproject / navaid drawing.
[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 void BaseDiagram::paintAirplaneIcon(QPainter* painter, const SGGeod& geod, int headingDeg)
119 {
120     QPointF pos = project(geod);
121     QPixmap pix(":/airplane-icon");
122     pos = painter->transform().map(pos);
123     painter->resetTransform();
124     painter->translate(pos.x(), pos.y());
125     painter->rotate(headingDeg);
126
127     painter->setRenderHint(QPainter::SmoothPixmapTransform, true);
128     QRect airplaneIconRect = pix.rect();
129     airplaneIconRect.moveCenter(QPoint(0,0));
130     painter->drawPixmap(airplaneIconRect, pix);
131 }
132
133 class MapFilter : public FGPositioned::TypeFilter
134 {
135 public:
136     MapFilter()
137     {
138       //  addType(FGPositioned::FIX);
139         addType(FGPositioned::AIRPORT);
140         addType(FGPositioned::HELIPORT);
141         addType(FGPositioned::SEAPORT);
142         addType(FGPositioned::NDB);
143         addType(FGPositioned::VOR);
144     }
145
146     virtual bool pass(FGPositioned* aPos) const
147     {
148         bool ok = TypeFilter::pass(aPos);
149         if (ok && (aPos->type() == FGPositioned::FIX)) {
150             // ignore fixes which end in digits
151             if (aPos->ident().length() > 4 && isdigit(aPos->ident()[3]) && isdigit(aPos->ident()[4])) {
152                 return false;
153             }
154         }
155
156         return ok;
157     }
158 };
159
160
161 void BaseDiagram::paintNavaids(QPainter* painter)
162 {
163     QTransform xf = painter->transform();
164     painter->setTransform(QTransform()); // reset to identity
165     QTransform invT = xf.inverted();
166
167
168     SGGeod topLeft = unproject(invT.map(rect().topLeft()), m_projectionCenter);
169     SGGeod viewCenter = unproject(invT.map(rect().center()), m_projectionCenter);
170     SGGeod bottomRight = unproject(invT.map(rect().bottomRight()), m_projectionCenter);
171
172     double minRunwayLengthFt = (16 / m_scale) * SG_METER_TO_FEET;
173
174     double drawRangeNm = std::max(SGGeodesy::distanceNm(viewCenter, topLeft),
175                                   SGGeodesy::distanceNm(viewCenter, bottomRight));
176
177     MapFilter f;
178     FGPositionedList items = FGPositioned::findWithinRange(viewCenter, drawRangeNm, &f);
179
180     m_labelRects.clear();
181     m_labelRects.reserve(items.size());
182
183     FGPositionedList::const_iterator it;
184     for (it = items.begin(); it != items.end(); ++it) {
185         FGPositionedRef pos(*it);
186         bool drawAsIcon = true;
187         if (isNavaidIgnored(pos))
188             continue;
189
190         FGPositioned::Type ty(pos->type());
191         if (ty == FGPositioned::AIRPORT) {
192             FGAirport* apt = static_cast<FGAirport*>(pos.ptr());
193             if (apt->hasHardRunwayOfLengthFt(minRunwayLengthFt)) {
194
195                 drawAsIcon = false;
196                 painter->setTransform(xf);
197                 QVector<QLineF> lines = projectAirportRuwaysWithCenter(apt, m_projectionCenter);
198
199                 QPen pen(QColor(0x03, 0x83, 0xbf), 8);
200                 pen.setCosmetic(true);
201                 painter->setPen(pen);
202                 painter->drawLines(lines);
203
204                 QPen linePen(Qt::white, 2);
205                 linePen.setCosmetic(true);
206                 painter->setPen(linePen);
207                 painter->drawLines(lines);
208
209                 painter->resetTransform();
210             }
211         }
212
213         if (drawAsIcon) {
214             QPixmap pm = iconForPositioned(pos);
215             QPointF loc = xf.map(project(pos->geod()));
216             QRect iconRect = pm.rect();
217             iconRect.moveCenter(loc.toPoint());
218             painter->drawPixmap(iconRect, pm);
219             bool isNDB = (ty == FGPositioned::NDB);
220
221        // compute label text so we can measure it
222             QString label;
223             if (FGAirport::isAirportType(pos.ptr())) {
224                 label = QString::fromStdString((*it)->name());
225             } else {
226                 label = QString::fromStdString((*it)->ident());
227             }
228
229             if (ty == FGPositioned::NDB) {
230                 FGNavRecord* nav = static_cast<FGNavRecord*>(pos.ptr());
231                 label.append("\n").append(QString::number(nav->get_freq() / 100));
232             } else if (ty == FGPositioned::VOR) {
233                 FGNavRecord* nav = static_cast<FGNavRecord*>(pos.ptr());
234                 label.append("\n").append(QString::number(nav->get_freq() / 100.0, 'f', 1));
235             }
236
237             QRect textBounds = painter->boundingRect(QRect(0, 0, 100, 100),
238                                                      Qt::TextWordWrap, label);
239             int textFlags;
240             textBounds = rectAndFlagsForLabel(pos->guid(), iconRect,
241                                               textBounds.size(),
242                                               textFlags);
243
244             painter->setPen(isNDB ? QColor(0x9b, 0x5d, 0xa2) : QColor(0x03, 0x83, 0xbf));
245             painter->drawText(textBounds, textFlags, label);
246         }
247     }
248
249     // restore transform
250     painter->setTransform(xf);
251 }
252
253 bool BaseDiagram::isNavaidIgnored(const FGPositionedRef &pos) const
254 {
255     return m_ignored.contains(pos);
256 }
257
258 bool BaseDiagram::isLabelRectAvailable(const QRect &r) const
259 {
260     Q_FOREACH(const QRect& lr, m_labelRects) {
261         if (lr.intersects(r))
262             return false;
263     }
264
265     return true;
266 }
267
268 int BaseDiagram::textFlagsForLabelPosition(LabelPosition pos)
269 {
270 #if 0
271     switch (pos) {
272     case LABEL_RIGHT:       return Qt::AlignLeft | Qt::AlignVCenter;
273     case LABEL_ABOVE:       return Qt::AlignHCenter | Qt::A
274     }
275 #endif
276     return 0;
277 }
278
279 QRect BaseDiagram::rectAndFlagsForLabel(PositionedID guid, const QRect& item,
280                                         const QSize &bounds,
281                                         int& flags) const
282 {
283     m_labelRects.append(item);
284     int pos = m_labelPositions.value(guid, LABEL_RIGHT);
285     bool firstAttempt = true;
286     flags = Qt::TextWordWrap;
287
288     while (pos < LAST_POSITION) {
289         QRect r = labelPositioned(item, bounds, static_cast<LabelPosition>(pos));
290         if (isLabelRectAvailable(r)) {
291             m_labelRects.append(r);
292             m_labelPositions[guid] = static_cast<LabelPosition>(pos);
293             flags |= textFlagsForLabelPosition(static_cast<LabelPosition>(pos));
294             return r;
295         } else if (firstAttempt && (pos != LABEL_RIGHT)) {
296             pos = LABEL_RIGHT;
297         } else {
298             ++pos;
299         }
300
301         firstAttempt = false;
302     }
303
304     return QRect(item.x(), item.y(), bounds.width(), bounds.height());
305 }
306
307 QRect BaseDiagram::labelPositioned(const QRect& itemRect,
308                                    const QSize& bounds,
309                                    LabelPosition lp) const
310 {
311     const int SHORT_MARGIN = 4;
312     const int DIAGONAL_MARGIN = 12;
313
314     QPoint topLeft = itemRect.topLeft();
315
316     switch (lp) {
317     // cardinal compass points are short (close in)
318     case LABEL_RIGHT:
319         topLeft = QPoint(itemRect.right() + SHORT_MARGIN,
320                      itemRect.center().y() - bounds.height() / 2);
321         break;
322     case LABEL_ABOVE:
323         topLeft = QPoint(itemRect.center().x() - (bounds.width() / 2),
324                      itemRect.top() - (SHORT_MARGIN + bounds.height()));
325         break;
326     case LABEL_BELOW:
327         topLeft = QPoint(itemRect.center().x() - (bounds.width() / 2),
328                      itemRect.bottom() + SHORT_MARGIN);
329         break;
330     case LABEL_LEFT:
331         topLeft = QPoint(itemRect.left() - (SHORT_MARGIN + bounds.width()),
332                      itemRect.center().y() - bounds.height() / 2);
333         break;
334
335     // first diagonals are further out (to hopefully have a better chance
336     // of finding clear space
337
338     case LABEL_NE:
339         topLeft = QPoint(itemRect.right() + DIAGONAL_MARGIN,
340                      itemRect.top() - (DIAGONAL_MARGIN + bounds.height()));
341         break;
342
343     case LABEL_NW:
344         topLeft = QPoint(itemRect.left() - (DIAGONAL_MARGIN + bounds.width()),
345                      itemRect.top() - (DIAGONAL_MARGIN + bounds.height()));
346         break;
347
348     case LABEL_SE:
349         topLeft = QPoint(itemRect.right() + DIAGONAL_MARGIN,
350                      itemRect.bottom() + DIAGONAL_MARGIN);
351         break;
352
353     case LABEL_SW:
354         topLeft = QPoint(itemRect.left() - (DIAGONAL_MARGIN + bounds.width()),
355                      itemRect.bottom() + DIAGONAL_MARGIN);
356         break;
357     default:
358         qWarning() << Q_FUNC_INFO << "Implement me";
359
360     }
361
362     return QRect(topLeft, bounds);
363 }
364
365 void BaseDiagram::mousePressEvent(QMouseEvent *me)
366 {
367     m_lastMousePos = me->pos();
368     m_didPan = false;
369 }
370
371 void BaseDiagram::mouseMoveEvent(QMouseEvent *me)
372 {
373     m_autoScalePan = false;
374
375     QPointF delta = me->pos() - m_lastMousePos;
376     m_lastMousePos = me->pos();
377
378     // offset is stored in metres so we don't have to modify it when
379     // zooming
380     m_panOffset += (delta / m_scale);
381     m_didPan = true;
382
383     update();
384 }
385
386 int intSign(int v)
387 {
388     return (v == 0) ? 0 : (v < 0) ? -1 : 1;
389 }
390
391 void BaseDiagram::wheelEvent(QWheelEvent *we)
392 {
393     m_autoScalePan = false;
394
395     int delta = we->angleDelta().y();
396     if (delta == 0)
397         return;
398
399     if (intSign(m_wheelAngleDeltaAccumulator) != intSign(delta)) {
400         m_wheelAngleDeltaAccumulator = 0;
401     }
402
403     m_wheelAngleDeltaAccumulator += delta;
404     if (m_wheelAngleDeltaAccumulator > 120) {
405         m_wheelAngleDeltaAccumulator = 0;
406         m_scale *= 2.0;
407     } else if (m_wheelAngleDeltaAccumulator < -120) {
408         m_wheelAngleDeltaAccumulator = 0;
409         m_scale *= 0.5;
410     }
411
412     update();
413 }
414
415 void BaseDiagram::paintContents(QPainter* painter)
416 {
417 }
418
419 void BaseDiagram::recomputeBounds(bool resetZoom)
420 {
421     m_bounds = QRectF();
422     doComputeBounds();
423
424     if (resetZoom) {
425         m_autoScalePan = true;
426         m_scale = 1.0;
427         m_panOffset = QPointF();
428     }
429
430     update();
431 }
432
433 void BaseDiagram::doComputeBounds()
434 {
435     // no-op in the base class
436 }
437
438 void BaseDiagram::extendBounds(const QPointF& p)
439 {
440     extendRect(m_bounds, p);
441 }
442
443 QPointF BaseDiagram::project(const SGGeod& geod, const SGGeod& center)
444 {
445     double r = earth_radius_lat(geod.getLatitudeRad());
446     double ref_lat = center.getLatitudeRad(),
447     ref_lon = center.getLongitudeRad(),
448     lat = geod.getLatitudeRad(),
449     lon = geod.getLongitudeRad(),
450     lonDiff = lon - ref_lon;
451
452     double c = acos( sin(ref_lat) * sin(lat) + cos(ref_lat) * cos(lat) * cos(lonDiff) );
453     if (c == 0.0) {
454         // angular distance from center is 0
455         return QPointF(0.0, 0.0);
456     }
457
458     double k = c / sin(c);
459     double x, y;
460     if (ref_lat == (90 * SG_DEGREES_TO_RADIANS))
461     {
462         x = (SGD_PI / 2 - lat) * sin(lonDiff);
463         y = -(SGD_PI / 2 - lat) * cos(lonDiff);
464     }
465     else if (ref_lat == -(90 * SG_DEGREES_TO_RADIANS))
466     {
467         x = (SGD_PI / 2 + lat) * sin(lonDiff);
468         y = (SGD_PI / 2 + lat) * cos(lonDiff);
469     }
470     else
471     {
472         x = k * cos(lat) * sin(lonDiff);
473         y = k * ( cos(ref_lat) * sin(lat) - sin(ref_lat) * cos(lat) * cos(lonDiff) );
474     }
475
476     // flip for top-left origin
477     return QPointF(x, -y) * r;
478 }
479
480 SGGeod BaseDiagram::unproject(const QPointF& xy, const SGGeod& center)
481 {
482     double r = earth_radius_lat(center.getLatitudeRad());
483     double lat = 0,
484            lon = 0,
485            ref_lat = center.getLatitudeRad(),
486            ref_lon = center.getLongitudeRad(),
487            rho = QVector2D(xy).length(),
488            c = rho/r;
489
490     if (rho == 0) {
491         return center;
492     }
493
494     // invert y to balance the equivalent in project()
495     double x = xy.x(),
496             y = -xy.y();
497     lat = asin( cos(c) * sin(ref_lat) + (y * sin(c) * cos(ref_lat)) / rho);
498
499     if (ref_lat == (90 * SG_DEGREES_TO_RADIANS)) // north pole
500     {
501         lon = ref_lon + atan(-x/y);
502     }
503     else if (ref_lat == -(90 * SG_DEGREES_TO_RADIANS)) // south pole
504     {
505         lon = ref_lon + atan(x/y);
506     }
507     else
508     {
509         lon = ref_lon + atan(x* sin(c) / (rho * cos(ref_lat) * cos(c) - y * sin(ref_lat) * sin(c)));
510     }
511
512     return SGGeod::fromRad(lon, lat);
513 }
514
515 QPointF BaseDiagram::project(const SGGeod& geod) const
516 {
517     return project(geod, m_projectionCenter);
518 }
519
520 QPixmap BaseDiagram::iconForPositioned(const FGPositionedRef& pos,
521                                        const IconOptions& options)
522 {
523     // if airport type, check towered or untowered
524     bool small = options.testFlag(SmallIcons);
525
526     bool isTowered = false;
527     if (FGAirport::isAirportType(pos)) {
528         FGAirport* apt = static_cast<FGAirport*>(pos.ptr());
529         isTowered = apt->hasTower();
530     }
531
532     switch (pos->type()) {
533     case FGPositioned::VOR:
534         if (static_cast<FGNavRecord*>(pos.ptr())->isVORTAC())
535             return QPixmap(":/vortac-icon");
536
537         if (static_cast<FGNavRecord*>(pos.ptr())->hasDME())
538             return QPixmap(":/vor-dme-icon");
539
540         return QPixmap(":/vor-icon");
541
542     case FGPositioned::AIRPORT:
543         return iconForAirport(static_cast<FGAirport*>(pos.ptr()), options);
544
545     case FGPositioned::HELIPORT:
546         return QPixmap(":/heliport-icon");
547     case FGPositioned::SEAPORT:
548         return QPixmap(isTowered ? ":/seaport-tower-icon" : ":/seaport-icon");
549     case FGPositioned::NDB:
550         return QPixmap(small ? ":/ndb-small-icon" : ":/ndb-icon");
551     case FGPositioned::FIX:
552         return QPixmap(":/waypoint-icon");
553
554     default:
555         break;
556     }
557
558     return QPixmap();
559 }
560
561 QPixmap BaseDiagram::iconForAirport(FGAirport* apt, const IconOptions& options)
562 {
563     if (apt->isClosed()) {
564         return QPixmap(":/airport-closed-icon");
565     }
566
567     if (!apt->hasHardRunwayOfLengthFt(1500)) {
568         return QPixmap(apt->hasTower() ? ":/airport-tower-icon" : ":/airport-icon");
569     }
570
571     if (options.testFlag(LargeAirportPlans) && apt->hasHardRunwayOfLengthFt(8500)) {
572         QPixmap result(32, 32);
573         result.fill(Qt::transparent);
574         {
575             QPainter p(&result);
576             p.setRenderHint(QPainter::Antialiasing, true);
577             QRectF b = result.rect().adjusted(4, 4, -4, -4);
578             QVector<QLineF> lines = projectAirportRuwaysIntoRect(apt, b);
579
580             p.setPen(QPen(QColor(0x03, 0x83, 0xbf), 8));
581             p.drawLines(lines);
582
583             p.setPen(QPen(Qt::white, 2));
584             p.drawLines(lines);
585         }
586         return result;
587     }
588
589     QPixmap result(25, 25);
590     result.fill(Qt::transparent);
591
592     {
593         QPainter p(&result);
594         p.setRenderHint(QPainter::Antialiasing, true);
595         p.setPen(Qt::NoPen);
596
597         p.setBrush(apt->hasTower() ? QColor(0x03, 0x83, 0xbf) :
598                                      QColor(0x9b, 0x5d, 0xa2));
599         p.drawEllipse(QPointF(13, 13), 10, 10);
600
601         FGRunwayRef r = apt->longestRunway();
602
603         p.setPen(QPen(Qt::white, 2));
604         p.translate(13, 13);
605         p.rotate(r->headingDeg());
606         p.drawLine(0, -8, 0, 8);
607     }
608
609     return result;
610 }
611
612 QVector<QLineF> BaseDiagram::projectAirportRuwaysWithCenter(FGAirportRef apt, const SGGeod& c)
613 {
614     QVector<QLineF> r;
615
616     const FGRunwayList& runways(apt->getRunwaysWithoutReciprocals());
617     FGRunwayList::const_iterator it;
618
619     for (it = runways.begin(); it != runways.end(); ++it) {
620         FGRunwayRef rwy = *it;
621         QPointF p1 = project(rwy->geod(), c);
622         QPointF p2 = project(rwy->end(), c);
623         r.append(QLineF(p1, p2));
624     }
625
626     return r;
627 }
628
629 QVector<QLineF> BaseDiagram::projectAirportRuwaysIntoRect(FGAirportRef apt, const QRectF &bounds)
630 {
631     QVector<QLineF> r = projectAirportRuwaysWithCenter(apt, apt->geod());
632
633     QRectF extent;
634     Q_FOREACH(const QLineF& l, r) {
635         extendRect(extent, l.p1());
636         extendRect(extent, l.p2());
637     }
638
639  // find constraining scale factor
640     double ratioInX = bounds.width() / extent.width();
641     double ratioInY = bounds.height() / extent.height();
642
643     QTransform t;
644     t.translate(bounds.left(), bounds.top());
645     t.scale(std::min(ratioInX, ratioInY),
646             std::min(ratioInX, ratioInY));
647     t.translate(-extent.left(), -extent.top()); // move unscaled to 0,0
648
649     for (int i=0; i<r.size(); ++i) {
650         r[i] = t.map(r[i]);
651     }
652
653     return r;
654 }