1 // BaseDiagram.cxx - part of GUI launcher using Qt5
3 // Written by James Turner, started December 2014.
5 // Copyright (C) 2014 James Turner <zakalawe@mac.com>
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.
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.
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.
21 #include "BaseDiagram.hxx"
28 #include <QMouseEvent>
30 #include <Navaids/navrecord.hxx>
31 #include <Navaids/positioned.hxx>
32 #include <Airports/airport.hxx>
34 /* equatorial and polar earth radius */
35 const float rec = 6378137; // earth radius, equator (?)
36 const float rpol = 6356752.314f; // earth radius, polar (?)
38 //Returns Earth radius at a given latitude (Ellipsoide equation with two equal axis)
39 static float earth_radius_lat( float lat )
41 double a = cos(lat)/rec;
42 double b = sin(lat)/rpol;
43 return 1.0f / sqrt( a * a + b * b );
46 BaseDiagram::BaseDiagram(QWidget* pr) :
49 m_wheelAngleDeltaAccumulator(0)
51 setSizePolicy(QSizePolicy::MinimumExpanding,
52 QSizePolicy::MinimumExpanding);
53 setMinimumSize(100, 100);
56 QTransform BaseDiagram::transform() const
59 t.translate(width() / 2, height() / 2); // center projection origin in the widget
60 t.scale(m_scale, m_scale);
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());
69 void BaseDiagram::clearIgnoredNavaids()
74 void BaseDiagram::addIgnoredNavaid(FGPositionedRef pos)
76 if (isNavaidIgnored(pos))
78 m_ignored.push_back(pos);
81 void BaseDiagram::extendRect(QRectF &r, const QPointF &p)
83 if (p.x() < r.left()) {
85 } else if (p.x() > r.right()) {
89 if (p.y() < r.top()) {
91 } else if (p.y() > r.bottom()) {
96 void BaseDiagram::paintEvent(QPaintEvent* pe)
99 p.setRenderHints(QPainter::Antialiasing);
100 p.fillRect(rect(), QColor(0x3f, 0x3f, 0x3f));
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);
110 QTransform t(transform());
118 void BaseDiagram::paintAirplaneIcon(QPainter* painter, const SGGeod& geod, int headingDeg)
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);
127 painter->setRenderHint(QPainter::SmoothPixmapTransform, true);
128 QRect airplaneIconRect = pix.rect();
129 airplaneIconRect.moveCenter(QPoint(0,0));
130 painter->drawPixmap(airplaneIconRect, pix);
133 class MapFilter : public FGPositioned::TypeFilter
138 // addType(FGPositioned::FIX);
139 addType(FGPositioned::AIRPORT);
140 addType(FGPositioned::HELIPORT);
141 addType(FGPositioned::SEAPORT);
142 addType(FGPositioned::NDB);
143 addType(FGPositioned::VOR);
146 virtual bool pass(FGPositioned* aPos) const
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])) {
161 void BaseDiagram::paintNavaids(QPainter* painter)
163 QTransform xf = painter->transform();
164 painter->setTransform(QTransform()); // reset to identity
165 QTransform invT = xf.inverted();
166 SGGeod topLeft = unproject(invT.map(QPointF(0,0)), m_projectionCenter);
168 double minRunwayLengthFt = (16 / m_scale) * SG_METER_TO_FEET;
169 // add 10nm fudge factor
170 double drawRangeNm = SGGeodesy::distanceNm(m_projectionCenter, topLeft) + 10.0;
171 //qDebug() << "draw range computed as:" << drawRangeNm;
175 FGPositionedList items = FGPositioned::findWithinRange(m_projectionCenter, drawRangeNm, &f);
177 m_labelRects.clear();
178 m_labelRects.reserve(items.size());
180 FGPositionedList::const_iterator it;
181 for (it = items.begin(); it != items.end(); ++it) {
182 FGPositionedRef pos(*it);
183 bool drawAsIcon = true;
184 if (isNavaidIgnored(pos))
187 FGPositioned::Type ty(pos->type());
188 if (ty == FGPositioned::AIRPORT) {
189 FGAirport* apt = static_cast<FGAirport*>(pos.ptr());
190 if (apt->hasHardRunwayOfLengthFt(minRunwayLengthFt)) {
193 painter->setTransform(xf);
194 QVector<QLineF> lines = projectAirportRuwaysWithCenter(apt, m_projectionCenter);
196 QPen pen(QColor(0x03, 0x83, 0xbf), 8);
197 pen.setCosmetic(true);
198 painter->setPen(pen);
199 painter->drawLines(lines);
201 QPen linePen(Qt::white, 2);
202 linePen.setCosmetic(true);
203 painter->setPen(linePen);
204 painter->drawLines(lines);
206 painter->resetTransform();
211 QPixmap pm = iconForPositioned(pos);
212 QPointF loc = xf.map(project(pos->geod()));
213 QRect iconRect = pm.rect();
214 iconRect.moveCenter(loc.toPoint());
215 painter->drawPixmap(iconRect, pm);
216 bool isNDB = (ty == FGPositioned::NDB);
218 // compute label text so we can measure it
220 if (FGAirport::isAirportType(pos.ptr())) {
221 label = QString::fromStdString((*it)->name());
223 label = QString::fromStdString((*it)->ident());
226 if (ty == FGPositioned::NDB) {
227 FGNavRecord* nav = static_cast<FGNavRecord*>(pos.ptr());
228 label.append("\n").append(QString::number(nav->get_freq() / 100));
229 } else if (ty == FGPositioned::VOR) {
230 FGNavRecord* nav = static_cast<FGNavRecord*>(pos.ptr());
231 label.append("\n").append(QString::number(nav->get_freq() / 100.0, 'f', 1));
234 QRect textBounds = painter->boundingRect(QRect(0, 0, 100, 100),
235 Qt::TextWordWrap, label);
237 textBounds = rectAndFlagsForLabel(pos->guid(), iconRect,
241 painter->setPen(isNDB ? QColor(0x9b, 0x5d, 0xa2) : QColor(0x03, 0x83, 0xbf));
242 painter->drawText(textBounds, textFlags, label);
247 painter->setTransform(xf);
250 bool BaseDiagram::isNavaidIgnored(const FGPositionedRef &pos) const
252 return m_ignored.contains(pos);
255 bool BaseDiagram::isLabelRectAvailable(const QRect &r) const
257 Q_FOREACH(const QRect& lr, m_labelRects) {
258 if (lr.intersects(r))
265 int BaseDiagram::textFlagsForLabelPosition(LabelPosition pos)
269 case LABEL_RIGHT: return Qt::AlignLeft | Qt::AlignVCenter;
270 case LABEL_ABOVE: return Qt::AlignHCenter | Qt::A
276 QRect BaseDiagram::rectAndFlagsForLabel(PositionedID guid, const QRect& item,
280 m_labelRects.append(item);
281 int pos = m_labelPositions.value(guid, LABEL_RIGHT);
282 bool firstAttempt = true;
283 flags = Qt::TextWordWrap;
285 while (pos < LAST_POSITION) {
286 QRect r = labelPositioned(item, bounds, static_cast<LabelPosition>(pos));
287 if (isLabelRectAvailable(r)) {
288 m_labelRects.append(r);
289 m_labelPositions[guid] = static_cast<LabelPosition>(pos);
290 flags |= textFlagsForLabelPosition(static_cast<LabelPosition>(pos));
292 } else if (firstAttempt && (pos != LABEL_RIGHT)) {
298 firstAttempt = false;
301 return QRect(item.x(), item.y(), bounds.width(), bounds.height());
304 QRect BaseDiagram::labelPositioned(const QRect& itemRect,
306 LabelPosition lp) const
308 const int SHORT_MARGIN = 4;
309 const int DIAGONAL_MARGIN = 12;
311 QPoint topLeft = itemRect.topLeft();
314 // cardinal compass points are short (close in)
316 topLeft = QPoint(itemRect.right() + SHORT_MARGIN,
317 itemRect.center().y() - bounds.height() / 2);
320 topLeft = QPoint(itemRect.center().x() - (bounds.width() / 2),
321 itemRect.top() - (SHORT_MARGIN + bounds.height()));
324 topLeft = QPoint(itemRect.center().x() - (bounds.width() / 2),
325 itemRect.bottom() + SHORT_MARGIN);
328 topLeft = QPoint(itemRect.left() - (SHORT_MARGIN + bounds.width()),
329 itemRect.center().y() - bounds.height() / 2);
332 // first diagonals are further out (to hopefully have a better chance
333 // of finding clear space
336 topLeft = QPoint(itemRect.right() + DIAGONAL_MARGIN,
337 itemRect.top() - (DIAGONAL_MARGIN + bounds.height()));
341 topLeft = QPoint(itemRect.left() - (DIAGONAL_MARGIN + bounds.width()),
342 itemRect.top() - (DIAGONAL_MARGIN + bounds.height()));
346 topLeft = QPoint(itemRect.right() + DIAGONAL_MARGIN,
347 itemRect.bottom() + DIAGONAL_MARGIN);
351 topLeft = QPoint(itemRect.left() - (DIAGONAL_MARGIN + bounds.width()),
352 itemRect.bottom() + DIAGONAL_MARGIN);
355 qWarning() << Q_FUNC_INFO << "Implement me";
359 return QRect(topLeft, bounds);
362 void BaseDiagram::mousePressEvent(QMouseEvent *me)
364 m_lastMousePos = me->pos();
368 void BaseDiagram::mouseMoveEvent(QMouseEvent *me)
370 m_autoScalePan = false;
372 QPointF delta = me->pos() - m_lastMousePos;
373 m_lastMousePos = me->pos();
375 // offset is stored in metres so we don't have to modify it when
377 m_panOffset += (delta / m_scale);
385 return (v == 0) ? 0 : (v < 0) ? -1 : 1;
388 void BaseDiagram::wheelEvent(QWheelEvent *we)
390 m_autoScalePan = false;
392 int delta = we->angleDelta().y();
396 if (intSign(m_wheelAngleDeltaAccumulator) != intSign(delta)) {
397 m_wheelAngleDeltaAccumulator = 0;
400 m_wheelAngleDeltaAccumulator += delta;
401 if (m_wheelAngleDeltaAccumulator > 120) {
402 m_wheelAngleDeltaAccumulator = 0;
404 } else if (m_wheelAngleDeltaAccumulator < -120) {
405 m_wheelAngleDeltaAccumulator = 0;
412 void BaseDiagram::paintContents(QPainter* painter)
416 void BaseDiagram::recomputeBounds(bool resetZoom)
422 m_autoScalePan = true;
424 m_panOffset = QPointF();
430 void BaseDiagram::doComputeBounds()
432 // no-op in the base class
435 void BaseDiagram::extendBounds(const QPointF& p)
437 extendRect(m_bounds, p);
440 QPointF BaseDiagram::project(const SGGeod& geod, const SGGeod& center)
442 double r = earth_radius_lat(geod.getLatitudeRad());
443 double ref_lat = center.getLatitudeRad(),
444 ref_lon = center.getLongitudeRad(),
445 lat = geod.getLatitudeRad(),
446 lon = geod.getLongitudeRad(),
447 lonDiff = lon - ref_lon;
449 double c = acos( sin(ref_lat) * sin(lat) + cos(ref_lat) * cos(lat) * cos(lonDiff) );
451 // angular distance from center is 0
452 return QPointF(0.0, 0.0);
455 double k = c / sin(c);
457 if (ref_lat == (90 * SG_DEGREES_TO_RADIANS))
459 x = (SGD_PI / 2 - lat) * sin(lonDiff);
460 y = -(SGD_PI / 2 - lat) * cos(lonDiff);
462 else if (ref_lat == -(90 * SG_DEGREES_TO_RADIANS))
464 x = (SGD_PI / 2 + lat) * sin(lonDiff);
465 y = (SGD_PI / 2 + lat) * cos(lonDiff);
469 x = k * cos(lat) * sin(lonDiff);
470 y = k * ( cos(ref_lat) * sin(lat) - sin(ref_lat) * cos(lat) * cos(lonDiff) );
473 return QPointF(x, -y) * r;
476 SGGeod BaseDiagram::unproject(const QPointF& xy, const SGGeod& center)
478 double r = earth_radius_lat(center.getLatitudeRad());
481 ref_lat = center.getLatitudeRad(),
482 ref_lon = center.getLongitudeRad(),
483 rho = QVector2D(xy).length(),
490 double x = xy.x(), y = xy.y();
491 lat = asin( cos(c) * sin(ref_lat) + (y * sin(c) * cos(ref_lat)) / rho);
493 if (ref_lat == (90 * SG_DEGREES_TO_RADIANS)) // north pole
495 lon = ref_lon + atan(-x/y);
497 else if (ref_lat == -(90 * SG_DEGREES_TO_RADIANS)) // south pole
499 lon = ref_lon + atan(x/y);
503 lon = ref_lon + atan(x* sin(c) / (rho * cos(ref_lat) * cos(c) - y * sin(ref_lat) * sin(c)));
506 return SGGeod::fromRad(lon, lat);
509 QPointF BaseDiagram::project(const SGGeod& geod) const
511 return project(geod, m_projectionCenter);
514 QPixmap BaseDiagram::iconForPositioned(const FGPositionedRef& pos,
515 const IconOptions& options)
517 // if airport type, check towered or untowered
518 bool small = options.testFlag(SmallIcons);
520 bool isTowered = false;
521 if (FGAirport::isAirportType(pos)) {
522 FGAirport* apt = static_cast<FGAirport*>(pos.ptr());
523 isTowered = apt->hasTower();
526 switch (pos->type()) {
527 case FGPositioned::VOR:
528 if (static_cast<FGNavRecord*>(pos.ptr())->isVORTAC())
529 return QPixmap(":/vortac-icon");
531 if (static_cast<FGNavRecord*>(pos.ptr())->hasDME())
532 return QPixmap(":/vor-dme-icon");
534 return QPixmap(":/vor-icon");
536 case FGPositioned::AIRPORT:
537 return iconForAirport(static_cast<FGAirport*>(pos.ptr()), options);
539 case FGPositioned::HELIPORT:
540 return QPixmap(":/heliport-icon");
541 case FGPositioned::SEAPORT:
542 return QPixmap(isTowered ? ":/seaport-tower-icon" : ":/seaport-icon");
543 case FGPositioned::NDB:
544 return QPixmap(small ? ":/ndb-small-icon" : ":/ndb-icon");
545 case FGPositioned::FIX:
546 return QPixmap(":/waypoint-icon");
555 QPixmap BaseDiagram::iconForAirport(FGAirport* apt, const IconOptions& options)
557 if (apt->isClosed()) {
558 return QPixmap(":/airport-closed-icon");
561 if (!apt->hasHardRunwayOfLengthFt(1500)) {
562 return QPixmap(apt->hasTower() ? ":/airport-tower-icon" : ":/airport-icon");
565 if (options.testFlag(LargeAirportPlans) && apt->hasHardRunwayOfLengthFt(8500)) {
566 QPixmap result(32, 32);
567 result.fill(Qt::transparent);
570 p.setRenderHint(QPainter::Antialiasing, true);
571 QRectF b = result.rect().adjusted(4, 4, -4, -4);
572 QVector<QLineF> lines = projectAirportRuwaysIntoRect(apt, b);
574 p.setPen(QPen(QColor(0x03, 0x83, 0xbf), 8));
577 p.setPen(QPen(Qt::white, 2));
583 QPixmap result(25, 25);
584 result.fill(Qt::transparent);
588 p.setRenderHint(QPainter::Antialiasing, true);
591 p.setBrush(apt->hasTower() ? QColor(0x03, 0x83, 0xbf) :
592 QColor(0x9b, 0x5d, 0xa2));
593 p.drawEllipse(QPointF(13, 13), 10, 10);
595 FGRunwayRef r = apt->longestRunway();
597 p.setPen(QPen(Qt::white, 2));
599 p.rotate(r->headingDeg());
600 p.drawLine(0, -8, 0, 8);
606 QVector<QLineF> BaseDiagram::projectAirportRuwaysWithCenter(FGAirportRef apt, const SGGeod& c)
610 const FGRunwayList& runways(apt->getRunwaysWithoutReciprocals());
611 FGRunwayList::const_iterator it;
613 for (it = runways.begin(); it != runways.end(); ++it) {
614 FGRunwayRef rwy = *it;
615 QPointF p1 = project(rwy->geod(), c);
616 QPointF p2 = project(rwy->end(), c);
617 r.append(QLineF(p1, p2));
623 QVector<QLineF> BaseDiagram::projectAirportRuwaysIntoRect(FGAirportRef apt, const QRectF &bounds)
625 QVector<QLineF> r = projectAirportRuwaysWithCenter(apt, apt->geod());
628 Q_FOREACH(const QLineF& l, r) {
629 extendRect(extent, l.p1());
630 extendRect(extent, l.p2());
633 // find constraining scale factor
634 double ratioInX = bounds.width() / extent.width();
635 double ratioInY = bounds.height() / extent.height();
638 t.translate(bounds.left(), bounds.top());
639 t.scale(std::min(ratioInX, ratioInY),
640 std::min(ratioInX, ratioInY));
641 t.translate(-extent.left(), -extent.top()); // move unscaled to 0,0
643 for (int i=0; i<r.size(); ++i) {