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>
33 #include <Navaids/PolyLine.hxx>
35 #include "QtLauncher_fwd.hxx"
37 /* equatorial and polar earth radius */
38 const float rec = 6378137; // earth radius, equator (?)
39 const float rpol = 6356752.314f; // earth radius, polar (?)
41 const double MINIMUM_SCALE = 0.002;
43 //Returns Earth radius at a given latitude (Ellipsoide equation with two equal axis)
44 static float earth_radius_lat( float lat )
46 double a = cos(lat)/rec;
47 double b = sin(lat)/rpol;
48 return 1.0f / sqrt( a * a + b * b );
51 BaseDiagram::BaseDiagram(QWidget* pr) :
54 m_wheelAngleDeltaAccumulator(0)
56 setSizePolicy(QSizePolicy::MinimumExpanding,
57 QSizePolicy::MinimumExpanding);
58 setMinimumSize(100, 100);
61 QTransform BaseDiagram::transform() const
64 t.translate(width() / 2, height() / 2); // center projection origin in the widget
65 t.scale(m_scale, m_scale);
67 // apply any pan offset that exists
68 t.translate(m_panOffset.x(), m_panOffset.y());
69 // center the bounding box (may not be at the origin)
70 t.translate(-m_bounds.center().x(), -m_bounds.center().y());
74 void BaseDiagram::clearIgnoredNavaids()
79 void BaseDiagram::addIgnoredNavaid(FGPositionedRef pos)
81 if (isNavaidIgnored(pos))
83 m_ignored.push_back(pos);
86 void BaseDiagram::extendRect(QRectF &r, const QPointF &p)
88 if (p.x() < r.left()) {
90 } else if (p.x() > r.right()) {
94 if (p.y() < r.top()) {
96 } else if (p.y() > r.bottom()) {
101 void BaseDiagram::paintEvent(QPaintEvent* pe)
104 p.setRenderHints(QPainter::Antialiasing);
105 p.fillRect(rect(), QColor(0x3f, 0x3f, 0x3f));
107 if (m_autoScalePan) {
108 // fit bounds within our available space, allowing for a margin
109 const int MARGIN = 32; // pixels
110 double ratioInX = (width() - MARGIN * 2) / m_bounds.width();
111 double ratioInY = (height() - MARGIN * 2) / m_bounds.height();
112 m_scale = std::min(ratioInX, ratioInY);
115 QTransform t(transform());
118 paintPolygonData(&p);
125 void BaseDiagram::paintAirplaneIcon(QPainter* painter, const SGGeod& geod, int headingDeg)
127 QPointF pos = project(geod);
128 QPixmap pix(":/airplane-icon");
129 pos = painter->transform().map(pos);
130 painter->resetTransform();
131 painter->translate(pos.x(), pos.y());
132 painter->rotate(headingDeg);
134 painter->setRenderHint(QPainter::SmoothPixmapTransform, true);
135 QRect airplaneIconRect = pix.rect();
136 airplaneIconRect.moveCenter(QPoint(0,0));
137 painter->drawPixmap(airplaneIconRect, pix);
140 void BaseDiagram::paintPolygonData(QPainter* painter)
142 QTransform xf = painter->transform();
143 QTransform invT = xf.inverted();
145 SGGeod topLeft = unproject(invT.map(rect().topLeft()), m_projectionCenter);
146 SGGeod viewCenter = unproject(invT.map(rect().center()), m_projectionCenter);
147 SGGeod bottomRight = unproject(invT.map(rect().bottomRight()), m_projectionCenter);
149 double drawRangeNm = std::max(SGGeodesy::distanceNm(viewCenter, topLeft),
150 SGGeodesy::distanceNm(viewCenter, bottomRight));
152 flightgear::PolyLineList lines(flightgear::PolyLine::linesNearPos(viewCenter, drawRangeNm,
153 flightgear::PolyLine::COASTLINE));
155 QPen waterPen(QColor(64, 64, 255), 1);
156 waterPen.setCosmetic(true);
157 painter->setPen(waterPen);
158 flightgear::PolyLineList::const_iterator it;
159 for (it=lines.begin(); it != lines.end(); ++it) {
160 paintGeodVec(painter, (*it)->points());
163 lines = flightgear::PolyLine::linesNearPos(viewCenter, drawRangeNm,
164 flightgear::PolyLine::URBAN);
165 for (it=lines.begin(); it != lines.end(); ++it) {
166 fillClosedGeodVec(painter, QColor(192, 192, 96), (*it)->points());
169 lines = flightgear::PolyLine::linesNearPos(viewCenter, drawRangeNm,
170 flightgear::PolyLine::RIVER);
172 painter->setPen(waterPen);
173 for (it=lines.begin(); it != lines.end(); ++it) {
174 paintGeodVec(painter, (*it)->points());
178 lines = flightgear::PolyLine::linesNearPos(viewCenter, drawRangeNm,
179 flightgear::PolyLine::LAKE);
181 for (it=lines.begin(); it != lines.end(); ++it) {
182 fillClosedGeodVec(painter, QColor(128, 128, 255),
189 void BaseDiagram::paintGeodVec(QPainter* painter, const flightgear::SGGeodVec& vec)
191 QVector<QPointF> projected;
192 projected.reserve(vec.size());
193 flightgear::SGGeodVec::const_iterator it;
194 for (it=vec.begin(); it != vec.end(); ++it) {
195 projected.append(project(*it));
198 painter->drawPolyline(projected.data(), projected.size());
201 void BaseDiagram::fillClosedGeodVec(QPainter* painter, const QColor& color, const flightgear::SGGeodVec& vec)
203 QVector<QPointF> projected;
204 projected.reserve(vec.size());
205 flightgear::SGGeodVec::const_iterator it;
206 for (it=vec.begin(); it != vec.end(); ++it) {
207 projected.append(project(*it));
210 painter->setPen(Qt::NoPen);
211 painter->setBrush(color);
212 painter->drawPolygon(projected.data(), projected.size());
215 class MapFilter : public FGPositioned::TypeFilter
219 MapFilter(LauncherAircraftType aircraft)
221 // addType(FGPositioned::FIX);
222 addType(FGPositioned::NDB);
223 addType(FGPositioned::VOR);
225 if (aircraft == Helicopter) {
226 addType(FGPositioned::HELIPAD);
229 if (aircraft == Seaplane) {
230 addType(FGPositioned::SEAPORT);
232 addType(FGPositioned::AIRPORT);
236 virtual bool pass(FGPositioned* aPos) const
238 bool ok = TypeFilter::pass(aPos);
239 // fix-filtering code disabled since fixed are entirely disabled
241 if (ok && (aPos->type() == FGPositioned::FIX)) {
242 // ignore fixes which end in digits
243 if (aPos->ident().length() > 4 && isdigit(aPos->ident()[3]) && isdigit(aPos->ident()[4])) {
252 void BaseDiagram::splitItems(const FGPositionedList& in, FGPositionedList& navaids,
253 FGPositionedList& ports)
255 FGPositionedList::const_iterator it = in.begin();
256 for (; it != in.end(); ++it) {
257 if (FGAirport::isAirportType(it->ptr())) {
258 ports.push_back(*it);
260 navaids.push_back(*it);
265 bool orderAirportsByRunwayLength(const FGPositionedRef& a,
266 const FGPositionedRef& b)
268 FGAirport* aptA = static_cast<FGAirport*>(a.ptr());
269 FGAirport* aptB = static_cast<FGAirport*>(b.ptr());
271 return aptA->longestRunway()->lengthFt() > aptB->longestRunway()->lengthFt();
274 void BaseDiagram::paintNavaids(QPainter* painter)
276 QTransform xf = painter->transform();
277 painter->setTransform(QTransform()); // reset to identity
278 QTransform invT = xf.inverted();
281 SGGeod topLeft = unproject(invT.map(rect().topLeft()), m_projectionCenter);
282 SGGeod viewCenter = unproject(invT.map(rect().center()), m_projectionCenter);
283 SGGeod bottomRight = unproject(invT.map(rect().bottomRight()), m_projectionCenter);
285 double drawRangeNm = std::max(SGGeodesy::distanceNm(viewCenter, topLeft),
286 SGGeodesy::distanceNm(viewCenter, bottomRight));
288 MapFilter f(m_aircraftType);
289 FGPositionedList items = FGPositioned::findWithinRange(viewCenter, drawRangeNm, &f);
291 FGPositionedList navaids, ports;
292 splitItems(items, navaids, ports);
294 if (ports.size() >= 40) {
295 FGPositionedList::iterator middle = ports.begin() + 40;
296 std::partial_sort(ports.begin(), middle, ports.end(),
297 orderAirportsByRunwayLength);
301 m_labelRects.clear();
302 m_labelRects.reserve(items.size());
304 FGPositionedList::const_iterator it;
305 for (it = ports.begin(); it != ports.end(); ++it) {
306 paintNavaid(painter, xf, *it);
309 for (it = navaids.begin(); it != navaids.end(); ++it) {
310 paintNavaid(painter, xf, *it);
315 painter->setTransform(xf);
318 QRect boundsOfLines(const QVector<QLineF>& lines)
321 Q_FOREACH(const QLineF& l, lines) {
322 r = r.united(QRectF(l.p1(), l.p2()).toRect());
328 void BaseDiagram::paintNavaid(QPainter* painter, const QTransform& t, const FGPositionedRef &pos)
330 if (isNavaidIgnored(pos))
333 bool drawAsIcon = true;
334 const double minRunwayLengthFt = (16 / m_scale) * SG_METER_TO_FEET;
335 const FGPositioned::Type ty(pos->type());
336 const bool isNDB = (ty == FGPositioned::NDB);
339 if (ty == FGPositioned::AIRPORT) {
340 FGAirport* apt = static_cast<FGAirport*>(pos.ptr());
341 if (apt->hasHardRunwayOfLengthFt(minRunwayLengthFt)) {
344 painter->setTransform(t);
345 QVector<QLineF> lines = projectAirportRuwaysWithCenter(apt, m_projectionCenter);
347 QPen pen(QColor(0x03, 0x83, 0xbf), 8);
348 pen.setCosmetic(true);
349 painter->setPen(pen);
350 painter->drawLines(lines);
352 QPen linePen(Qt::white, 2);
353 linePen.setCosmetic(true);
354 painter->setPen(linePen);
355 painter->drawLines(lines);
357 painter->resetTransform();
359 iconRect = t.mapRect(boundsOfLines(lines));
364 QPixmap pm = iconForPositioned(pos);
365 QPointF loc = t.map(project(pos->geod()));
366 iconRect = pm.rect();
367 iconRect.moveCenter(loc.toPoint());
368 painter->drawPixmap(iconRect, pm);
371 // compute label text so we can measure it
373 if (FGAirport::isAirportType(pos.ptr())) {
374 label = QString::fromStdString(pos->name());
375 label = fixNavaidName(label);
377 label = QString::fromStdString(pos->ident());
380 if (ty == FGPositioned::NDB) {
381 FGNavRecord* nav = static_cast<FGNavRecord*>(pos.ptr());
382 label.append("\n").append(QString::number(nav->get_freq() / 100));
383 } else if (ty == FGPositioned::VOR) {
384 FGNavRecord* nav = static_cast<FGNavRecord*>(pos.ptr());
385 label.append("\n").append(QString::number(nav->get_freq() / 100.0, 'f', 1));
388 QRect textBounds = painter->boundingRect(QRect(0, 0, 100, 100),
389 Qt::TextWordWrap, label);
391 textBounds = rectAndFlagsForLabel(pos->guid(), iconRect,
395 painter->setPen(isNDB ? QColor(0x9b, 0x5d, 0xa2) : QColor(0x03, 0x83, 0xbf));
396 painter->drawText(textBounds, textFlags, label);
399 bool BaseDiagram::isNavaidIgnored(const FGPositionedRef &pos) const
401 return m_ignored.contains(pos);
404 bool BaseDiagram::isLabelRectAvailable(const QRect &r) const
406 Q_FOREACH(const QRect& lr, m_labelRects) {
407 if (lr.intersects(r))
414 int BaseDiagram::textFlagsForLabelPosition(LabelPosition pos)
418 case LABEL_RIGHT: return Qt::AlignLeft | Qt::AlignVCenter;
419 case LABEL_ABOVE: return Qt::AlignHCenter | Qt::A
425 QRect BaseDiagram::rectAndFlagsForLabel(PositionedID guid, const QRect& item,
429 m_labelRects.append(item);
430 int pos = m_labelPositions.value(guid, LABEL_RIGHT);
431 bool firstAttempt = true;
432 flags = Qt::TextWordWrap;
434 while (pos < LAST_POSITION) {
435 QRect r = labelPositioned(item, bounds, static_cast<LabelPosition>(pos));
436 if (isLabelRectAvailable(r)) {
437 m_labelRects.append(r);
438 m_labelPositions[guid] = static_cast<LabelPosition>(pos);
439 flags |= textFlagsForLabelPosition(static_cast<LabelPosition>(pos));
441 } else if (firstAttempt && (pos != LABEL_RIGHT)) {
447 firstAttempt = false;
450 return QRect(item.x(), item.y(), bounds.width(), bounds.height());
453 QRect BaseDiagram::labelPositioned(const QRect& itemRect,
455 LabelPosition lp) const
457 const int SHORT_MARGIN = 4;
458 const int DIAGONAL_MARGIN = 12;
460 QPoint topLeft = itemRect.topLeft();
463 // cardinal compass points are short (close in)
465 topLeft = QPoint(itemRect.right() + SHORT_MARGIN,
466 itemRect.center().y() - bounds.height() / 2);
469 topLeft = QPoint(itemRect.center().x() - (bounds.width() / 2),
470 itemRect.top() - (SHORT_MARGIN + bounds.height()));
473 topLeft = QPoint(itemRect.center().x() - (bounds.width() / 2),
474 itemRect.bottom() + SHORT_MARGIN);
477 topLeft = QPoint(itemRect.left() - (SHORT_MARGIN + bounds.width()),
478 itemRect.center().y() - bounds.height() / 2);
481 // first diagonals are further out (to hopefully have a better chance
482 // of finding clear space
485 topLeft = QPoint(itemRect.right() + DIAGONAL_MARGIN,
486 itemRect.top() - (DIAGONAL_MARGIN + bounds.height()));
490 topLeft = QPoint(itemRect.left() - (DIAGONAL_MARGIN + bounds.width()),
491 itemRect.top() - (DIAGONAL_MARGIN + bounds.height()));
495 topLeft = QPoint(itemRect.right() + DIAGONAL_MARGIN,
496 itemRect.bottom() + DIAGONAL_MARGIN);
500 topLeft = QPoint(itemRect.left() - (DIAGONAL_MARGIN + bounds.width()),
501 itemRect.bottom() + DIAGONAL_MARGIN);
504 qWarning() << Q_FUNC_INFO << "Implement me";
508 return QRect(topLeft, bounds);
511 void BaseDiagram::mousePressEvent(QMouseEvent *me)
513 m_lastMousePos = me->pos();
517 void BaseDiagram::mouseMoveEvent(QMouseEvent *me)
519 m_autoScalePan = false;
521 QPointF delta = me->pos() - m_lastMousePos;
522 m_lastMousePos = me->pos();
524 // offset is stored in metres so we don't have to modify it when
526 m_panOffset += (delta / m_scale);
534 return (v == 0) ? 0 : (v < 0) ? -1 : 1;
537 void BaseDiagram::wheelEvent(QWheelEvent *we)
539 m_autoScalePan = false;
541 int delta = we->angleDelta().y();
545 if (intSign(m_wheelAngleDeltaAccumulator) != intSign(delta)) {
546 m_wheelAngleDeltaAccumulator = 0;
549 m_wheelAngleDeltaAccumulator += delta;
550 if (m_wheelAngleDeltaAccumulator > 120) {
551 m_wheelAngleDeltaAccumulator = 0;
555 } else if (m_wheelAngleDeltaAccumulator < -120) {
556 m_wheelAngleDeltaAccumulator = 0;
561 SG_CLAMP_RANGE(m_scale, MINIMUM_SCALE, 1.0);
565 void BaseDiagram::paintContents(QPainter* painter)
569 void BaseDiagram::recomputeBounds(bool resetZoom)
575 m_autoScalePan = true;
577 m_panOffset = QPointF();
583 void BaseDiagram::doComputeBounds()
585 // no-op in the base class
588 void BaseDiagram::extendBounds(const QPointF& p)
590 extendRect(m_bounds, p);
593 QPointF BaseDiagram::project(const SGGeod& geod, const SGGeod& center)
595 double r = earth_radius_lat(geod.getLatitudeRad());
596 double ref_lat = center.getLatitudeRad(),
597 ref_lon = center.getLongitudeRad(),
598 lat = geod.getLatitudeRad(),
599 lon = geod.getLongitudeRad(),
600 lonDiff = lon - ref_lon;
602 double c = acos( sin(ref_lat) * sin(lat) + cos(ref_lat) * cos(lat) * cos(lonDiff) );
604 // angular distance from center is 0
605 return QPointF(0.0, 0.0);
608 double k = c / sin(c);
610 if (ref_lat == (90 * SG_DEGREES_TO_RADIANS))
612 x = (SGD_PI / 2 - lat) * sin(lonDiff);
613 y = -(SGD_PI / 2 - lat) * cos(lonDiff);
615 else if (ref_lat == -(90 * SG_DEGREES_TO_RADIANS))
617 x = (SGD_PI / 2 + lat) * sin(lonDiff);
618 y = (SGD_PI / 2 + lat) * cos(lonDiff);
622 x = k * cos(lat) * sin(lonDiff);
623 y = k * ( cos(ref_lat) * sin(lat) - sin(ref_lat) * cos(lat) * cos(lonDiff) );
626 // flip for top-left origin
627 return QPointF(x, -y) * r;
630 SGGeod BaseDiagram::unproject(const QPointF& xy, const SGGeod& center)
632 double r = earth_radius_lat(center.getLatitudeRad());
635 ref_lat = center.getLatitudeRad(),
636 ref_lon = center.getLongitudeRad(),
637 rho = QVector2D(xy).length(),
644 // invert y to balance the equivalent in project()
647 lat = asin( cos(c) * sin(ref_lat) + (y * sin(c) * cos(ref_lat)) / rho);
649 if (ref_lat == (90 * SG_DEGREES_TO_RADIANS)) // north pole
651 lon = ref_lon + atan(-x/y);
653 else if (ref_lat == -(90 * SG_DEGREES_TO_RADIANS)) // south pole
655 lon = ref_lon + atan(x/y);
659 lon = ref_lon + atan(x* sin(c) / (rho * cos(ref_lat) * cos(c) - y * sin(ref_lat) * sin(c)));
662 return SGGeod::fromRad(lon, lat);
665 QPointF BaseDiagram::project(const SGGeod& geod) const
667 return project(geod, m_projectionCenter);
670 QPixmap BaseDiagram::iconForPositioned(const FGPositionedRef& pos,
671 const IconOptions& options)
673 // if airport type, check towered or untowered
674 bool small = options.testFlag(SmallIcons);
676 bool isTowered = false;
677 if (FGAirport::isAirportType(pos)) {
678 FGAirport* apt = static_cast<FGAirport*>(pos.ptr());
679 isTowered = apt->hasTower();
682 switch (pos->type()) {
683 case FGPositioned::VOR:
684 if (static_cast<FGNavRecord*>(pos.ptr())->isVORTAC())
685 return QPixmap(":/vortac-icon");
687 if (static_cast<FGNavRecord*>(pos.ptr())->hasDME())
688 return QPixmap(":/vor-dme-icon");
690 return QPixmap(":/vor-icon");
692 case FGPositioned::AIRPORT:
693 return iconForAirport(static_cast<FGAirport*>(pos.ptr()), options);
695 case FGPositioned::HELIPORT:
696 return QPixmap(":/heliport-icon");
697 case FGPositioned::SEAPORT:
698 return QPixmap(isTowered ? ":/seaport-tower-icon" : ":/seaport-icon");
699 case FGPositioned::NDB:
700 return QPixmap(small ? ":/ndb-small-icon" : ":/ndb-icon");
701 case FGPositioned::FIX:
702 return QPixmap(":/waypoint-icon");
711 QPixmap BaseDiagram::iconForAirport(FGAirport* apt, const IconOptions& options)
713 if (apt->isClosed()) {
714 return QPixmap(":/airport-closed-icon");
717 if (!apt->hasHardRunwayOfLengthFt(1500)) {
718 return QPixmap(apt->hasTower() ? ":/airport-tower-icon" : ":/airport-icon");
721 if (options.testFlag(LargeAirportPlans) && apt->hasHardRunwayOfLengthFt(8500)) {
722 QPixmap result(32, 32);
723 result.fill(Qt::transparent);
726 p.setRenderHint(QPainter::Antialiasing, true);
727 QRectF b = result.rect().adjusted(4, 4, -4, -4);
728 QVector<QLineF> lines = projectAirportRuwaysIntoRect(apt, b);
730 p.setPen(QPen(QColor(0x03, 0x83, 0xbf), 8));
733 p.setPen(QPen(Qt::white, 2));
739 QPixmap result(25, 25);
740 result.fill(Qt::transparent);
744 p.setRenderHint(QPainter::Antialiasing, true);
747 p.setBrush(apt->hasTower() ? QColor(0x03, 0x83, 0xbf) :
748 QColor(0x9b, 0x5d, 0xa2));
749 p.drawEllipse(QPointF(13, 13), 10, 10);
751 FGRunwayRef r = apt->longestRunway();
753 p.setPen(QPen(Qt::white, 2));
755 p.rotate(r->headingDeg());
756 p.drawLine(0, -8, 0, 8);
762 QVector<QLineF> BaseDiagram::projectAirportRuwaysWithCenter(FGAirportRef apt, const SGGeod& c)
766 const FGRunwayList& runways(apt->getRunwaysWithoutReciprocals());
767 FGRunwayList::const_iterator it;
769 for (it = runways.begin(); it != runways.end(); ++it) {
770 FGRunwayRef rwy = *it;
771 QPointF p1 = project(rwy->geod(), c);
772 QPointF p2 = project(rwy->end(), c);
773 r.append(QLineF(p1, p2));
779 void BaseDiagram::setAircraftType(LauncherAircraftType type)
781 m_aircraftType = type;
785 QVector<QLineF> BaseDiagram::projectAirportRuwaysIntoRect(FGAirportRef apt, const QRectF &bounds)
787 QVector<QLineF> r = projectAirportRuwaysWithCenter(apt, apt->geod());
790 Q_FOREACH(const QLineF& l, r) {
791 extendRect(extent, l.p1());
792 extendRect(extent, l.p2());
795 // find constraining scale factor
796 double ratioInX = bounds.width() / extent.width();
797 double ratioInY = bounds.height() / extent.height();
800 t.translate(bounds.left(), bounds.top());
801 t.scale(std::min(ratioInX, ratioInY),
802 std::min(ratioInX, ratioInY));
803 t.translate(-extent.left(), -extent.top()); // move unscaled to 0,0
805 for (int i=0; i<r.size(); ++i) {